🔑 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

@ -1,4 +1,12 @@
import { GoogleIcon, FacebookIcon, OpenIDIcon, GithubIcon, DiscordIcon, AppleIcon } from '~/components';
import {
GoogleIcon,
FacebookIcon,
OpenIDIcon,
GithubIcon,
DiscordIcon,
AppleIcon,
SamlIcon,
} from '~/components';
import SocialButton from './SocialButton';
@ -90,6 +98,23 @@ function SocialLoginRender({
id="openid"
/>
),
saml: startupConfig.samlLoginEnabled && (
<SocialButton
key="saml"
enabled={startupConfig.samlLoginEnabled}
serverDomain={startupConfig.serverDomain}
oauthPath="saml"
Icon={() =>
startupConfig.samlImageUrl ? (
<img src={startupConfig.samlImageUrl} alt="SAML Logo" className="h-5 w-5" />
) : (
<SamlIcon />
)
}
label={startupConfig.samlLabel ? startupConfig.samlLabel : localize('com_auth_saml_login')}
id="saml"
/>
),
};
return (

View file

@ -16,7 +16,7 @@ const mockStartupConfig = {
isLoading: false,
isError: false,
data: {
socialLogins: ['google', 'facebook', 'openid', 'github', 'discord'],
socialLogins: ['google', 'facebook', 'openid', 'github', 'discord', 'saml'],
discordLoginEnabled: true,
facebookLoginEnabled: true,
githubLoginEnabled: true,
@ -24,6 +24,9 @@ const mockStartupConfig = {
openidLoginEnabled: true,
openidLabel: 'Test OpenID',
openidImageUrl: 'http://test-server.com',
samlLoginEnabled: true,
samlLabel: 'Test SAML',
samlImageUrl: 'http://test-server.com',
ldap: {
enabled: false,
},
@ -143,6 +146,11 @@ test('renders login form', () => {
'href',
'mock-server/oauth/discord',
);
expect(getByRole('link', { name: /Test SAML/i })).toBeInTheDocument();
expect(getByRole('link', { name: /Test SAML/i })).toHaveAttribute(
'href',
'mock-server/oauth/saml',
);
});
test('calls loginUser.mutate on login', async () => {

View file

@ -12,7 +12,7 @@ jest.mock('librechat-data-provider/react-query');
const mockLogin = jest.fn();
const mockStartupConfig: TStartupConfig = {
socialLogins: ['google', 'facebook', 'openid', 'github', 'discord'],
socialLogins: ['google', 'facebook', 'openid', 'github', 'discord', 'saml'],
discordLoginEnabled: true,
facebookLoginEnabled: true,
githubLoginEnabled: true,
@ -20,6 +20,9 @@ const mockStartupConfig: TStartupConfig = {
openidLoginEnabled: true,
openidLabel: 'Test OpenID',
openidImageUrl: 'http://test-server.com',
samlLoginEnabled: true,
samlLabel: 'Test SAML',
samlImageUrl: 'http://test-server.com',
registrationEnabled: true,
emailLoginEnabled: true,
socialLoginEnabled: true,

View file

@ -17,7 +17,7 @@ const mockStartupConfig = {
isLoading: false,
isError: false,
data: {
socialLogins: ['google', 'facebook', 'openid', 'github', 'discord'],
socialLogins: ['google', 'facebook', 'openid', 'github', 'discord', 'saml'],
discordLoginEnabled: true,
facebookLoginEnabled: true,
githubLoginEnabled: true,
@ -25,6 +25,9 @@ const mockStartupConfig = {
openidLoginEnabled: true,
openidLabel: 'Test OpenID',
openidImageUrl: 'http://test-server.com',
samlLoginEnabled: true,
samlLabel: 'Test SAML',
samlImageUrl: 'http://test-server.com',
registrationEnabled: true,
socialLoginEnabled: true,
serverDomain: 'mock-server',
@ -146,6 +149,11 @@ test('renders registration form', () => {
'href',
'mock-server/oauth/discord',
);
expect(getByRole('link', { name: /Test SAML/i })).toBeInTheDocument();
expect(getByRole('link', { name: /Test SAML/i })).toHaveAttribute(
'href',
'mock-server/oauth/saml',
);
});
// eslint-disable-next-line jest/no-commented-out-tests

View file

@ -0,0 +1,31 @@
/**
* SamlIcon Component
*
* Source: SVG Repo
* URL: https://www.svgrepo.com/svg/448590/saml
* - COLLECTION: Hashicorp Line Interface Icons
* - LICENSE: MLP License
* - AUTHOR: HashiCorp
*/
import React from 'react';
export default function SamlIcon() {
return (
<svg
width="800px"
height="800px"
viewBox="0 0 16 16"
xmlns="http://www.w3.org/2000/svg"
fill="none"
className="h-5 w-5"
>
<g fill="#000000">
<path d="M7.754 2l.463.41c.343.304.687.607 1.026.915C11.44 5.32 13.3 7.565 14.7 10.149c.072.132.137.268.202.403l.098.203-.108.057-.081-.115-.21-.299-.147-.214c-1.019-1.479-2.04-2.96-3.442-4.145a6.563 6.563 0 00-1.393-.904c-1.014-.485-1.916-.291-2.69.505-.736.757-1.118 1.697-1.463 2.653-.045.123-.092.245-.139.367l-.082.215-.172-.055c.1-.348.192-.698.284-1.049.21-.795.42-1.59.712-2.356.31-.816.702-1.603 1.093-2.39.169-.341.338-.682.5-1.025h.092z" />
<path d="M8.448 11.822c-1.626.77-5.56 1.564-7.426 1.36C.717 11.576 3.71 4.05 5.18 2.91l-.095.218a4.638 4.638 0 01-.138.303l-.066.129c-.76 1.462-1.519 2.926-1.908 4.53a7.482 7.482 0 00-.228 1.689c-.01 1.34.824 2.252 2.217 2.309.67.027 1.347-.043 2.023-.114.294-.03.587-.061.88-.084.108-.008.214-.021.352-.039l.231-.028z" />
<path d="M3.825 14.781c-.445.034-.89.068-1.333.108 4.097.39 8.03-.277 11.91-1.644-1.265-2.23-2.97-3.991-4.952-5.522.026.098.084.169.141.239l.048.06c.17.226.348.448.527.67.409.509.818 1.018 1.126 1.578.778 1.42.356 2.648-1.168 3.296-1.002.427-2.097.718-3.18.892-1.03.164-2.075.243-3.119.323z" />
</g>
</svg>
);
}

View file

@ -24,6 +24,7 @@ export { default as OpenIDIcon } from './OpenIDIcon';
export { default as GithubIcon } from './GithubIcon';
export { default as DiscordIcon } from './DiscordIcon';
export { default as AppleIcon } from './AppleIcon';
export { default as SamlIcon } from './SamlIcon';
export { default as AnthropicIcon } from './AnthropicIcon';
export { default as SendIcon } from './SendIcon';
export { default as LinkIcon } from './LinkIcon';

View file

@ -124,6 +124,7 @@
"com_auth_reset_password_if_email_exists": "If an account with that email exists, an email with password reset instructions has been sent. Please make sure to check your spam folder.",
"com_auth_reset_password_link_sent": "Email Sent",
"com_auth_reset_password_success": "Password Reset Success",
"com_auth_saml_login": "Continue with SAML",
"com_auth_sign_in": "Sign in",
"com_auth_sign_up": "Sign up",
"com_auth_submit_registration": "Submit registration",