🔑 feat: SAML authentication (#6169)

* feat: add SAML authentication

* refactor: change SAML icon

* refactor: resolve SAML metadata paths using paths.js

* test: add samlStrategy tests

* fix: update setupSaml import

* test: add SAML settings tests in config.spec.js

* test: add client tests

* refactor: improve SAML button label and fallback localization

* feat: allow only one authentication method OpenID or SAML at a time

* doc: add SAML configuration sample to docker-compose.override

* fix: require SAML_SESSION_SECRET to enable SAML

* feat: update samlStrategy

* test: update samle tests

* feat: add SAML login button label to translations and remove default value

* fix: update SAML cert file binding

* chore: update override example with SAML cert volume

* fix: update SAML session handling with Redis backend

---------

Co-authored-by: Ruben Talstra <RubenTalstra1211@outlook.com>
This commit is contained in:
tsutsu3 2025-05-30 00:00:58 +09:00 committed by GitHub
parent 87255dac81
commit 939b4ce659
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 1134 additions and 20 deletions

View file

@ -24,6 +24,12 @@ afterEach(() => {
delete process.env.GITHUB_CLIENT_SECRET;
delete process.env.DISCORD_CLIENT_ID;
delete process.env.DISCORD_CLIENT_SECRET;
delete process.env.SAML_ENTRY_POINT;
delete process.env.SAML_ISSUER;
delete process.env.SAML_CERT;
delete process.env.SAML_SESSION_SECRET;
delete process.env.SAML_BUTTON_LABEL;
delete process.env.SAML_IMAGE_URL;
delete process.env.DOMAIN_SERVER;
delete process.env.ALLOW_REGISTRATION;
delete process.env.ALLOW_SOCIAL_LOGIN;
@ -55,6 +61,12 @@ describe.skip('GET /', () => {
process.env.GITHUB_CLIENT_SECRET = 'Test Github client Secret';
process.env.DISCORD_CLIENT_ID = 'Test Discord client Id';
process.env.DISCORD_CLIENT_SECRET = 'Test Discord client Secret';
process.env.SAML_ENTRY_POINT = 'http://test-server.com';
process.env.SAML_ISSUER = 'Test SAML Issuer';
process.env.SAML_CERT = 'saml.pem';
process.env.SAML_SESSION_SECRET = 'Test Secret';
process.env.SAML_BUTTON_LABEL = 'Test SAML';
process.env.SAML_IMAGE_URL = 'http://test-server.com';
process.env.DOMAIN_SERVER = 'http://test-server.com';
process.env.ALLOW_REGISTRATION = 'true';
process.env.ALLOW_SOCIAL_LOGIN = 'true';
@ -70,7 +82,7 @@ describe.skip('GET /', () => {
expect(response.statusCode).toBe(200);
expect(response.body).toEqual({
appTitle: 'Test Title',
socialLogins: ['google', 'facebook', 'openid', 'github', 'discord'],
socialLogins: ['google', 'facebook', 'openid', 'github', 'discord', 'saml'],
discordLoginEnabled: true,
facebookLoginEnabled: true,
githubLoginEnabled: true,
@ -78,6 +90,9 @@ describe.skip('GET /', () => {
openidLoginEnabled: true,
openidLabel: 'Test OpenID',
openidImageUrl: 'http://test-server.com',
samlLoginEnabled: true,
samlLabel: 'Test SAML',
samlImageUrl: 'http://test-server.com',
ldap: {
enabled: true,
},

View file

@ -37,6 +37,18 @@ router.get('/', async function (req, res) {
const ldap = getLdapConfig();
try {
const isOpenIdEnabled =
!!process.env.OPENID_CLIENT_ID &&
!!process.env.OPENID_CLIENT_SECRET &&
!!process.env.OPENID_ISSUER &&
!!process.env.OPENID_SESSION_SECRET;
const isSamlEnabled =
!!process.env.SAML_ENTRY_POINT &&
!!process.env.SAML_ISSUER &&
!!process.env.SAML_CERT &&
!!process.env.SAML_SESSION_SECRET;
/** @type {TStartupConfig} */
const payload = {
appTitle: process.env.APP_TITLE || 'LibreChat',
@ -51,14 +63,13 @@ router.get('/', async function (req, res) {
!!process.env.APPLE_TEAM_ID &&
!!process.env.APPLE_KEY_ID &&
!!process.env.APPLE_PRIVATE_KEY_PATH,
openidLoginEnabled:
!!process.env.OPENID_CLIENT_ID &&
!!process.env.OPENID_CLIENT_SECRET &&
!!process.env.OPENID_ISSUER &&
!!process.env.OPENID_SESSION_SECRET,
openidLoginEnabled: isOpenIdEnabled,
openidLabel: process.env.OPENID_BUTTON_LABEL || 'Continue with OpenID',
openidImageUrl: process.env.OPENID_IMAGE_URL,
openidAutoRedirect: isEnabled(process.env.OPENID_AUTO_REDIRECT),
samlLoginEnabled: !isOpenIdEnabled && isSamlEnabled,
samlLabel: process.env.SAML_BUTTON_LABEL,
samlImageUrl: process.env.SAML_IMAGE_URL,
serverDomain: process.env.DOMAIN_SERVER || 'http://localhost:3080',
emailLoginEnabled,
registrationEnabled: !ldap?.enabled && isEnabled(process.env.ALLOW_REGISTRATION),

View file

@ -189,4 +189,24 @@ router.post(
oauthHandler,
);
/**
* SAML Routes
*/
router.get(
'/saml',
passport.authenticate('saml', {
session: false,
}),
);
router.post(
'/saml/callback',
passport.authenticate('saml', {
failureRedirect: `${domains.client}/oauth/error`,
failureMessage: true,
session: false,
}),
oauthHandler,
);
module.exports = router;