2025-05-30 00:00:58 +09:00
|
|
|
// --- Mocks ---
|
|
|
|
|
jest.mock('fs');
|
|
|
|
|
jest.mock('path');
|
|
|
|
|
jest.mock('node-fetch');
|
|
|
|
|
jest.mock('@node-saml/passport-saml');
|
🪐 feat: MCP OAuth 2.0 Discovery Support (#7924)
* chore: Update @modelcontextprotocol/sdk to version 1.12.3 in package.json and package-lock.json
- Bump version of @modelcontextprotocol/sdk to 1.12.3 to incorporate recent updates.
- Update dependencies for ajv and cross-spawn to their latest versions.
- Add ajv as a new dependency in the sdk module.
- Include json-schema-traverse as a new dependency in the sdk module.
* feat: @librechat/auth
* feat: Add crypto module exports to auth package
- Introduced a new crypto module by creating index.ts in the crypto directory.
- Updated the main index.ts of the auth package to export from the new crypto module.
* feat: Update package dependencies and build scripts for auth package
- Added @librechat/auth as a dependency in package.json and package-lock.json.
- Updated build scripts to include the auth package in both frontend and bun build processes.
- Removed unused mongoose and openid-client dependencies from package-lock.json for cleaner dependency management.
* refactor: Migrate crypto utility functions to @librechat/auth
- Replaced local crypto utility imports with the new @librechat/auth package across multiple files.
- Removed the obsolete crypto.js file and its exports.
- Updated relevant services and models to utilize the new encryption and decryption methods from @librechat/auth.
* feat: Enhance OAuth token handling and update dependencies in auth package
* chore: Remove Token model and TokenService due to restructuring of OAuth handling
- Deleted the Token.js model and TokenService.js, which were responsible for managing OAuth tokens.
- This change is part of a broader refactor to streamline OAuth token management and improve code organization.
* refactor: imports from '@librechat/auth' to '@librechat/api' and add OAuth token handling functionality
* refactor: Simplify logger usage in MCP and FlowStateManager classes
* chore: fix imports
* feat: Add OAuth configuration schema to MCP with token exchange method support
* feat: FIRST PASS Implement MCP OAuth flow with token management and error handling
- Added a new route for handling OAuth callbacks and token retrieval.
- Integrated OAuth token storage and retrieval mechanisms.
- Enhanced MCP connection to support automatic OAuth flow initiation on 401 errors.
- Implemented dynamic client registration and metadata discovery for OAuth.
- Updated MCPManager to manage OAuth tokens and handle authentication requirements.
- Introduced comprehensive logging for OAuth processes and error handling.
* refactor: Update MCPConnection and MCPManager to utilize new URL handling
- Added a `url` property to MCPConnection for better URL management.
- Refactored MCPManager to use the new `url` property instead of a deprecated method for OAuth handling.
- Changed logging from info to debug level for flow manager and token methods initialization.
- Improved comments for clarity on existing tokens and OAuth event listener setup.
* refactor: Improve connection timeout error messages in MCPConnection and MCPManager and use initTimeout for connection
- Updated the connection timeout error messages to include the duration of the timeout.
- Introduced a configurable `connectTimeout` variable in both MCPConnection and MCPManager for better flexibility.
* chore: cleanup MCP OAuth Token exchange handling; fix: erroneous use of flowsCache and remove verbose logs
* refactor: Update MCPManager and MCPTokenStorage to use TokenMethods for token management
- Removed direct token storage handling in MCPManager and replaced it with TokenMethods for better abstraction.
- Refactored MCPTokenStorage methods to accept parameters for token operations, enhancing flexibility and readability.
- Improved logging messages related to token persistence and retrieval processes.
* refactor: Update MCP OAuth handling to use static methods and improve flow management
- Refactored MCPOAuthHandler to utilize static methods for initiating and completing OAuth flows, enhancing clarity and reducing instance dependencies.
- Updated MCPManager to pass flowManager explicitly to OAuth handling methods, improving flexibility in flow state management.
- Enhanced comments and logging for better understanding of OAuth processes and flow state retrieval.
* refactor: Integrate token methods into createMCPTool for enhanced token management
* refactor: Change logging from info to debug level in MCPOAuthHandler for improved log management
* chore: clean up logging
* feat: first pass, auth URL from MCP OAuth flow
* chore: Improve logging format for OAuth authentication URL display
* chore: cleanup mcp manager comments
* feat: add connection reconnection logic in MCPManager
* refactor: reorganize token storage handling in MCP
- Moved token storage logic from MCPManager to a new MCPTokenStorage class for better separation of concerns.
- Updated imports to reflect the new token storage structure.
- Enhanced methods for storing, retrieving, updating, and deleting OAuth tokens, improving overall token management.
* chore: update comment for SYSTEM_USER_ID in MCPManager for clarity
* feat: implement refresh token functionality in MCP
- Added refresh token handling in MCPManager to support token renewal for both app-level and user-specific connections.
- Introduced a refreshTokens function to facilitate token refresh logic.
- Enhanced MCPTokenStorage to manage client information and refresh token processes.
- Updated logging for better traceability during token operations.
* chore: cleanup @librechat/auth
* feat: implement MCP server initialization in a separate service
- Added a new service to handle the initialization of MCP servers, improving code organization and readability.
- Refactored the server startup logic to utilize the new initializeMCP function.
- Removed redundant MCP initialization code from the main server file.
* fix: don't log auth url for user connections
* feat: enhance OAuth flow with success and error handling components
- Updated OAuth callback routes to redirect to new success and error pages instead of sending status messages.
- Introduced `OAuthSuccess` and `OAuthError` components to provide user feedback during authentication.
- Added localization support for success and error messages in the translation files.
- Implemented countdown functionality in the success component for a better user experience.
* fix: refresh token handling for user connections, add missing URL and methods
- add standard enum for system user id and helper for determining app-lvel vs. user-level connections
* refactor: update token handling in MCPManager and MCPTokenStorage
* fix: improve error logging in OAuth authentication handler
* fix: concurrency issues for both login url emission and concurrency of oauth flows for shared flows (same user, same server, multiple calls for same server)
* fix: properly fail shared flows for concurrent server calls and prevent duplication of tokens
* chore: remove unused auth package directory from update configuration
* ci: fix mocks in samlStrategy tests
* ci: add mcpConfig to AppService test setup
* chore: remove obsolete MCP OAuth implementation documentation
* fix: update build script for API to use correct command
* chore: bump version of @librechat/api to 1.2.4
* fix: update abort signal handling in createMCPTool function
* fix: add optional clientInfo parameter to refreshTokensFunction metadata
* refactor: replace app.locals.availableTools with getCachedTools in multiple services and controllers for improved tool management
* fix: concurrent refresh token handling issue
* refactor: add signal parameter to getUserConnection method for improved abort handling
* chore: JSDoc typing for `loadEphemeralAgent`
* refactor: update isConnectionActive method to use destructured parameters for improved readability
* feat: implement caching for MCP tools to handle app-level disconnects for loading list of tools
* ci: fix agent test
2025-06-17 13:50:33 -04:00
|
|
|
jest.mock('@librechat/data-schemas', () => ({
|
|
|
|
|
logger: {
|
|
|
|
|
info: jest.fn(),
|
|
|
|
|
debug: jest.fn(),
|
|
|
|
|
error: jest.fn(),
|
|
|
|
|
},
|
|
|
|
|
hashToken: jest.fn().mockResolvedValue('hashed-token'),
|
|
|
|
|
}));
|
2025-05-30 22:18:13 -04:00
|
|
|
jest.mock('~/models', () => ({
|
2025-05-30 00:00:58 +09:00
|
|
|
findUser: jest.fn(),
|
|
|
|
|
createUser: jest.fn(),
|
|
|
|
|
updateUser: jest.fn(),
|
|
|
|
|
}));
|
2025-05-30 22:18:13 -04:00
|
|
|
jest.mock('~/server/services/Config', () => ({
|
|
|
|
|
config: {
|
|
|
|
|
registration: {
|
|
|
|
|
socialLogins: ['saml'],
|
|
|
|
|
},
|
|
|
|
|
},
|
2025-08-26 12:10:18 -04:00
|
|
|
getAppConfig: jest.fn().mockResolvedValue({}),
|
|
|
|
|
}));
|
|
|
|
|
jest.mock('@librechat/api', () => ({
|
2025-09-27 21:20:19 -04:00
|
|
|
isEmailDomainAllowed: jest.fn(() => true),
|
2025-08-26 12:10:18 -04:00
|
|
|
getBalanceConfig: jest.fn(() => ({
|
2025-05-30 22:18:13 -04:00
|
|
|
tokenCredits: 1000,
|
2025-08-26 12:10:18 -04:00
|
|
|
startBalance: 1000,
|
|
|
|
|
})),
|
🏢 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
|
|
|
resolveAppConfigForUser: jest.fn(async (_getAppConfig, _user) => ({})),
|
2025-05-30 22:18:13 -04:00
|
|
|
}));
|
|
|
|
|
jest.mock('~/server/services/Config/EndpointService', () => ({
|
|
|
|
|
config: {},
|
|
|
|
|
}));
|
2025-05-30 00:00:58 +09:00
|
|
|
jest.mock('~/server/services/Files/strategies', () => ({
|
|
|
|
|
getStrategyFunctions: jest.fn(() => ({
|
|
|
|
|
saveBuffer: jest.fn().mockResolvedValue('/fake/path/to/avatar.png'),
|
|
|
|
|
})),
|
|
|
|
|
}));
|
🪐 feat: MCP OAuth 2.0 Discovery Support (#7924)
* chore: Update @modelcontextprotocol/sdk to version 1.12.3 in package.json and package-lock.json
- Bump version of @modelcontextprotocol/sdk to 1.12.3 to incorporate recent updates.
- Update dependencies for ajv and cross-spawn to their latest versions.
- Add ajv as a new dependency in the sdk module.
- Include json-schema-traverse as a new dependency in the sdk module.
* feat: @librechat/auth
* feat: Add crypto module exports to auth package
- Introduced a new crypto module by creating index.ts in the crypto directory.
- Updated the main index.ts of the auth package to export from the new crypto module.
* feat: Update package dependencies and build scripts for auth package
- Added @librechat/auth as a dependency in package.json and package-lock.json.
- Updated build scripts to include the auth package in both frontend and bun build processes.
- Removed unused mongoose and openid-client dependencies from package-lock.json for cleaner dependency management.
* refactor: Migrate crypto utility functions to @librechat/auth
- Replaced local crypto utility imports with the new @librechat/auth package across multiple files.
- Removed the obsolete crypto.js file and its exports.
- Updated relevant services and models to utilize the new encryption and decryption methods from @librechat/auth.
* feat: Enhance OAuth token handling and update dependencies in auth package
* chore: Remove Token model and TokenService due to restructuring of OAuth handling
- Deleted the Token.js model and TokenService.js, which were responsible for managing OAuth tokens.
- This change is part of a broader refactor to streamline OAuth token management and improve code organization.
* refactor: imports from '@librechat/auth' to '@librechat/api' and add OAuth token handling functionality
* refactor: Simplify logger usage in MCP and FlowStateManager classes
* chore: fix imports
* feat: Add OAuth configuration schema to MCP with token exchange method support
* feat: FIRST PASS Implement MCP OAuth flow with token management and error handling
- Added a new route for handling OAuth callbacks and token retrieval.
- Integrated OAuth token storage and retrieval mechanisms.
- Enhanced MCP connection to support automatic OAuth flow initiation on 401 errors.
- Implemented dynamic client registration and metadata discovery for OAuth.
- Updated MCPManager to manage OAuth tokens and handle authentication requirements.
- Introduced comprehensive logging for OAuth processes and error handling.
* refactor: Update MCPConnection and MCPManager to utilize new URL handling
- Added a `url` property to MCPConnection for better URL management.
- Refactored MCPManager to use the new `url` property instead of a deprecated method for OAuth handling.
- Changed logging from info to debug level for flow manager and token methods initialization.
- Improved comments for clarity on existing tokens and OAuth event listener setup.
* refactor: Improve connection timeout error messages in MCPConnection and MCPManager and use initTimeout for connection
- Updated the connection timeout error messages to include the duration of the timeout.
- Introduced a configurable `connectTimeout` variable in both MCPConnection and MCPManager for better flexibility.
* chore: cleanup MCP OAuth Token exchange handling; fix: erroneous use of flowsCache and remove verbose logs
* refactor: Update MCPManager and MCPTokenStorage to use TokenMethods for token management
- Removed direct token storage handling in MCPManager and replaced it with TokenMethods for better abstraction.
- Refactored MCPTokenStorage methods to accept parameters for token operations, enhancing flexibility and readability.
- Improved logging messages related to token persistence and retrieval processes.
* refactor: Update MCP OAuth handling to use static methods and improve flow management
- Refactored MCPOAuthHandler to utilize static methods for initiating and completing OAuth flows, enhancing clarity and reducing instance dependencies.
- Updated MCPManager to pass flowManager explicitly to OAuth handling methods, improving flexibility in flow state management.
- Enhanced comments and logging for better understanding of OAuth processes and flow state retrieval.
* refactor: Integrate token methods into createMCPTool for enhanced token management
* refactor: Change logging from info to debug level in MCPOAuthHandler for improved log management
* chore: clean up logging
* feat: first pass, auth URL from MCP OAuth flow
* chore: Improve logging format for OAuth authentication URL display
* chore: cleanup mcp manager comments
* feat: add connection reconnection logic in MCPManager
* refactor: reorganize token storage handling in MCP
- Moved token storage logic from MCPManager to a new MCPTokenStorage class for better separation of concerns.
- Updated imports to reflect the new token storage structure.
- Enhanced methods for storing, retrieving, updating, and deleting OAuth tokens, improving overall token management.
* chore: update comment for SYSTEM_USER_ID in MCPManager for clarity
* feat: implement refresh token functionality in MCP
- Added refresh token handling in MCPManager to support token renewal for both app-level and user-specific connections.
- Introduced a refreshTokens function to facilitate token refresh logic.
- Enhanced MCPTokenStorage to manage client information and refresh token processes.
- Updated logging for better traceability during token operations.
* chore: cleanup @librechat/auth
* feat: implement MCP server initialization in a separate service
- Added a new service to handle the initialization of MCP servers, improving code organization and readability.
- Refactored the server startup logic to utilize the new initializeMCP function.
- Removed redundant MCP initialization code from the main server file.
* fix: don't log auth url for user connections
* feat: enhance OAuth flow with success and error handling components
- Updated OAuth callback routes to redirect to new success and error pages instead of sending status messages.
- Introduced `OAuthSuccess` and `OAuthError` components to provide user feedback during authentication.
- Added localization support for success and error messages in the translation files.
- Implemented countdown functionality in the success component for a better user experience.
* fix: refresh token handling for user connections, add missing URL and methods
- add standard enum for system user id and helper for determining app-lvel vs. user-level connections
* refactor: update token handling in MCPManager and MCPTokenStorage
* fix: improve error logging in OAuth authentication handler
* fix: concurrency issues for both login url emission and concurrency of oauth flows for shared flows (same user, same server, multiple calls for same server)
* fix: properly fail shared flows for concurrent server calls and prevent duplication of tokens
* chore: remove unused auth package directory from update configuration
* ci: fix mocks in samlStrategy tests
* ci: add mcpConfig to AppService test setup
* chore: remove obsolete MCP OAuth implementation documentation
* fix: update build script for API to use correct command
* chore: bump version of @librechat/api to 1.2.4
* fix: update abort signal handling in createMCPTool function
* fix: add optional clientInfo parameter to refreshTokensFunction metadata
* refactor: replace app.locals.availableTools with getCachedTools in multiple services and controllers for improved tool management
* fix: concurrent refresh token handling issue
* refactor: add signal parameter to getUserConnection method for improved abort handling
* chore: JSDoc typing for `loadEphemeralAgent`
* refactor: update isConnectionActive method to use destructured parameters for improved readability
* feat: implement caching for MCP tools to handle app-level disconnects for loading list of tools
* ci: fix agent test
2025-06-17 13:50:33 -04:00
|
|
|
jest.mock('~/config/paths', () => ({
|
|
|
|
|
root: '/fake/root/path',
|
2025-05-30 00:00:58 +09:00
|
|
|
}));
|
|
|
|
|
|
🪐 feat: MCP OAuth 2.0 Discovery Support (#7924)
* chore: Update @modelcontextprotocol/sdk to version 1.12.3 in package.json and package-lock.json
- Bump version of @modelcontextprotocol/sdk to 1.12.3 to incorporate recent updates.
- Update dependencies for ajv and cross-spawn to their latest versions.
- Add ajv as a new dependency in the sdk module.
- Include json-schema-traverse as a new dependency in the sdk module.
* feat: @librechat/auth
* feat: Add crypto module exports to auth package
- Introduced a new crypto module by creating index.ts in the crypto directory.
- Updated the main index.ts of the auth package to export from the new crypto module.
* feat: Update package dependencies and build scripts for auth package
- Added @librechat/auth as a dependency in package.json and package-lock.json.
- Updated build scripts to include the auth package in both frontend and bun build processes.
- Removed unused mongoose and openid-client dependencies from package-lock.json for cleaner dependency management.
* refactor: Migrate crypto utility functions to @librechat/auth
- Replaced local crypto utility imports with the new @librechat/auth package across multiple files.
- Removed the obsolete crypto.js file and its exports.
- Updated relevant services and models to utilize the new encryption and decryption methods from @librechat/auth.
* feat: Enhance OAuth token handling and update dependencies in auth package
* chore: Remove Token model and TokenService due to restructuring of OAuth handling
- Deleted the Token.js model and TokenService.js, which were responsible for managing OAuth tokens.
- This change is part of a broader refactor to streamline OAuth token management and improve code organization.
* refactor: imports from '@librechat/auth' to '@librechat/api' and add OAuth token handling functionality
* refactor: Simplify logger usage in MCP and FlowStateManager classes
* chore: fix imports
* feat: Add OAuth configuration schema to MCP with token exchange method support
* feat: FIRST PASS Implement MCP OAuth flow with token management and error handling
- Added a new route for handling OAuth callbacks and token retrieval.
- Integrated OAuth token storage and retrieval mechanisms.
- Enhanced MCP connection to support automatic OAuth flow initiation on 401 errors.
- Implemented dynamic client registration and metadata discovery for OAuth.
- Updated MCPManager to manage OAuth tokens and handle authentication requirements.
- Introduced comprehensive logging for OAuth processes and error handling.
* refactor: Update MCPConnection and MCPManager to utilize new URL handling
- Added a `url` property to MCPConnection for better URL management.
- Refactored MCPManager to use the new `url` property instead of a deprecated method for OAuth handling.
- Changed logging from info to debug level for flow manager and token methods initialization.
- Improved comments for clarity on existing tokens and OAuth event listener setup.
* refactor: Improve connection timeout error messages in MCPConnection and MCPManager and use initTimeout for connection
- Updated the connection timeout error messages to include the duration of the timeout.
- Introduced a configurable `connectTimeout` variable in both MCPConnection and MCPManager for better flexibility.
* chore: cleanup MCP OAuth Token exchange handling; fix: erroneous use of flowsCache and remove verbose logs
* refactor: Update MCPManager and MCPTokenStorage to use TokenMethods for token management
- Removed direct token storage handling in MCPManager and replaced it with TokenMethods for better abstraction.
- Refactored MCPTokenStorage methods to accept parameters for token operations, enhancing flexibility and readability.
- Improved logging messages related to token persistence and retrieval processes.
* refactor: Update MCP OAuth handling to use static methods and improve flow management
- Refactored MCPOAuthHandler to utilize static methods for initiating and completing OAuth flows, enhancing clarity and reducing instance dependencies.
- Updated MCPManager to pass flowManager explicitly to OAuth handling methods, improving flexibility in flow state management.
- Enhanced comments and logging for better understanding of OAuth processes and flow state retrieval.
* refactor: Integrate token methods into createMCPTool for enhanced token management
* refactor: Change logging from info to debug level in MCPOAuthHandler for improved log management
* chore: clean up logging
* feat: first pass, auth URL from MCP OAuth flow
* chore: Improve logging format for OAuth authentication URL display
* chore: cleanup mcp manager comments
* feat: add connection reconnection logic in MCPManager
* refactor: reorganize token storage handling in MCP
- Moved token storage logic from MCPManager to a new MCPTokenStorage class for better separation of concerns.
- Updated imports to reflect the new token storage structure.
- Enhanced methods for storing, retrieving, updating, and deleting OAuth tokens, improving overall token management.
* chore: update comment for SYSTEM_USER_ID in MCPManager for clarity
* feat: implement refresh token functionality in MCP
- Added refresh token handling in MCPManager to support token renewal for both app-level and user-specific connections.
- Introduced a refreshTokens function to facilitate token refresh logic.
- Enhanced MCPTokenStorage to manage client information and refresh token processes.
- Updated logging for better traceability during token operations.
* chore: cleanup @librechat/auth
* feat: implement MCP server initialization in a separate service
- Added a new service to handle the initialization of MCP servers, improving code organization and readability.
- Refactored the server startup logic to utilize the new initializeMCP function.
- Removed redundant MCP initialization code from the main server file.
* fix: don't log auth url for user connections
* feat: enhance OAuth flow with success and error handling components
- Updated OAuth callback routes to redirect to new success and error pages instead of sending status messages.
- Introduced `OAuthSuccess` and `OAuthError` components to provide user feedback during authentication.
- Added localization support for success and error messages in the translation files.
- Implemented countdown functionality in the success component for a better user experience.
* fix: refresh token handling for user connections, add missing URL and methods
- add standard enum for system user id and helper for determining app-lvel vs. user-level connections
* refactor: update token handling in MCPManager and MCPTokenStorage
* fix: improve error logging in OAuth authentication handler
* fix: concurrency issues for both login url emission and concurrency of oauth flows for shared flows (same user, same server, multiple calls for same server)
* fix: properly fail shared flows for concurrent server calls and prevent duplication of tokens
* chore: remove unused auth package directory from update configuration
* ci: fix mocks in samlStrategy tests
* ci: add mcpConfig to AppService test setup
* chore: remove obsolete MCP OAuth implementation documentation
* fix: update build script for API to use correct command
* chore: bump version of @librechat/api to 1.2.4
* fix: update abort signal handling in createMCPTool function
* fix: add optional clientInfo parameter to refreshTokensFunction metadata
* refactor: replace app.locals.availableTools with getCachedTools in multiple services and controllers for improved tool management
* fix: concurrent refresh token handling issue
* refactor: add signal parameter to getUserConnection method for improved abort handling
* chore: JSDoc typing for `loadEphemeralAgent`
* refactor: update isConnectionActive method to use destructured parameters for improved readability
* feat: implement caching for MCP tools to handle app-level disconnects for loading list of tools
* ci: fix agent test
2025-06-17 13:50:33 -04:00
|
|
|
const fs = require('fs');
|
|
|
|
|
const path = require('path');
|
|
|
|
|
const fetch = require('node-fetch');
|
|
|
|
|
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 { findUser } = require('~/models');
|
|
|
|
|
const { resolveAppConfigForUser } = require('@librechat/api');
|
|
|
|
|
const { getAppConfig } = require('~/server/services/Config');
|
🪐 feat: MCP OAuth 2.0 Discovery Support (#7924)
* chore: Update @modelcontextprotocol/sdk to version 1.12.3 in package.json and package-lock.json
- Bump version of @modelcontextprotocol/sdk to 1.12.3 to incorporate recent updates.
- Update dependencies for ajv and cross-spawn to their latest versions.
- Add ajv as a new dependency in the sdk module.
- Include json-schema-traverse as a new dependency in the sdk module.
* feat: @librechat/auth
* feat: Add crypto module exports to auth package
- Introduced a new crypto module by creating index.ts in the crypto directory.
- Updated the main index.ts of the auth package to export from the new crypto module.
* feat: Update package dependencies and build scripts for auth package
- Added @librechat/auth as a dependency in package.json and package-lock.json.
- Updated build scripts to include the auth package in both frontend and bun build processes.
- Removed unused mongoose and openid-client dependencies from package-lock.json for cleaner dependency management.
* refactor: Migrate crypto utility functions to @librechat/auth
- Replaced local crypto utility imports with the new @librechat/auth package across multiple files.
- Removed the obsolete crypto.js file and its exports.
- Updated relevant services and models to utilize the new encryption and decryption methods from @librechat/auth.
* feat: Enhance OAuth token handling and update dependencies in auth package
* chore: Remove Token model and TokenService due to restructuring of OAuth handling
- Deleted the Token.js model and TokenService.js, which were responsible for managing OAuth tokens.
- This change is part of a broader refactor to streamline OAuth token management and improve code organization.
* refactor: imports from '@librechat/auth' to '@librechat/api' and add OAuth token handling functionality
* refactor: Simplify logger usage in MCP and FlowStateManager classes
* chore: fix imports
* feat: Add OAuth configuration schema to MCP with token exchange method support
* feat: FIRST PASS Implement MCP OAuth flow with token management and error handling
- Added a new route for handling OAuth callbacks and token retrieval.
- Integrated OAuth token storage and retrieval mechanisms.
- Enhanced MCP connection to support automatic OAuth flow initiation on 401 errors.
- Implemented dynamic client registration and metadata discovery for OAuth.
- Updated MCPManager to manage OAuth tokens and handle authentication requirements.
- Introduced comprehensive logging for OAuth processes and error handling.
* refactor: Update MCPConnection and MCPManager to utilize new URL handling
- Added a `url` property to MCPConnection for better URL management.
- Refactored MCPManager to use the new `url` property instead of a deprecated method for OAuth handling.
- Changed logging from info to debug level for flow manager and token methods initialization.
- Improved comments for clarity on existing tokens and OAuth event listener setup.
* refactor: Improve connection timeout error messages in MCPConnection and MCPManager and use initTimeout for connection
- Updated the connection timeout error messages to include the duration of the timeout.
- Introduced a configurable `connectTimeout` variable in both MCPConnection and MCPManager for better flexibility.
* chore: cleanup MCP OAuth Token exchange handling; fix: erroneous use of flowsCache and remove verbose logs
* refactor: Update MCPManager and MCPTokenStorage to use TokenMethods for token management
- Removed direct token storage handling in MCPManager and replaced it with TokenMethods for better abstraction.
- Refactored MCPTokenStorage methods to accept parameters for token operations, enhancing flexibility and readability.
- Improved logging messages related to token persistence and retrieval processes.
* refactor: Update MCP OAuth handling to use static methods and improve flow management
- Refactored MCPOAuthHandler to utilize static methods for initiating and completing OAuth flows, enhancing clarity and reducing instance dependencies.
- Updated MCPManager to pass flowManager explicitly to OAuth handling methods, improving flexibility in flow state management.
- Enhanced comments and logging for better understanding of OAuth processes and flow state retrieval.
* refactor: Integrate token methods into createMCPTool for enhanced token management
* refactor: Change logging from info to debug level in MCPOAuthHandler for improved log management
* chore: clean up logging
* feat: first pass, auth URL from MCP OAuth flow
* chore: Improve logging format for OAuth authentication URL display
* chore: cleanup mcp manager comments
* feat: add connection reconnection logic in MCPManager
* refactor: reorganize token storage handling in MCP
- Moved token storage logic from MCPManager to a new MCPTokenStorage class for better separation of concerns.
- Updated imports to reflect the new token storage structure.
- Enhanced methods for storing, retrieving, updating, and deleting OAuth tokens, improving overall token management.
* chore: update comment for SYSTEM_USER_ID in MCPManager for clarity
* feat: implement refresh token functionality in MCP
- Added refresh token handling in MCPManager to support token renewal for both app-level and user-specific connections.
- Introduced a refreshTokens function to facilitate token refresh logic.
- Enhanced MCPTokenStorage to manage client information and refresh token processes.
- Updated logging for better traceability during token operations.
* chore: cleanup @librechat/auth
* feat: implement MCP server initialization in a separate service
- Added a new service to handle the initialization of MCP servers, improving code organization and readability.
- Refactored the server startup logic to utilize the new initializeMCP function.
- Removed redundant MCP initialization code from the main server file.
* fix: don't log auth url for user connections
* feat: enhance OAuth flow with success and error handling components
- Updated OAuth callback routes to redirect to new success and error pages instead of sending status messages.
- Introduced `OAuthSuccess` and `OAuthError` components to provide user feedback during authentication.
- Added localization support for success and error messages in the translation files.
- Implemented countdown functionality in the success component for a better user experience.
* fix: refresh token handling for user connections, add missing URL and methods
- add standard enum for system user id and helper for determining app-lvel vs. user-level connections
* refactor: update token handling in MCPManager and MCPTokenStorage
* fix: improve error logging in OAuth authentication handler
* fix: concurrency issues for both login url emission and concurrency of oauth flows for shared flows (same user, same server, multiple calls for same server)
* fix: properly fail shared flows for concurrent server calls and prevent duplication of tokens
* chore: remove unused auth package directory from update configuration
* ci: fix mocks in samlStrategy tests
* ci: add mcpConfig to AppService test setup
* chore: remove obsolete MCP OAuth implementation documentation
* fix: update build script for API to use correct command
* chore: bump version of @librechat/api to 1.2.4
* fix: update abort signal handling in createMCPTool function
* fix: add optional clientInfo parameter to refreshTokensFunction metadata
* refactor: replace app.locals.availableTools with getCachedTools in multiple services and controllers for improved tool management
* fix: concurrent refresh token handling issue
* refactor: add signal parameter to getUserConnection method for improved abort handling
* chore: JSDoc typing for `loadEphemeralAgent`
* refactor: update isConnectionActive method to use destructured parameters for improved readability
* feat: implement caching for MCP tools to handle app-level disconnects for loading list of tools
* ci: fix agent test
2025-06-17 13:50:33 -04:00
|
|
|
const { setupSaml, getCertificateContent } = require('./samlStrategy');
|
|
|
|
|
|
|
|
|
|
// Configure fs mock
|
|
|
|
|
jest.mocked(fs).existsSync = jest.fn();
|
|
|
|
|
jest.mocked(fs).statSync = jest.fn();
|
|
|
|
|
jest.mocked(fs).readFileSync = jest.fn();
|
|
|
|
|
|
2025-05-30 00:00:58 +09:00
|
|
|
// To capture the verify callback from the strategy, we grab it from the mock constructor
|
|
|
|
|
let verifyCallback;
|
|
|
|
|
SamlStrategy.mockImplementation((options, verify) => {
|
|
|
|
|
verifyCallback = verify;
|
|
|
|
|
return { name: 'saml', options, verify };
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('getCertificateContent', () => {
|
|
|
|
|
const certWithHeader = `-----BEGIN CERTIFICATE-----
|
|
|
|
|
MIIDazCCAlOgAwIBAgIUKhXaFJGJJPx466rlwYORIsqCq7MwDQYJKoZIhvcNAQEL
|
|
|
|
|
BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
|
|
|
|
|
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yNTAzMDQwODUxNTJaFw0yNjAz
|
|
|
|
|
MDQwODUxNTJaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw
|
|
|
|
|
HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB
|
|
|
|
|
AQUAA4IBDwAwggEKAoIBAQCWP09NZg0xaRiLpNygCVgV3M+4RFW2S0c5X/fg/uFT
|
|
|
|
|
O5MfaVYzG5GxzhXzWRB8RtNPsxX/nlbPsoUroeHbz+SABkOsNEv6JuKRH4VXRH34
|
|
|
|
|
VzjazVkPAwj+N4WqsC/Wo4EGGpKIGeGi8Zed4yvMqoTyE3mrS19fY0nMHT62wUwS
|
|
|
|
|
GMm2pAQdAQePZ9WY7A5XOA1IoxW2Zh2Oxaf1p59epBkZDhoxSMu8GoSkvK27Km4A
|
|
|
|
|
4UXftzdg/wHNPrNirmcYouioHdmrOtYxPjrhUBQ74AmE1/QK45B6wEgirKH1A1AW
|
|
|
|
|
6C+ApLwpBMvy9+8Gbyvc8G18W3CjdEVKmAeWb9JUedSXAgMBAAGjUzBRMB0GA1Ud
|
|
|
|
|
DgQWBBRxpaqBx8VDLLc8IkHATujj8IOs6jAfBgNVHSMEGDAWgBRxpaqBx8VDLLc8
|
|
|
|
|
IkHATujj8IOs6jAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBc
|
|
|
|
|
Puk6i+yowwGccB3LhfxZ+Fz6s6/Lfx6bP/Hy4NYOxmx2/awGBgyfp1tmotjaS9Cf
|
|
|
|
|
FWd67LuEru4TYtz12RNMDBF5ypcEfibvb3I8O6igOSQX/Jl5D2pMChesZxhmCift
|
|
|
|
|
Qp09T41MA8PmHf1G9oMG0A3ZnjKDG5ebaJNRFImJhMHsgh/TP7V3uZy7YHTgopKX
|
|
|
|
|
Hv63V3Uo3Oihav29Q7urwmf7Ly7X7J2WE86/w3vRHi5dhaWWqEqxmnAXl+H+sG4V
|
|
|
|
|
meeVRI332bg1Nuy8KnnX8v3ZeJzMBkAhzvSr6Ri96R0/Un/oEFwVC5jDTq8sXVn6
|
|
|
|
|
u7wlOSk+oFzDIO/UILIA
|
|
|
|
|
-----END CERTIFICATE-----`;
|
|
|
|
|
|
|
|
|
|
const certWithoutHeader = certWithHeader
|
|
|
|
|
.replace(/-----BEGIN CERTIFICATE-----/g, '')
|
|
|
|
|
.replace(/-----END CERTIFICATE-----/g, '')
|
|
|
|
|
.replace(/\s+/g, '');
|
|
|
|
|
|
|
|
|
|
it('should throw an error if SAML_CERT is not set', () => {
|
|
|
|
|
process.env.SAML_CERT;
|
|
|
|
|
expect(() => getCertificateContent(process.env.SAML_CERT)).toThrow(
|
|
|
|
|
'Invalid input: SAML_CERT must be a string.',
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should throw an error if SAML_CERT is empty', () => {
|
|
|
|
|
process.env.SAML_CERT = '';
|
|
|
|
|
expect(() => getCertificateContent(process.env.SAML_CERT)).toThrow(
|
|
|
|
|
'Invalid cert: SAML_CERT must be a valid file path or certificate string.',
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should load cert from an environment variable if it is a single-line string(with header)', () => {
|
|
|
|
|
process.env.SAML_CERT = certWithHeader;
|
|
|
|
|
|
|
|
|
|
const actual = getCertificateContent(process.env.SAML_CERT);
|
|
|
|
|
expect(actual).toBe(certWithHeader);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should load cert from an environment variable if it is a single-line string(with no header)', () => {
|
|
|
|
|
process.env.SAML_CERT = certWithoutHeader;
|
|
|
|
|
|
|
|
|
|
const actual = getCertificateContent(process.env.SAML_CERT);
|
|
|
|
|
expect(actual).toBe(certWithoutHeader);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should throw an error if SAML_CERT is a single-line string (with header, no newline characters)', () => {
|
|
|
|
|
process.env.SAML_CERT = certWithHeader.replace(/\n/g, '');
|
|
|
|
|
expect(() => getCertificateContent(process.env.SAML_CERT)).toThrow(
|
|
|
|
|
'Invalid cert: SAML_CERT must be a valid file path or certificate string.',
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should load cert from a relative file path if SAML_CERT is valid', () => {
|
|
|
|
|
process.env.SAML_CERT = 'test.pem';
|
|
|
|
|
const resolvedPath = '/absolute/path/to/test.pem';
|
|
|
|
|
|
|
|
|
|
path.isAbsolute.mockReturnValue(false);
|
|
|
|
|
path.join.mockReturnValue(resolvedPath);
|
|
|
|
|
path.normalize.mockReturnValue(resolvedPath);
|
|
|
|
|
|
|
|
|
|
fs.existsSync.mockReturnValue(true);
|
|
|
|
|
fs.statSync.mockReturnValue({ isFile: () => true });
|
|
|
|
|
fs.readFileSync.mockReturnValue(certWithHeader);
|
|
|
|
|
|
|
|
|
|
const actual = getCertificateContent(process.env.SAML_CERT);
|
|
|
|
|
expect(actual).toBe(certWithHeader);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should load cert from an absolute file path if SAML_CERT is valid', () => {
|
|
|
|
|
process.env.SAML_CERT = '/absolute/path/to/test.pem';
|
|
|
|
|
|
|
|
|
|
path.isAbsolute.mockReturnValue(true);
|
|
|
|
|
path.normalize.mockReturnValue(process.env.SAML_CERT);
|
|
|
|
|
|
|
|
|
|
fs.existsSync.mockReturnValue(true);
|
|
|
|
|
fs.statSync.mockReturnValue({ isFile: () => true });
|
|
|
|
|
fs.readFileSync.mockReturnValue(certWithHeader);
|
|
|
|
|
|
|
|
|
|
const actual = getCertificateContent(process.env.SAML_CERT);
|
|
|
|
|
expect(actual).toBe(certWithHeader);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should throw an error if the file does not exist', () => {
|
|
|
|
|
process.env.SAML_CERT = 'missing.pem';
|
|
|
|
|
const resolvedPath = '/absolute/path/to/missing.pem';
|
|
|
|
|
|
|
|
|
|
path.isAbsolute.mockReturnValue(false);
|
|
|
|
|
path.join.mockReturnValue(resolvedPath);
|
|
|
|
|
path.normalize.mockReturnValue(resolvedPath);
|
|
|
|
|
|
|
|
|
|
fs.existsSync.mockReturnValue(false);
|
|
|
|
|
|
|
|
|
|
expect(() => getCertificateContent(process.env.SAML_CERT)).toThrow(
|
|
|
|
|
'Invalid cert: SAML_CERT must be a valid file path or certificate string.',
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should throw an error if the file is not readable', () => {
|
|
|
|
|
process.env.SAML_CERT = 'unreadable.pem';
|
|
|
|
|
const resolvedPath = '/absolute/path/to/unreadable.pem';
|
|
|
|
|
|
|
|
|
|
path.isAbsolute.mockReturnValue(false);
|
|
|
|
|
path.join.mockReturnValue(resolvedPath);
|
|
|
|
|
path.normalize.mockReturnValue(resolvedPath);
|
|
|
|
|
|
|
|
|
|
fs.existsSync.mockReturnValue(true);
|
|
|
|
|
fs.statSync.mockReturnValue({ isFile: () => true });
|
|
|
|
|
fs.readFileSync.mockImplementation(() => {
|
|
|
|
|
throw new Error('Permission denied');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(() => getCertificateContent(process.env.SAML_CERT)).toThrow(
|
|
|
|
|
'Error reading certificate file: Permission denied',
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('setupSaml', () => {
|
|
|
|
|
// Helper to wrap the verify callback in a promise
|
|
|
|
|
const validate = (profile) =>
|
|
|
|
|
new Promise((resolve, reject) => {
|
|
|
|
|
verifyCallback(profile, (err, user, details) => {
|
|
|
|
|
if (err) {
|
|
|
|
|
reject(err);
|
|
|
|
|
} else {
|
|
|
|
|
resolve({ user, details });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const baseProfile = {
|
|
|
|
|
nameID: 'saml-1234',
|
|
|
|
|
email: 'test@example.com',
|
|
|
|
|
given_name: 'First',
|
|
|
|
|
family_name: 'Last',
|
|
|
|
|
name: 'My Full Name',
|
|
|
|
|
username: 'flast',
|
|
|
|
|
picture: 'https://example.com/avatar.png',
|
|
|
|
|
custom_name: 'custom',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
beforeEach(async () => {
|
|
|
|
|
jest.clearAllMocks();
|
|
|
|
|
|
2025-05-30 22:18:13 -04:00
|
|
|
// Configure mocks
|
|
|
|
|
const { findUser, createUser, updateUser } = require('~/models');
|
|
|
|
|
findUser.mockResolvedValue(null);
|
|
|
|
|
createUser.mockImplementation(async (userData) => ({
|
|
|
|
|
_id: 'mock-user-id',
|
|
|
|
|
...userData,
|
|
|
|
|
}));
|
|
|
|
|
updateUser.mockImplementation(async (id, userData) => ({
|
|
|
|
|
_id: id,
|
|
|
|
|
...userData,
|
|
|
|
|
}));
|
|
|
|
|
|
2025-05-30 00:00:58 +09:00
|
|
|
const cert = `
|
|
|
|
|
-----BEGIN CERTIFICATE-----
|
|
|
|
|
MIIDazCCAlOgAwIBAgIUKhXaFJGJJPx466rlwYORIsqCq7MwDQYJKoZIhvcNAQEL
|
|
|
|
|
BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
|
|
|
|
|
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yNTAzMDQwODUxNTJaFw0yNjAz
|
|
|
|
|
MDQwODUxNTJaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw
|
|
|
|
|
HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB
|
|
|
|
|
AQUAA4IBDwAwggEKAoIBAQCWP09NZg0xaRiLpNygCVgV3M+4RFW2S0c5X/fg/uFT
|
|
|
|
|
O5MfaVYzG5GxzhXzWRB8RtNPsxX/nlbPsoUroeHbz+SABkOsNEv6JuKRH4VXRH34
|
|
|
|
|
VzjazVkPAwj+N4WqsC/Wo4EGGpKIGeGi8Zed4yvMqoTyE3mrS19fY0nMHT62wUwS
|
|
|
|
|
GMm2pAQdAQePZ9WY7A5XOA1IoxW2Zh2Oxaf1p59epBkZDhoxSMu8GoSkvK27Km4A
|
|
|
|
|
4UXftzdg/wHNPrNirmcYouioHdmrOtYxPjrhUBQ74AmE1/QK45B6wEgirKH1A1AW
|
|
|
|
|
6C+ApLwpBMvy9+8Gbyvc8G18W3CjdEVKmAeWb9JUedSXAgMBAAGjUzBRMB0GA1Ud
|
|
|
|
|
DgQWBBRxpaqBx8VDLLc8IkHATujj8IOs6jAfBgNVHSMEGDAWgBRxpaqBx8VDLLc8
|
|
|
|
|
IkHATujj8IOs6jAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBc
|
|
|
|
|
Puk6i+yowwGccB3LhfxZ+Fz6s6/Lfx6bP/Hy4NYOxmx2/awGBgyfp1tmotjaS9Cf
|
|
|
|
|
FWd67LuEru4TYtz12RNMDBF5ypcEfibvb3I8O6igOSQX/Jl5D2pMChesZxhmCift
|
|
|
|
|
Qp09T41MA8PmHf1G9oMG0A3ZnjKDG5ebaJNRFImJhMHsgh/TP7V3uZy7YHTgopKX
|
|
|
|
|
Hv63V3Uo3Oihav29Q7urwmf7Ly7X7J2WE86/w3vRHi5dhaWWqEqxmnAXl+H+sG4V
|
|
|
|
|
meeVRI332bg1Nuy8KnnX8v3ZeJzMBkAhzvSr6Ri96R0/Un/oEFwVC5jDTq8sXVn6
|
|
|
|
|
u7wlOSk+oFzDIO/UILIA
|
|
|
|
|
-----END CERTIFICATE-----
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
// Reset environment variables
|
|
|
|
|
process.env.SAML_ENTRY_POINT = 'https://example.com/saml';
|
|
|
|
|
process.env.SAML_ISSUER = 'saml-issuer';
|
|
|
|
|
process.env.SAML_CERT = cert;
|
|
|
|
|
process.env.SAML_CALLBACK_URL = '/oauth/saml/callback';
|
|
|
|
|
delete process.env.SAML_EMAIL_CLAIM;
|
|
|
|
|
delete process.env.SAML_USERNAME_CLAIM;
|
|
|
|
|
delete process.env.SAML_GIVEN_NAME_CLAIM;
|
|
|
|
|
delete process.env.SAML_FAMILY_NAME_CLAIM;
|
|
|
|
|
delete process.env.SAML_PICTURE_CLAIM;
|
|
|
|
|
delete process.env.SAML_NAME_CLAIM;
|
|
|
|
|
|
|
|
|
|
// Simulate image download
|
|
|
|
|
const fakeBuffer = Buffer.from('fake image');
|
|
|
|
|
fetch.mockResolvedValue({
|
|
|
|
|
ok: true,
|
|
|
|
|
buffer: jest.fn().mockResolvedValue(fakeBuffer),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await setupSaml();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should create a new user with correct username when username claim exists', async () => {
|
|
|
|
|
const profile = { ...baseProfile };
|
|
|
|
|
const { user } = await validate(profile);
|
|
|
|
|
|
|
|
|
|
expect(user.username).toBe(profile.username);
|
2025-05-30 22:18:13 -04:00
|
|
|
expect(user.provider).toBe('saml');
|
|
|
|
|
expect(user.samlId).toBe(profile.nameID);
|
|
|
|
|
expect(user.email).toBe(profile.email);
|
|
|
|
|
expect(user.name).toBe(`${profile.given_name} ${profile.family_name}`);
|
2025-05-30 00:00:58 +09:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should use given_name as username when username claim is missing', async () => {
|
|
|
|
|
const profile = { ...baseProfile };
|
|
|
|
|
delete profile.username;
|
|
|
|
|
const expectUsername = profile.given_name;
|
|
|
|
|
|
|
|
|
|
const { user } = await validate(profile);
|
|
|
|
|
|
|
|
|
|
expect(user.username).toBe(expectUsername);
|
2025-05-30 22:18:13 -04:00
|
|
|
expect(user.provider).toBe('saml');
|
2025-05-30 00:00:58 +09:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should use email as username when username and given_name are missing', async () => {
|
|
|
|
|
const profile = { ...baseProfile };
|
|
|
|
|
delete profile.username;
|
|
|
|
|
delete profile.given_name;
|
|
|
|
|
const expectUsername = profile.email;
|
|
|
|
|
|
|
|
|
|
const { user } = await validate(profile);
|
|
|
|
|
|
|
|
|
|
expect(user.username).toBe(expectUsername);
|
2025-05-30 22:18:13 -04:00
|
|
|
expect(user.provider).toBe('saml');
|
2025-05-30 00:00:58 +09:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should override username with SAML_USERNAME_CLAIM when set', async () => {
|
|
|
|
|
process.env.SAML_USERNAME_CLAIM = 'nameID';
|
|
|
|
|
const profile = { ...baseProfile };
|
|
|
|
|
|
|
|
|
|
const { user } = await validate(profile);
|
|
|
|
|
|
|
|
|
|
expect(user.username).toBe(profile.nameID);
|
2025-05-30 22:18:13 -04:00
|
|
|
expect(user.provider).toBe('saml');
|
2025-05-30 00:00:58 +09:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should set the full name correctly when given_name and family_name exist', async () => {
|
|
|
|
|
const profile = { ...baseProfile };
|
|
|
|
|
const expectedFullName = `${profile.given_name} ${profile.family_name}`;
|
|
|
|
|
|
|
|
|
|
const { user } = await validate(profile);
|
|
|
|
|
|
|
|
|
|
expect(user.name).toBe(expectedFullName);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should set the full name correctly when given_name exist', async () => {
|
|
|
|
|
const profile = { ...baseProfile };
|
|
|
|
|
delete profile.family_name;
|
|
|
|
|
const expectedFullName = profile.given_name;
|
|
|
|
|
|
|
|
|
|
const { user } = await validate(profile);
|
|
|
|
|
|
|
|
|
|
expect(user.name).toBe(expectedFullName);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should set the full name correctly when family_name exist', async () => {
|
|
|
|
|
const profile = { ...baseProfile };
|
|
|
|
|
delete profile.given_name;
|
|
|
|
|
const expectedFullName = profile.family_name;
|
|
|
|
|
|
|
|
|
|
const { user } = await validate(profile);
|
|
|
|
|
|
|
|
|
|
expect(user.name).toBe(expectedFullName);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should set the full name correctly when username exist', async () => {
|
|
|
|
|
const profile = { ...baseProfile };
|
|
|
|
|
delete profile.family_name;
|
|
|
|
|
delete profile.given_name;
|
|
|
|
|
const expectedFullName = profile.username;
|
|
|
|
|
|
|
|
|
|
const { user } = await validate(profile);
|
|
|
|
|
|
|
|
|
|
expect(user.name).toBe(expectedFullName);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should set the full name correctly when email only exist', async () => {
|
|
|
|
|
const profile = { ...baseProfile };
|
|
|
|
|
delete profile.family_name;
|
|
|
|
|
delete profile.given_name;
|
|
|
|
|
delete profile.username;
|
|
|
|
|
const expectedFullName = profile.email;
|
|
|
|
|
|
|
|
|
|
const { user } = await validate(profile);
|
|
|
|
|
|
|
|
|
|
expect(user.name).toBe(expectedFullName);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should set the full name correctly with SAML_NAME_CLAIM when set', async () => {
|
|
|
|
|
process.env.SAML_NAME_CLAIM = 'custom_name';
|
|
|
|
|
const profile = { ...baseProfile };
|
|
|
|
|
const expectedFullName = profile.custom_name;
|
|
|
|
|
|
|
|
|
|
const { user } = await validate(profile);
|
|
|
|
|
|
|
|
|
|
expect(user.name).toBe(expectedFullName);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should update an existing user on login', async () => {
|
2025-08-11 18:49:34 -04:00
|
|
|
// Set up findUser to return an existing user with saml provider
|
2025-05-30 22:18:13 -04:00
|
|
|
const { findUser } = require('~/models');
|
2025-05-30 00:00:58 +09:00
|
|
|
const existingUser = {
|
2025-05-30 22:18:13 -04:00
|
|
|
_id: 'existing-user-id',
|
2025-08-11 18:49:34 -04:00
|
|
|
provider: 'saml',
|
2025-05-30 00:00:58 +09:00
|
|
|
email: baseProfile.email,
|
|
|
|
|
samlId: '',
|
2025-05-30 22:18:13 -04:00
|
|
|
username: 'oldusername',
|
|
|
|
|
name: 'Old Name',
|
2025-05-30 00:00:58 +09:00
|
|
|
};
|
2025-05-30 22:18:13 -04:00
|
|
|
findUser.mockResolvedValue(existingUser);
|
2025-05-30 00:00:58 +09:00
|
|
|
|
|
|
|
|
const profile = { ...baseProfile };
|
2025-05-30 22:18:13 -04:00
|
|
|
const { user } = await validate(profile);
|
2025-05-30 00:00:58 +09:00
|
|
|
|
2025-05-30 22:18:13 -04:00
|
|
|
expect(user.provider).toBe('saml');
|
|
|
|
|
expect(user.samlId).toBe(baseProfile.nameID);
|
|
|
|
|
expect(user.username).toBe(baseProfile.username);
|
|
|
|
|
expect(user.name).toBe(`${baseProfile.given_name} ${baseProfile.family_name}`);
|
|
|
|
|
expect(user.email).toBe(baseProfile.email);
|
2025-05-30 00:00:58 +09:00
|
|
|
});
|
|
|
|
|
|
2025-08-11 18:49:34 -04:00
|
|
|
it('should block login when email exists with different provider', async () => {
|
|
|
|
|
// Set up findUser to return a user with different provider
|
|
|
|
|
const { findUser } = require('~/models');
|
|
|
|
|
const existingUser = {
|
|
|
|
|
_id: 'existing-user-id',
|
|
|
|
|
provider: 'google',
|
|
|
|
|
email: baseProfile.email,
|
|
|
|
|
googleId: 'some-google-id',
|
|
|
|
|
username: 'existinguser',
|
|
|
|
|
name: 'Existing User',
|
|
|
|
|
};
|
|
|
|
|
findUser.mockResolvedValue(existingUser);
|
|
|
|
|
|
|
|
|
|
const profile = { ...baseProfile };
|
|
|
|
|
const result = await validate(profile);
|
|
|
|
|
|
|
|
|
|
expect(result.user).toBe(false);
|
|
|
|
|
expect(result.details.message).toBe(require('librechat-data-provider').ErrorTypes.AUTH_FAILED);
|
|
|
|
|
});
|
|
|
|
|
|
2025-05-30 00:00:58 +09:00
|
|
|
it('should attempt to download and save the avatar if picture is provided', async () => {
|
|
|
|
|
const profile = { ...baseProfile };
|
|
|
|
|
|
|
|
|
|
const { user } = await validate(profile);
|
|
|
|
|
|
|
|
|
|
expect(fetch).toHaveBeenCalled();
|
|
|
|
|
expect(user.avatar).toBe('/fake/path/to/avatar.png');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should not attempt to download avatar if picture is not provided', async () => {
|
|
|
|
|
const profile = { ...baseProfile };
|
|
|
|
|
delete profile.picture;
|
|
|
|
|
|
|
|
|
|
await validate(profile);
|
|
|
|
|
|
|
|
|
|
expect(fetch).not.toHaveBeenCalled();
|
|
|
|
|
});
|
🏢 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
|
|
|
|
|
|
|
|
it('should pass the found user to resolveAppConfigForUser', async () => {
|
|
|
|
|
const existingUser = {
|
|
|
|
|
_id: 'tenant-user-id',
|
|
|
|
|
provider: 'saml',
|
|
|
|
|
samlId: 'saml-1234',
|
|
|
|
|
email: 'test@example.com',
|
|
|
|
|
tenantId: 'tenant-c',
|
|
|
|
|
role: 'USER',
|
|
|
|
|
};
|
|
|
|
|
findUser.mockResolvedValue(existingUser);
|
|
|
|
|
|
|
|
|
|
const profile = { ...baseProfile };
|
|
|
|
|
await validate(profile);
|
|
|
|
|
|
|
|
|
|
expect(resolveAppConfigForUser).toHaveBeenCalledWith(getAppConfig, existingUser);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should use baseConfig for new SAML user without calling resolveAppConfigForUser', async () => {
|
|
|
|
|
const profile = { ...baseProfile };
|
|
|
|
|
await validate(profile);
|
|
|
|
|
|
|
|
|
|
expect(resolveAppConfigForUser).not.toHaveBeenCalled();
|
|
|
|
|
expect(getAppConfig).toHaveBeenCalledWith({ baseOnly: true });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should block login when tenant config restricts the domain', async () => {
|
|
|
|
|
const { isEmailDomainAllowed } = require('@librechat/api');
|
|
|
|
|
const existingUser = {
|
|
|
|
|
_id: 'tenant-blocked',
|
|
|
|
|
provider: 'saml',
|
|
|
|
|
samlId: 'saml-1234',
|
|
|
|
|
email: 'test@example.com',
|
|
|
|
|
tenantId: 'tenant-restrict',
|
|
|
|
|
role: 'USER',
|
|
|
|
|
};
|
|
|
|
|
findUser.mockResolvedValue(existingUser);
|
|
|
|
|
resolveAppConfigForUser.mockResolvedValue({
|
|
|
|
|
registration: { allowedDomains: ['other.com'] },
|
|
|
|
|
});
|
|
|
|
|
isEmailDomainAllowed.mockReturnValueOnce(true).mockReturnValueOnce(false);
|
|
|
|
|
|
|
|
|
|
const profile = { ...baseProfile };
|
|
|
|
|
const { user } = await validate(profile);
|
|
|
|
|
expect(user).toBe(false);
|
|
|
|
|
});
|
2025-05-30 00:00:58 +09:00
|
|
|
});
|