LibreChat/api/strategies/samlStrategy.js

357 lines
12 KiB
JavaScript
Raw Normal View History

const fs = require('fs');
const path = require('path');
const fetch = require('node-fetch');
const passport = require('passport');
const { ErrorTypes } = require('librechat-data-provider');
🏗️ refactor: Extract DB layers to `data-schemas` for shared use (#7650) * refactor: move model definitions and database-related methods to packages/data-schemas * ci: update tests due to new DB structure fix: disable mocking `librechat-data-provider` feat: Add schema exports to data-schemas package - Introduced a new schema module that exports various schemas including action, agent, and user schemas. - Updated index.ts to include the new schema exports for better modularity and organization. ci: fix appleStrategy tests fix: Agent.spec.js ci: refactor handleTools tests to use MongoMemoryServer for in-memory database fix: getLogStores imports ci: update banViolation tests to use MongoMemoryServer and improve session mocking test: refactor samlStrategy tests to improve mock configurations and user handling ci: fix crypto mock in handleText tests for improved accuracy ci: refactor spendTokens tests to improve model imports and setup ci: refactor Message model tests to use MongoMemoryServer and improve database interactions * refactor: streamline IMessage interface and move feedback properties to types/message.ts * refactor: use exported initializeRoles from `data-schemas`, remove api workspace version (this serves as an example of future migrations that still need to happen) * refactor: update model imports to use destructuring from `~/db/models` for consistency and clarity * refactor: remove unused mongoose imports from model files for cleaner code * refactor: remove unused mongoose imports from Share, Prompt, and Transaction model files for cleaner code * refactor: remove unused import in Transaction model for cleaner code * ci: update deploy workflow to reference new Docker Dev Branch Images Build and add new workflow for building Docker images on dev branch * chore: cleanup imports
2025-05-30 22:18:13 -04:00
const { hashToken, logger } = require('@librechat/data-schemas');
const { Strategy: SamlStrategy } = require('@node-saml/passport-saml');
🏢 feat: Tenant-Scoped App Config in Auth Login Flows (#12434) * feat: add resolveAppConfigForUser utility for tenant-scoped auth config TypeScript utility in packages/api that wraps getAppConfig in tenantStorage.run() when the user has a tenantId, falling back to baseOnly for new users or non-tenant deployments. Uses DI pattern (getAppConfig passed as parameter) for testability. Auth flows apply role-level overrides only (userId not passed) because user/group principal resolution is deferred to post-auth. * feat: tenant-scoped app config in auth login flows All auth strategies (LDAP, SAML, OpenID, social login) now use a two-phase domain check consistent with requestPasswordReset: 1. Fast-fail with base config (memory-cached, zero DB queries) 2. DB user lookup 3. Tenant-scoped re-check via resolveAppConfigForUser (only when user has a tenantId; otherwise reuse base config) This preserves the original fast-fail protection against globally blocked domains while enabling tenant-specific config overrides. OpenID error ordering preserved: AUTH_FAILED checked before domain re-check so users with wrong providers get the correct error type. registerUser unchanged (baseOnly, no user identity yet). * test: add tenant-scoped config tests for auth strategies Add resolveAppConfig.spec.ts in packages/api with 8 tests: - baseOnly fallback for null/undefined/no-tenant users - tenant-scoped config with role and tenantId - ALS context propagation verified inside getAppConfig callback - undefined role with tenantId edge case Update strategy and AuthService tests to mock resolveAppConfigForUser via @librechat/api. Tests verify two-phase domain check behavior: fast-fail before DB, tenant re-check after. Non-tenant users reuse base config without calling resolveAppConfigForUser. * refactor: skip redundant domain re-check for non-tenant users Guard the second isEmailDomainAllowed call with appConfig !== baseConfig in SAML, OpenID, and social strategies. For non-tenant users the tenant config is the same base config object, so the second check is a no-op. Narrow eslint-disable in resolveAppConfig.spec.ts to the specific require line instead of blanket file-level suppression. * fix: address review findings — consistency, tests, and ordering - Consolidate duplicate require('@librechat/api') in AuthService.js - Add two-phase domain check to LDAP (base fast-fail before findUser), making all strategies consistent with PR description - Add appConfig !== baseConfig guard to requestPasswordReset second domain check, consistent with SAML/OpenID/social strategies - Move SAML provider check before tenant config resolution to avoid unnecessary resolveAppConfigForUser call for wrong-provider users - Add tenant domain rejection tests to SAML, OpenID, and social specs verifying that tenant config restrictions actually block login - Add error propagation tests to resolveAppConfig.spec.ts - Remove redundant mockTenantStorage alias in resolveAppConfig.spec.ts - Narrow eslint-disable to specific require line * test: add tenant domain rejection test for LDAP strategy Covers the appConfig !== baseConfig && !isEmailDomainAllowed path, consistent with SAML, OpenID, and social strategy specs. * refactor: rename resolveAppConfig to app/resolve per AGENTS.md Rename resolveAppConfig.ts → resolve.ts and resolveAppConfig.spec.ts → resolve.spec.ts to align with the project's concise naming convention. * fix: remove fragile reference-equality guard, add logging and docs Remove appConfig !== baseConfig guard from all strategies and requestPasswordReset. The guard relied on implicit cache-backend identity semantics (in-memory Keyv returns same object reference) that would silently break with Redis or cloned configs. The second isEmailDomainAllowed call is a cheap synchronous check — always running it is clearer and eliminates the coupling. Add audit logging to requestPasswordReset domain blocks (base and tenant), consistent with all auth strategies. Extract duplicated error construction into makeDomainDeniedError(). Wrap resolveAppConfigForUser in requestPasswordReset with try/catch to prevent DB errors from leaking to the client via the controller's generic catch handler. Document the dual tenantId propagation (ALS for DB isolation, explicit param for cache key) in resolveAppConfigForUser JSDoc. Add comment documenting the LDAP error-type ordering change (cross-provider users from blocked domains now get 'domain not allowed' instead of AUTH_FAILED). Assert resolveAppConfigForUser is not called on LDAP provider mismatch path. * fix: return generic response for tenant domain block in password reset Tenant-scoped domain rejection in requestPasswordReset now returns the same generic "If an account with that email exists..." response instead of an Error. This prevents user-enumeration: an attacker cannot distinguish between "email not found" and "tenant blocks this domain" by comparing HTTP responses. The base-config fast-fail (pre-user-lookup) still returns an Error since it fires before any user existence is revealed. * docs: document phase 1 vs phase 2 domain check behavior in JSDoc Phase 1 (base config, pre-findUser) intentionally returns Error/400 to reveal globally blocked domains without confirming user existence. Phase 2 (tenant config, post-findUser) returns generic 200 to prevent user-enumeration. This distinction is now explicit in the JSDoc.
2026-03-27 16:08:43 -04:00
const {
getBalanceConfig,
isEmailDomainAllowed,
resolveAppConfigForUser,
} = require('@librechat/api');
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
🏗️ refactor: Extract DB layers to `data-schemas` for shared use (#7650) * refactor: move model definitions and database-related methods to packages/data-schemas * ci: update tests due to new DB structure fix: disable mocking `librechat-data-provider` feat: Add schema exports to data-schemas package - Introduced a new schema module that exports various schemas including action, agent, and user schemas. - Updated index.ts to include the new schema exports for better modularity and organization. ci: fix appleStrategy tests fix: Agent.spec.js ci: refactor handleTools tests to use MongoMemoryServer for in-memory database fix: getLogStores imports ci: update banViolation tests to use MongoMemoryServer and improve session mocking test: refactor samlStrategy tests to improve mock configurations and user handling ci: fix crypto mock in handleText tests for improved accuracy ci: refactor spendTokens tests to improve model imports and setup ci: refactor Message model tests to use MongoMemoryServer and improve database interactions * refactor: streamline IMessage interface and move feedback properties to types/message.ts * refactor: use exported initializeRoles from `data-schemas`, remove api workspace version (this serves as an example of future migrations that still need to happen) * refactor: update model imports to use destructuring from `~/db/models` for consistency and clarity * refactor: remove unused mongoose imports from model files for cleaner code * refactor: remove unused mongoose imports from Share, Prompt, and Transaction model files for cleaner code * refactor: remove unused import in Transaction model for cleaner code * ci: update deploy workflow to reference new Docker Dev Branch Images Build and add new workflow for building Docker images on dev branch * chore: cleanup imports
2025-05-30 22:18:13 -04:00
const { findUser, createUser, updateUser } = require('~/models');
🛜 refactor: Streamline App Config Usage (#9234) * WIP: app.locals refactoring WIP: appConfig fix: update memory configuration retrieval to use getAppConfig based on user role fix: update comment for AppConfig interface to clarify purpose 🏷️ refactor: Update tests to use getAppConfig for endpoint configurations ci: Update AppService tests to initialize app config instead of app.locals ci: Integrate getAppConfig into remaining tests refactor: Update multer storage destination to use promise-based getAppConfig and improve error handling in tests refactor: Rename initializeAppConfig to setAppConfig and update related tests ci: Mock getAppConfig in various tests to provide default configurations refactor: Update convertMCPToolsToPlugins to use mcpManager for server configuration and adjust related tests chore: rename `Config/getAppConfig` -> `Config/app` fix: streamline OpenAI image tools configuration by removing direct appConfig dependency and using function parameters chore: correct parameter documentation for imageOutputType in ToolService.js refactor: remove `getCustomConfig` dependency in config route refactor: update domain validation to use appConfig for allowed domains refactor: use appConfig registration property chore: remove app parameter from AppService invocation refactor: update AppConfig interface to correct registration and turnstile configurations refactor: remove getCustomConfig dependency and use getAppConfig in PluginController, multer, and MCP services refactor: replace getCustomConfig with getAppConfig in STTService, TTSService, and related files refactor: replace getCustomConfig with getAppConfig in Conversation and Message models, update tempChatRetention functions to use AppConfig type refactor: update getAppConfig calls in Conversation and Message models to include user role for temporary chat expiration ci: update related tests refactor: update getAppConfig call in getCustomConfigSpeech to include user role fix: update appConfig usage to access allowedDomains from actions instead of registration refactor: enhance AppConfig to include fileStrategies and update related file strategy logic refactor: update imports to use normalizeEndpointName from @librechat/api and remove redundant definitions chore: remove deprecated unused RunManager refactor: get balance config primarily from appConfig refactor: remove customConfig dependency for appConfig and streamline loadConfigModels logic refactor: remove getCustomConfig usage and use app config in file citations refactor: consolidate endpoint loading logic into loadEndpoints function refactor: update appConfig access to use endpoints structure across various services refactor: implement custom endpoints configuration and streamline endpoint loading logic refactor: update getAppConfig call to include user role parameter refactor: streamline endpoint configuration and enhance appConfig usage across services refactor: replace getMCPAuthMap with getUserMCPAuthMap and remove unused getCustomConfig file refactor: add type annotation for loadedEndpoints in loadEndpoints function refactor: move /services/Files/images/parse to TS API chore: add missing FILE_CITATIONS permission to IRole interface refactor: restructure toolkits to TS API refactor: separate manifest logic into its own module refactor: consolidate tool loading logic into a new tools module for startup logic refactor: move interface config logic to TS API refactor: migrate checkEmailConfig to TypeScript and update imports refactor: add FunctionTool interface and availableTools to AppConfig refactor: decouple caching and DB operations from AppService, make part of consolidated `getAppConfig` WIP: fix tests * fix: rebase conflicts * refactor: remove app.locals references * refactor: replace getBalanceConfig with getAppConfig in various strategies and middleware * refactor: replace appConfig?.balance with getBalanceConfig in various controllers and clients * test: add balance configuration to titleConvo method in AgentClient tests * chore: remove unused `openai-chat-tokens` package * chore: remove unused imports in initializeMCPs.js * refactor: update balance configuration to use getAppConfig instead of getBalanceConfig * refactor: integrate configMiddleware for centralized configuration handling * refactor: optimize email domain validation by removing unnecessary async calls * refactor: simplify multer storage configuration by removing async calls * refactor: reorder imports for better readability in user.js * refactor: replace getAppConfig calls with req.config for improved performance * chore: replace getAppConfig calls with req.config in tests for centralized configuration handling * chore: remove unused override config * refactor: add configMiddleware to endpoint route and replace getAppConfig with req.config * chore: remove customConfig parameter from TTSService constructor * refactor: pass appConfig from request to processFileCitations for improved configuration handling * refactor: remove configMiddleware from endpoint route and retrieve appConfig directly in getEndpointsConfig if not in `req.config` * test: add mockAppConfig to processFileCitations tests for improved configuration handling * fix: pass req.config to hasCustomUserVars and call without await after synchronous refactor * fix: type safety in useExportConversation * refactor: retrieve appConfig using getAppConfig in PluginController and remove configMiddleware from plugins route, to avoid always retrieving when plugins are cached * chore: change `MongoUser` typedef to `IUser` * fix: Add `user` and `config` fields to ServerRequest and update JSDoc type annotations from Express.Request to ServerRequest * fix: remove unused setAppConfig mock from Server configuration tests
2025-08-26 12:10:18 -04:00
const { getAppConfig } = require('~/server/services/Config');
const paths = require('~/config/paths');
let crypto;
try {
crypto = require('node:crypto');
} catch (err) {
logger.error('[samlStrategy] crypto support is disabled!', err);
}
/**
* Retrieves the certificate content from the given value.
*
* This function determines whether the provided value is a certificate string (RFC7468 format or
* base64-encoded without a header) or a valid file path. If the value matches one of these formats,
* the certificate content is returned. Otherwise, an error is thrown.
*
* @see https://github.com/node-saml/node-saml/tree/master?tab=readme-ov-file#configuration-option-idpcert
* @param {string} value - The certificate string or file path.
* @returns {string} The certificate content if valid.
* @throws {Error} If the value is not a valid certificate string or file path.
*/
function getCertificateContent(value) {
if (typeof value !== 'string') {
throw new Error('Invalid input: SAML_CERT must be a string.');
}
// Check if it's an RFC7468 formatted PEM certificate
const pemRegex = new RegExp(
'-----BEGIN (CERTIFICATE|PUBLIC KEY)-----\n' + // header
'([A-Za-z0-9+/=]{64}\n)+' + // base64 content (64 characters per line)
'[A-Za-z0-9+/=]{1,64}\n' + // base64 content (last line)
'-----END (CERTIFICATE|PUBLIC KEY)-----', // footer
);
if (pemRegex.test(value)) {
logger.info('[samlStrategy] Detected RFC7468-formatted certificate string.');
return value;
}
// Check if it's a Base64-encoded certificate (no header)
if (/^[A-Za-z0-9+/=]+$/.test(value) && value.length % 4 === 0) {
logger.info('[samlStrategy] Detected base64-encoded certificate string (no header).');
return value;
}
// Check if file exists and is readable
const certPath = path.normalize(path.isAbsolute(value) ? value : path.join(paths.root, value));
if (fs.existsSync(certPath) && fs.statSync(certPath).isFile()) {
try {
logger.info(`[samlStrategy] Loading certificate from file: ${certPath}`);
return fs.readFileSync(certPath, 'utf8').trim();
} catch (error) {
throw new Error(`Error reading certificate file: ${error.message}`);
}
}
throw new Error('Invalid cert: SAML_CERT must be a valid file path or certificate string.');
}
/**
* Retrieves a SAML claim from a profile object based on environment configuration.
* @param {object} profile - Saml profile
* @param {string} envVar - Environment variable name (SAML_*)
* @param {string} defaultKey - Default key to use if the environment variable is not set
* @returns {string}
*/
function getSamlClaim(profile, envVar, defaultKey) {
const claimKey = process.env[envVar];
// Avoids accessing `profile[""]` when the environment variable is empty string.
if (claimKey) {
return profile[claimKey] ?? profile[defaultKey];
}
return profile[defaultKey];
}
function getEmail(profile) {
return getSamlClaim(profile, 'SAML_EMAIL_CLAIM', 'email');
}
function getUserName(profile) {
return getSamlClaim(profile, 'SAML_USERNAME_CLAIM', 'username');
}
function getGivenName(profile) {
return getSamlClaim(profile, 'SAML_GIVEN_NAME_CLAIM', 'given_name');
}
function getFamilyName(profile) {
return getSamlClaim(profile, 'SAML_FAMILY_NAME_CLAIM', 'family_name');
}
function getPicture(profile) {
return getSamlClaim(profile, 'SAML_PICTURE_CLAIM', 'picture');
}
/**
* Downloads an image from a URL using an access token.
* @param {string} url
* @returns {Promise<Buffer>}
*/
const downloadImage = async (url) => {
try {
const response = await fetch(url);
if (response.ok) {
return await response.buffer();
} else {
throw new Error(`${response.statusText} (HTTP ${response.status})`);
}
} catch (error) {
logger.error(`[samlStrategy] Error downloading image at URL "${url}": ${error}`);
return null;
}
};
/**
* Determines the full name of a user based on SAML profile and environment configuration.
*
* @param {Object} profile - The user profile object from SAML Connect
* @returns {string} The determined full name of the user
*/
function getFullName(profile) {
if (process.env.SAML_NAME_CLAIM) {
logger.info(
`[samlStrategy] Using SAML_NAME_CLAIM: ${process.env.SAML_NAME_CLAIM}, profile: ${profile[process.env.SAML_NAME_CLAIM]}`,
);
return profile[process.env.SAML_NAME_CLAIM];
}
const givenName = getGivenName(profile);
const familyName = getFamilyName(profile);
if (givenName && familyName) {
return `${givenName} ${familyName}`;
}
if (givenName) {
return givenName;
}
if (familyName) {
return familyName;
}
return getUserName(profile) || getEmail(profile);
}
/**
* Converts an input into a string suitable for a username.
* If the input is a string, it will be returned as is.
* If the input is an array, elements will be joined with underscores.
* In case of undefined or other falsy values, a default value will be returned.
*
* @param {string | string[] | undefined} input - The input value to be converted into a username.
* @param {string} [defaultValue=''] - The default value to return if the input is falsy.
* @returns {string} The processed input as a string suitable for a username.
*/
function convertToUsername(input, defaultValue = '') {
if (typeof input === 'string') {
return input;
} else if (Array.isArray(input)) {
return input.join('_');
}
return defaultValue;
}
🔐 feat: Admin Auth Support for SAML and Social OAuth Providers (#12472) * refactor: Add existingUsersOnly support to social and SAML auth callbacks - Add `existingUsersOnly` option to the `socialLogin` handler factory to reject unknown users instead of creating new accounts - Refactor SAML strategy callback into `createSamlCallback(existingUsersOnly)` factory function, mirroring the OpenID `createOpenIDCallback` pattern - Extract shared SAML config into `getBaseSamlConfig()` helper - Register `samlAdmin` passport strategy with `existingUsersOnly: true` and admin-specific callback URL, called automatically from `setupSaml()` * feat: Register admin OAuth strategy variants for all social providers - Add admin strategy exports to Google, GitHub, Discord, Facebook, and Apple strategy files with admin callback URLs and existingUsersOnly - Extract provider configs into reusable helpers to avoid duplication between regular and admin strategy constructors - Re-export all admin strategy factories from strategies/index.js - Register admin passport strategies (googleAdmin, githubAdmin, etc.) alongside regular ones in socialLogins.js when env vars are present * feat: Add admin auth routes for SAML and social OAuth providers - Add initiation and callback routes for SAML, Google, GitHub, Discord, Facebook, and Apple to the admin auth router - Each provider follows the exchange code + PKCE pattern established by OpenID admin auth: store PKCE challenge on initiation, retrieve on callback, generate exchange code for the admin panel - SAML and Apple use POST callbacks with state extracted from req.body.RelayState and req.body.state respectively - Extract storePkceChallenge(), retrievePkceChallenge(), and generateState() helpers; refactor existing OpenID routes to use them - All callback chains enforce requireAdminAccess, setBalanceConfig, checkDomainAllowed, and the shared createOAuthHandler - No changes needed to the generic POST /oauth/exchange endpoint * fix: Update SAML strategy test to handle dual strategy registration setupSaml() now registers both 'saml' and 'samlAdmin' strategies, causing the SamlStrategy mock to be called twice. The verifyCallback variable was getting overwritten with the admin callback (which has existingUsersOnly: true), making all new-user tests fail. Fix: capture only the first callback per setupSaml() call and reset between tests. * fix: Address review findings for admin OAuth strategy changes - Fix existingUsersOnly rejection in socialLogin.js to use cb(null, false, { message }) instead of cb(error), ensuring passport's failureRedirect fires correctly for admin flows - Consolidate duplicate require() calls in strategies/index.js by destructuring admin exports from the already-imported default export - Pass pre-parsed baseConfig to setupSamlAdmin() to avoid redundant certificate file I/O at startup - Extract getGoogleConfig() helper in googleStrategy.js for consistency with all other provider strategy files - Replace randomState() (openid-client) with generateState() (crypto) in the OpenID admin route for consistency with all other providers, and remove the now-unused openid-client import * Reorder import statements in auth.js
2026-03-30 22:49:44 -04:00
/**
* Creates a SAML authentication callback.
* @param {boolean} [existingUsersOnly=false] - If true, only existing users will be authenticated.
* @returns {Function} The SAML callback function for passport.
*/
function createSamlCallback(existingUsersOnly = false) {
return async (profile, done) => {
try {
logger.info(`[samlStrategy] SAML authentication received for NameID: ${profile.nameID}`);
logger.debug('[samlStrategy] SAML profile:', profile);
const userEmail = getEmail(profile) || '';
const baseConfig = await getAppConfig({ baseOnly: true });
if (!isEmailDomainAllowed(userEmail, baseConfig?.registration?.allowedDomains)) {
logger.error(
`[SAML Strategy] Authentication blocked - email domain not allowed [Email: ${userEmail}]`,
);
return done(null, false, { message: 'Email domain not allowed' });
}
let user = await findUser({ samlId: profile.nameID });
logger.info(
`[samlStrategy] User ${user ? 'found' : 'not found'} with SAML ID: ${profile.nameID}`,
);
if (!user) {
user = await findUser({ email: userEmail });
logger.info(`[samlStrategy] User ${user ? 'found' : 'not found'} with email: ${userEmail}`);
}
if (user && user.provider !== 'saml') {
logger.info(
`[samlStrategy] User ${user.email} already exists with provider ${user.provider}`,
);
return done(null, false, {
message: ErrorTypes.AUTH_FAILED,
});
}
const appConfig = user?.tenantId
? await resolveAppConfigForUser(getAppConfig, user)
: baseConfig;
if (!isEmailDomainAllowed(userEmail, appConfig?.registration?.allowedDomains)) {
logger.error(
`[SAML Strategy] Authentication blocked - email domain not allowed [Email: ${userEmail}]`,
);
return done(null, false, { message: 'Email domain not allowed' });
}
const fullName = getFullName(profile);
const username = convertToUsername(
getUserName(profile) || getGivenName(profile) || getEmail(profile),
);
if (!user) {
if (existingUsersOnly) {
logger.error(
`[samlStrategy] Admin auth blocked - user does not exist [Email: ${userEmail}]`,
);
🔐 feat: Admin Auth Support for SAML and Social OAuth Providers (#12472) * refactor: Add existingUsersOnly support to social and SAML auth callbacks - Add `existingUsersOnly` option to the `socialLogin` handler factory to reject unknown users instead of creating new accounts - Refactor SAML strategy callback into `createSamlCallback(existingUsersOnly)` factory function, mirroring the OpenID `createOpenIDCallback` pattern - Extract shared SAML config into `getBaseSamlConfig()` helper - Register `samlAdmin` passport strategy with `existingUsersOnly: true` and admin-specific callback URL, called automatically from `setupSaml()` * feat: Register admin OAuth strategy variants for all social providers - Add admin strategy exports to Google, GitHub, Discord, Facebook, and Apple strategy files with admin callback URLs and existingUsersOnly - Extract provider configs into reusable helpers to avoid duplication between regular and admin strategy constructors - Re-export all admin strategy factories from strategies/index.js - Register admin passport strategies (googleAdmin, githubAdmin, etc.) alongside regular ones in socialLogins.js when env vars are present * feat: Add admin auth routes for SAML and social OAuth providers - Add initiation and callback routes for SAML, Google, GitHub, Discord, Facebook, and Apple to the admin auth router - Each provider follows the exchange code + PKCE pattern established by OpenID admin auth: store PKCE challenge on initiation, retrieve on callback, generate exchange code for the admin panel - SAML and Apple use POST callbacks with state extracted from req.body.RelayState and req.body.state respectively - Extract storePkceChallenge(), retrievePkceChallenge(), and generateState() helpers; refactor existing OpenID routes to use them - All callback chains enforce requireAdminAccess, setBalanceConfig, checkDomainAllowed, and the shared createOAuthHandler - No changes needed to the generic POST /oauth/exchange endpoint * fix: Update SAML strategy test to handle dual strategy registration setupSaml() now registers both 'saml' and 'samlAdmin' strategies, causing the SamlStrategy mock to be called twice. The verifyCallback variable was getting overwritten with the admin callback (which has existingUsersOnly: true), making all new-user tests fail. Fix: capture only the first callback per setupSaml() call and reset between tests. * fix: Address review findings for admin OAuth strategy changes - Fix existingUsersOnly rejection in socialLogin.js to use cb(null, false, { message }) instead of cb(error), ensuring passport's failureRedirect fires correctly for admin flows - Consolidate duplicate require() calls in strategies/index.js by destructuring admin exports from the already-imported default export - Pass pre-parsed baseConfig to setupSamlAdmin() to avoid redundant certificate file I/O at startup - Extract getGoogleConfig() helper in googleStrategy.js for consistency with all other provider strategy files - Replace randomState() (openid-client) with generateState() (crypto) in the OpenID admin route for consistency with all other providers, and remove the now-unused openid-client import * Reorder import statements in auth.js
2026-03-30 22:49:44 -04:00
return done(null, false, { message: 'User does not exist' });
}
🔐 feat: Admin Auth Support for SAML and Social OAuth Providers (#12472) * refactor: Add existingUsersOnly support to social and SAML auth callbacks - Add `existingUsersOnly` option to the `socialLogin` handler factory to reject unknown users instead of creating new accounts - Refactor SAML strategy callback into `createSamlCallback(existingUsersOnly)` factory function, mirroring the OpenID `createOpenIDCallback` pattern - Extract shared SAML config into `getBaseSamlConfig()` helper - Register `samlAdmin` passport strategy with `existingUsersOnly: true` and admin-specific callback URL, called automatically from `setupSaml()` * feat: Register admin OAuth strategy variants for all social providers - Add admin strategy exports to Google, GitHub, Discord, Facebook, and Apple strategy files with admin callback URLs and existingUsersOnly - Extract provider configs into reusable helpers to avoid duplication between regular and admin strategy constructors - Re-export all admin strategy factories from strategies/index.js - Register admin passport strategies (googleAdmin, githubAdmin, etc.) alongside regular ones in socialLogins.js when env vars are present * feat: Add admin auth routes for SAML and social OAuth providers - Add initiation and callback routes for SAML, Google, GitHub, Discord, Facebook, and Apple to the admin auth router - Each provider follows the exchange code + PKCE pattern established by OpenID admin auth: store PKCE challenge on initiation, retrieve on callback, generate exchange code for the admin panel - SAML and Apple use POST callbacks with state extracted from req.body.RelayState and req.body.state respectively - Extract storePkceChallenge(), retrievePkceChallenge(), and generateState() helpers; refactor existing OpenID routes to use them - All callback chains enforce requireAdminAccess, setBalanceConfig, checkDomainAllowed, and the shared createOAuthHandler - No changes needed to the generic POST /oauth/exchange endpoint * fix: Update SAML strategy test to handle dual strategy registration setupSaml() now registers both 'saml' and 'samlAdmin' strategies, causing the SamlStrategy mock to be called twice. The verifyCallback variable was getting overwritten with the admin callback (which has existingUsersOnly: true), making all new-user tests fail. Fix: capture only the first callback per setupSaml() call and reset between tests. * fix: Address review findings for admin OAuth strategy changes - Fix existingUsersOnly rejection in socialLogin.js to use cb(null, false, { message }) instead of cb(error), ensuring passport's failureRedirect fires correctly for admin flows - Consolidate duplicate require() calls in strategies/index.js by destructuring admin exports from the already-imported default export - Pass pre-parsed baseConfig to setupSamlAdmin() to avoid redundant certificate file I/O at startup - Extract getGoogleConfig() helper in googleStrategy.js for consistency with all other provider strategy files - Replace randomState() (openid-client) with generateState() (crypto) in the OpenID admin route for consistency with all other providers, and remove the now-unused openid-client import * Reorder import statements in auth.js
2026-03-30 22:49:44 -04:00
user = {
provider: 'saml',
samlId: profile.nameID,
username,
email: userEmail,
emailVerified: true,
name: fullName,
};
const balanceConfig = getBalanceConfig(appConfig);
user = await createUser(user, balanceConfig, true, true);
} else {
user.provider = 'saml';
user.samlId = profile.nameID;
user.username = username;
user.name = fullName;
}
const picture = getPicture(profile);
if (picture && !user.avatar?.includes('manual=true')) {
const imageBuffer = await downloadImage(profile.picture);
if (imageBuffer) {
let fileName;
if (crypto) {
fileName = (await hashToken(profile.nameID)) + '.png';
} else {
fileName = profile.nameID + '.png';
🏢 feat: Tenant-Scoped App Config in Auth Login Flows (#12434) * feat: add resolveAppConfigForUser utility for tenant-scoped auth config TypeScript utility in packages/api that wraps getAppConfig in tenantStorage.run() when the user has a tenantId, falling back to baseOnly for new users or non-tenant deployments. Uses DI pattern (getAppConfig passed as parameter) for testability. Auth flows apply role-level overrides only (userId not passed) because user/group principal resolution is deferred to post-auth. * feat: tenant-scoped app config in auth login flows All auth strategies (LDAP, SAML, OpenID, social login) now use a two-phase domain check consistent with requestPasswordReset: 1. Fast-fail with base config (memory-cached, zero DB queries) 2. DB user lookup 3. Tenant-scoped re-check via resolveAppConfigForUser (only when user has a tenantId; otherwise reuse base config) This preserves the original fast-fail protection against globally blocked domains while enabling tenant-specific config overrides. OpenID error ordering preserved: AUTH_FAILED checked before domain re-check so users with wrong providers get the correct error type. registerUser unchanged (baseOnly, no user identity yet). * test: add tenant-scoped config tests for auth strategies Add resolveAppConfig.spec.ts in packages/api with 8 tests: - baseOnly fallback for null/undefined/no-tenant users - tenant-scoped config with role and tenantId - ALS context propagation verified inside getAppConfig callback - undefined role with tenantId edge case Update strategy and AuthService tests to mock resolveAppConfigForUser via @librechat/api. Tests verify two-phase domain check behavior: fast-fail before DB, tenant re-check after. Non-tenant users reuse base config without calling resolveAppConfigForUser. * refactor: skip redundant domain re-check for non-tenant users Guard the second isEmailDomainAllowed call with appConfig !== baseConfig in SAML, OpenID, and social strategies. For non-tenant users the tenant config is the same base config object, so the second check is a no-op. Narrow eslint-disable in resolveAppConfig.spec.ts to the specific require line instead of blanket file-level suppression. * fix: address review findings — consistency, tests, and ordering - Consolidate duplicate require('@librechat/api') in AuthService.js - Add two-phase domain check to LDAP (base fast-fail before findUser), making all strategies consistent with PR description - Add appConfig !== baseConfig guard to requestPasswordReset second domain check, consistent with SAML/OpenID/social strategies - Move SAML provider check before tenant config resolution to avoid unnecessary resolveAppConfigForUser call for wrong-provider users - Add tenant domain rejection tests to SAML, OpenID, and social specs verifying that tenant config restrictions actually block login - Add error propagation tests to resolveAppConfig.spec.ts - Remove redundant mockTenantStorage alias in resolveAppConfig.spec.ts - Narrow eslint-disable to specific require line * test: add tenant domain rejection test for LDAP strategy Covers the appConfig !== baseConfig && !isEmailDomainAllowed path, consistent with SAML, OpenID, and social strategy specs. * refactor: rename resolveAppConfig to app/resolve per AGENTS.md Rename resolveAppConfig.ts → resolve.ts and resolveAppConfig.spec.ts → resolve.spec.ts to align with the project's concise naming convention. * fix: remove fragile reference-equality guard, add logging and docs Remove appConfig !== baseConfig guard from all strategies and requestPasswordReset. The guard relied on implicit cache-backend identity semantics (in-memory Keyv returns same object reference) that would silently break with Redis or cloned configs. The second isEmailDomainAllowed call is a cheap synchronous check — always running it is clearer and eliminates the coupling. Add audit logging to requestPasswordReset domain blocks (base and tenant), consistent with all auth strategies. Extract duplicated error construction into makeDomainDeniedError(). Wrap resolveAppConfigForUser in requestPasswordReset with try/catch to prevent DB errors from leaking to the client via the controller's generic catch handler. Document the dual tenantId propagation (ALS for DB isolation, explicit param for cache key) in resolveAppConfigForUser JSDoc. Add comment documenting the LDAP error-type ordering change (cross-provider users from blocked domains now get 'domain not allowed' instead of AUTH_FAILED). Assert resolveAppConfigForUser is not called on LDAP provider mismatch path. * fix: return generic response for tenant domain block in password reset Tenant-scoped domain rejection in requestPasswordReset now returns the same generic "If an account with that email exists..." response instead of an Error. This prevents user-enumeration: an attacker cannot distinguish between "email not found" and "tenant blocks this domain" by comparing HTTP responses. The base-config fast-fail (pre-user-lookup) still returns an Error since it fires before any user existence is revealed. * docs: document phase 1 vs phase 2 domain check behavior in JSDoc Phase 1 (base config, pre-findUser) intentionally returns Error/400 to reveal globally blocked domains without confirming user existence. Phase 2 (tenant config, post-findUser) returns generic 200 to prevent user-enumeration. This distinction is now explicit in the JSDoc.
2026-03-27 16:08:43 -04:00
}
🔐 feat: Admin Auth Support for SAML and Social OAuth Providers (#12472) * refactor: Add existingUsersOnly support to social and SAML auth callbacks - Add `existingUsersOnly` option to the `socialLogin` handler factory to reject unknown users instead of creating new accounts - Refactor SAML strategy callback into `createSamlCallback(existingUsersOnly)` factory function, mirroring the OpenID `createOpenIDCallback` pattern - Extract shared SAML config into `getBaseSamlConfig()` helper - Register `samlAdmin` passport strategy with `existingUsersOnly: true` and admin-specific callback URL, called automatically from `setupSaml()` * feat: Register admin OAuth strategy variants for all social providers - Add admin strategy exports to Google, GitHub, Discord, Facebook, and Apple strategy files with admin callback URLs and existingUsersOnly - Extract provider configs into reusable helpers to avoid duplication between regular and admin strategy constructors - Re-export all admin strategy factories from strategies/index.js - Register admin passport strategies (googleAdmin, githubAdmin, etc.) alongside regular ones in socialLogins.js when env vars are present * feat: Add admin auth routes for SAML and social OAuth providers - Add initiation and callback routes for SAML, Google, GitHub, Discord, Facebook, and Apple to the admin auth router - Each provider follows the exchange code + PKCE pattern established by OpenID admin auth: store PKCE challenge on initiation, retrieve on callback, generate exchange code for the admin panel - SAML and Apple use POST callbacks with state extracted from req.body.RelayState and req.body.state respectively - Extract storePkceChallenge(), retrievePkceChallenge(), and generateState() helpers; refactor existing OpenID routes to use them - All callback chains enforce requireAdminAccess, setBalanceConfig, checkDomainAllowed, and the shared createOAuthHandler - No changes needed to the generic POST /oauth/exchange endpoint * fix: Update SAML strategy test to handle dual strategy registration setupSaml() now registers both 'saml' and 'samlAdmin' strategies, causing the SamlStrategy mock to be called twice. The verifyCallback variable was getting overwritten with the admin callback (which has existingUsersOnly: true), making all new-user tests fail. Fix: capture only the first callback per setupSaml() call and reset between tests. * fix: Address review findings for admin OAuth strategy changes - Fix existingUsersOnly rejection in socialLogin.js to use cb(null, false, { message }) instead of cb(error), ensuring passport's failureRedirect fires correctly for admin flows - Consolidate duplicate require() calls in strategies/index.js by destructuring admin exports from the already-imported default export - Pass pre-parsed baseConfig to setupSamlAdmin() to avoid redundant certificate file I/O at startup - Extract getGoogleConfig() helper in googleStrategy.js for consistency with all other provider strategy files - Replace randomState() (openid-client) with generateState() (crypto) in the OpenID admin route for consistency with all other providers, and remove the now-unused openid-client import * Reorder import statements in auth.js
2026-03-30 22:49:44 -04:00
const { saveBuffer } = getStrategyFunctions(
appConfig?.fileStrategy ?? process.env.CDN_PROVIDER,
);
🔐 feat: Admin Auth Support for SAML and Social OAuth Providers (#12472) * refactor: Add existingUsersOnly support to social and SAML auth callbacks - Add `existingUsersOnly` option to the `socialLogin` handler factory to reject unknown users instead of creating new accounts - Refactor SAML strategy callback into `createSamlCallback(existingUsersOnly)` factory function, mirroring the OpenID `createOpenIDCallback` pattern - Extract shared SAML config into `getBaseSamlConfig()` helper - Register `samlAdmin` passport strategy with `existingUsersOnly: true` and admin-specific callback URL, called automatically from `setupSaml()` * feat: Register admin OAuth strategy variants for all social providers - Add admin strategy exports to Google, GitHub, Discord, Facebook, and Apple strategy files with admin callback URLs and existingUsersOnly - Extract provider configs into reusable helpers to avoid duplication between regular and admin strategy constructors - Re-export all admin strategy factories from strategies/index.js - Register admin passport strategies (googleAdmin, githubAdmin, etc.) alongside regular ones in socialLogins.js when env vars are present * feat: Add admin auth routes for SAML and social OAuth providers - Add initiation and callback routes for SAML, Google, GitHub, Discord, Facebook, and Apple to the admin auth router - Each provider follows the exchange code + PKCE pattern established by OpenID admin auth: store PKCE challenge on initiation, retrieve on callback, generate exchange code for the admin panel - SAML and Apple use POST callbacks with state extracted from req.body.RelayState and req.body.state respectively - Extract storePkceChallenge(), retrievePkceChallenge(), and generateState() helpers; refactor existing OpenID routes to use them - All callback chains enforce requireAdminAccess, setBalanceConfig, checkDomainAllowed, and the shared createOAuthHandler - No changes needed to the generic POST /oauth/exchange endpoint * fix: Update SAML strategy test to handle dual strategy registration setupSaml() now registers both 'saml' and 'samlAdmin' strategies, causing the SamlStrategy mock to be called twice. The verifyCallback variable was getting overwritten with the admin callback (which has existingUsersOnly: true), making all new-user tests fail. Fix: capture only the first callback per setupSaml() call and reset between tests. * fix: Address review findings for admin OAuth strategy changes - Fix existingUsersOnly rejection in socialLogin.js to use cb(null, false, { message }) instead of cb(error), ensuring passport's failureRedirect fires correctly for admin flows - Consolidate duplicate require() calls in strategies/index.js by destructuring admin exports from the already-imported default export - Pass pre-parsed baseConfig to setupSamlAdmin() to avoid redundant certificate file I/O at startup - Extract getGoogleConfig() helper in googleStrategy.js for consistency with all other provider strategy files - Replace randomState() (openid-client) with generateState() (crypto) in the OpenID admin route for consistency with all other providers, and remove the now-unused openid-client import * Reorder import statements in auth.js
2026-03-30 22:49:44 -04:00
const imagePath = await saveBuffer({
fileName,
userId: user._id.toString(),
buffer: imageBuffer,
});
user.avatar = imagePath ?? '';
}
}
user = await updateUser(user._id, user);
logger.info(
`[samlStrategy] Login success SAML ID: ${user.samlId} | email: ${user.email} | username: ${user.username}`,
{
user: {
samlId: user.samlId,
username: user.username,
email: user.email,
name: user.name,
},
},
);
done(null, user);
} catch (err) {
logger.error('[samlStrategy] Login failed', err);
done(err);
}
};
}
🔐 feat: Admin Auth Support for SAML and Social OAuth Providers (#12472) * refactor: Add existingUsersOnly support to social and SAML auth callbacks - Add `existingUsersOnly` option to the `socialLogin` handler factory to reject unknown users instead of creating new accounts - Refactor SAML strategy callback into `createSamlCallback(existingUsersOnly)` factory function, mirroring the OpenID `createOpenIDCallback` pattern - Extract shared SAML config into `getBaseSamlConfig()` helper - Register `samlAdmin` passport strategy with `existingUsersOnly: true` and admin-specific callback URL, called automatically from `setupSaml()` * feat: Register admin OAuth strategy variants for all social providers - Add admin strategy exports to Google, GitHub, Discord, Facebook, and Apple strategy files with admin callback URLs and existingUsersOnly - Extract provider configs into reusable helpers to avoid duplication between regular and admin strategy constructors - Re-export all admin strategy factories from strategies/index.js - Register admin passport strategies (googleAdmin, githubAdmin, etc.) alongside regular ones in socialLogins.js when env vars are present * feat: Add admin auth routes for SAML and social OAuth providers - Add initiation and callback routes for SAML, Google, GitHub, Discord, Facebook, and Apple to the admin auth router - Each provider follows the exchange code + PKCE pattern established by OpenID admin auth: store PKCE challenge on initiation, retrieve on callback, generate exchange code for the admin panel - SAML and Apple use POST callbacks with state extracted from req.body.RelayState and req.body.state respectively - Extract storePkceChallenge(), retrievePkceChallenge(), and generateState() helpers; refactor existing OpenID routes to use them - All callback chains enforce requireAdminAccess, setBalanceConfig, checkDomainAllowed, and the shared createOAuthHandler - No changes needed to the generic POST /oauth/exchange endpoint * fix: Update SAML strategy test to handle dual strategy registration setupSaml() now registers both 'saml' and 'samlAdmin' strategies, causing the SamlStrategy mock to be called twice. The verifyCallback variable was getting overwritten with the admin callback (which has existingUsersOnly: true), making all new-user tests fail. Fix: capture only the first callback per setupSaml() call and reset between tests. * fix: Address review findings for admin OAuth strategy changes - Fix existingUsersOnly rejection in socialLogin.js to use cb(null, false, { message }) instead of cb(error), ensuring passport's failureRedirect fires correctly for admin flows - Consolidate duplicate require() calls in strategies/index.js by destructuring admin exports from the already-imported default export - Pass pre-parsed baseConfig to setupSamlAdmin() to avoid redundant certificate file I/O at startup - Extract getGoogleConfig() helper in googleStrategy.js for consistency with all other provider strategy files - Replace randomState() (openid-client) with generateState() (crypto) in the OpenID admin route for consistency with all other providers, and remove the now-unused openid-client import * Reorder import statements in auth.js
2026-03-30 22:49:44 -04:00
/**
* Returns the base SAML configuration shared by both regular and admin strategies.
* @returns {object} The SAML configuration object.
*/
function getBaseSamlConfig() {
return {
entryPoint: process.env.SAML_ENTRY_POINT,
issuer: process.env.SAML_ISSUER,
idpCert: getCertificateContent(process.env.SAML_CERT),
wantAssertionsSigned: process.env.SAML_USE_AUTHN_RESPONSE_SIGNED === 'true' ? false : true,
wantAuthnResponseSigned: process.env.SAML_USE_AUTHN_RESPONSE_SIGNED === 'true' ? true : false,
};
}
🔐 feat: Admin Auth Support for SAML and Social OAuth Providers (#12472) * refactor: Add existingUsersOnly support to social and SAML auth callbacks - Add `existingUsersOnly` option to the `socialLogin` handler factory to reject unknown users instead of creating new accounts - Refactor SAML strategy callback into `createSamlCallback(existingUsersOnly)` factory function, mirroring the OpenID `createOpenIDCallback` pattern - Extract shared SAML config into `getBaseSamlConfig()` helper - Register `samlAdmin` passport strategy with `existingUsersOnly: true` and admin-specific callback URL, called automatically from `setupSaml()` * feat: Register admin OAuth strategy variants for all social providers - Add admin strategy exports to Google, GitHub, Discord, Facebook, and Apple strategy files with admin callback URLs and existingUsersOnly - Extract provider configs into reusable helpers to avoid duplication between regular and admin strategy constructors - Re-export all admin strategy factories from strategies/index.js - Register admin passport strategies (googleAdmin, githubAdmin, etc.) alongside regular ones in socialLogins.js when env vars are present * feat: Add admin auth routes for SAML and social OAuth providers - Add initiation and callback routes for SAML, Google, GitHub, Discord, Facebook, and Apple to the admin auth router - Each provider follows the exchange code + PKCE pattern established by OpenID admin auth: store PKCE challenge on initiation, retrieve on callback, generate exchange code for the admin panel - SAML and Apple use POST callbacks with state extracted from req.body.RelayState and req.body.state respectively - Extract storePkceChallenge(), retrievePkceChallenge(), and generateState() helpers; refactor existing OpenID routes to use them - All callback chains enforce requireAdminAccess, setBalanceConfig, checkDomainAllowed, and the shared createOAuthHandler - No changes needed to the generic POST /oauth/exchange endpoint * fix: Update SAML strategy test to handle dual strategy registration setupSaml() now registers both 'saml' and 'samlAdmin' strategies, causing the SamlStrategy mock to be called twice. The verifyCallback variable was getting overwritten with the admin callback (which has existingUsersOnly: true), making all new-user tests fail. Fix: capture only the first callback per setupSaml() call and reset between tests. * fix: Address review findings for admin OAuth strategy changes - Fix existingUsersOnly rejection in socialLogin.js to use cb(null, false, { message }) instead of cb(error), ensuring passport's failureRedirect fires correctly for admin flows - Consolidate duplicate require() calls in strategies/index.js by destructuring admin exports from the already-imported default export - Pass pre-parsed baseConfig to setupSamlAdmin() to avoid redundant certificate file I/O at startup - Extract getGoogleConfig() helper in googleStrategy.js for consistency with all other provider strategy files - Replace randomState() (openid-client) with generateState() (crypto) in the OpenID admin route for consistency with all other providers, and remove the now-unused openid-client import * Reorder import statements in auth.js
2026-03-30 22:49:44 -04:00
async function setupSaml() {
try {
const baseConfig = getBaseSamlConfig();
const samlConfig = {
...baseConfig,
callbackUrl: process.env.SAML_CALLBACK_URL,
};
🔐 feat: Admin Auth Support for SAML and Social OAuth Providers (#12472) * refactor: Add existingUsersOnly support to social and SAML auth callbacks - Add `existingUsersOnly` option to the `socialLogin` handler factory to reject unknown users instead of creating new accounts - Refactor SAML strategy callback into `createSamlCallback(existingUsersOnly)` factory function, mirroring the OpenID `createOpenIDCallback` pattern - Extract shared SAML config into `getBaseSamlConfig()` helper - Register `samlAdmin` passport strategy with `existingUsersOnly: true` and admin-specific callback URL, called automatically from `setupSaml()` * feat: Register admin OAuth strategy variants for all social providers - Add admin strategy exports to Google, GitHub, Discord, Facebook, and Apple strategy files with admin callback URLs and existingUsersOnly - Extract provider configs into reusable helpers to avoid duplication between regular and admin strategy constructors - Re-export all admin strategy factories from strategies/index.js - Register admin passport strategies (googleAdmin, githubAdmin, etc.) alongside regular ones in socialLogins.js when env vars are present * feat: Add admin auth routes for SAML and social OAuth providers - Add initiation and callback routes for SAML, Google, GitHub, Discord, Facebook, and Apple to the admin auth router - Each provider follows the exchange code + PKCE pattern established by OpenID admin auth: store PKCE challenge on initiation, retrieve on callback, generate exchange code for the admin panel - SAML and Apple use POST callbacks with state extracted from req.body.RelayState and req.body.state respectively - Extract storePkceChallenge(), retrievePkceChallenge(), and generateState() helpers; refactor existing OpenID routes to use them - All callback chains enforce requireAdminAccess, setBalanceConfig, checkDomainAllowed, and the shared createOAuthHandler - No changes needed to the generic POST /oauth/exchange endpoint * fix: Update SAML strategy test to handle dual strategy registration setupSaml() now registers both 'saml' and 'samlAdmin' strategies, causing the SamlStrategy mock to be called twice. The verifyCallback variable was getting overwritten with the admin callback (which has existingUsersOnly: true), making all new-user tests fail. Fix: capture only the first callback per setupSaml() call and reset between tests. * fix: Address review findings for admin OAuth strategy changes - Fix existingUsersOnly rejection in socialLogin.js to use cb(null, false, { message }) instead of cb(error), ensuring passport's failureRedirect fires correctly for admin flows - Consolidate duplicate require() calls in strategies/index.js by destructuring admin exports from the already-imported default export - Pass pre-parsed baseConfig to setupSamlAdmin() to avoid redundant certificate file I/O at startup - Extract getGoogleConfig() helper in googleStrategy.js for consistency with all other provider strategy files - Replace randomState() (openid-client) with generateState() (crypto) in the OpenID admin route for consistency with all other providers, and remove the now-unused openid-client import * Reorder import statements in auth.js
2026-03-30 22:49:44 -04:00
passport.use('saml', new SamlStrategy(samlConfig, createSamlCallback(false)));
setupSamlAdmin(baseConfig);
} catch (err) {
logger.error('[samlStrategy]', err);
}
}
🔐 feat: Admin Auth Support for SAML and Social OAuth Providers (#12472) * refactor: Add existingUsersOnly support to social and SAML auth callbacks - Add `existingUsersOnly` option to the `socialLogin` handler factory to reject unknown users instead of creating new accounts - Refactor SAML strategy callback into `createSamlCallback(existingUsersOnly)` factory function, mirroring the OpenID `createOpenIDCallback` pattern - Extract shared SAML config into `getBaseSamlConfig()` helper - Register `samlAdmin` passport strategy with `existingUsersOnly: true` and admin-specific callback URL, called automatically from `setupSaml()` * feat: Register admin OAuth strategy variants for all social providers - Add admin strategy exports to Google, GitHub, Discord, Facebook, and Apple strategy files with admin callback URLs and existingUsersOnly - Extract provider configs into reusable helpers to avoid duplication between regular and admin strategy constructors - Re-export all admin strategy factories from strategies/index.js - Register admin passport strategies (googleAdmin, githubAdmin, etc.) alongside regular ones in socialLogins.js when env vars are present * feat: Add admin auth routes for SAML and social OAuth providers - Add initiation and callback routes for SAML, Google, GitHub, Discord, Facebook, and Apple to the admin auth router - Each provider follows the exchange code + PKCE pattern established by OpenID admin auth: store PKCE challenge on initiation, retrieve on callback, generate exchange code for the admin panel - SAML and Apple use POST callbacks with state extracted from req.body.RelayState and req.body.state respectively - Extract storePkceChallenge(), retrievePkceChallenge(), and generateState() helpers; refactor existing OpenID routes to use them - All callback chains enforce requireAdminAccess, setBalanceConfig, checkDomainAllowed, and the shared createOAuthHandler - No changes needed to the generic POST /oauth/exchange endpoint * fix: Update SAML strategy test to handle dual strategy registration setupSaml() now registers both 'saml' and 'samlAdmin' strategies, causing the SamlStrategy mock to be called twice. The verifyCallback variable was getting overwritten with the admin callback (which has existingUsersOnly: true), making all new-user tests fail. Fix: capture only the first callback per setupSaml() call and reset between tests. * fix: Address review findings for admin OAuth strategy changes - Fix existingUsersOnly rejection in socialLogin.js to use cb(null, false, { message }) instead of cb(error), ensuring passport's failureRedirect fires correctly for admin flows - Consolidate duplicate require() calls in strategies/index.js by destructuring admin exports from the already-imported default export - Pass pre-parsed baseConfig to setupSamlAdmin() to avoid redundant certificate file I/O at startup - Extract getGoogleConfig() helper in googleStrategy.js for consistency with all other provider strategy files - Replace randomState() (openid-client) with generateState() (crypto) in the OpenID admin route for consistency with all other providers, and remove the now-unused openid-client import * Reorder import statements in auth.js
2026-03-30 22:49:44 -04:00
/**
* Sets up the SAML strategy specifically for admin authentication.
* Rejects users that don't already exist.
* @param {object} [baseConfig] - Pre-parsed base SAML config to avoid redundant cert parsing.
*/
function setupSamlAdmin(baseConfig) {
try {
const samlAdminConfig = {
...(baseConfig ?? getBaseSamlConfig()),
callbackUrl: `${process.env.DOMAIN_SERVER}/api/admin/oauth/saml/callback`,
};
passport.use('samlAdmin', new SamlStrategy(samlAdminConfig, createSamlCallback(true)));
logger.info('[samlStrategy] Admin SAML strategy registered.');
} catch (err) {
logger.error('[samlStrategy] setupSamlAdmin', err);
}
}
module.exports = { setupSaml, getCertificateContent };