mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-03-02 06:10:18 +01:00
🔐 feat: Admin Auth. Routes with Secure Cross-Origin Token Exchange (#11297)
* feat: implement admin authentication with OpenID & Local Auth proxy support * feat: implement admin OAuth exchange flow with caching support - Added caching for admin OAuth exchange codes with a short TTL. - Introduced new endpoints for generating and exchanging admin OAuth codes. - Updated relevant controllers and routes to handle admin panel redirects and token exchanges. - Enhanced logging for better traceability of OAuth operations. * refactor: enhance OpenID strategy mock to support multiple verify callbacks - Updated the OpenID strategy mock to store and retrieve verify callbacks by strategy name. - Improved backward compatibility by maintaining a method to get the last registered callback. - Adjusted tests to utilize the new callback retrieval methods, ensuring clarity in the verification process for the 'openid' strategy. * refactor: reorder import statements for better organization * refactor: admin OAuth flow with improved URL handling and validation - Added a utility function to retrieve the admin panel URL, defaulting to a local development URL if not set in the environment. - Updated the OAuth exchange endpoint to include validation for the authorization code format. - Refactored the admin panel redirect logic to handle URL parsing more robustly, ensuring accurate origin comparisons. - Removed redundant local URL definitions from the codebase for better maintainability. * refactor: remove deprecated requireAdmin middleware and migrate to TypeScript - Deleted the old requireAdmin middleware file and its references in the middleware index. - Introduced a new TypeScript version of the requireAdmin middleware with enhanced error handling and logging. - Updated routes to utilize the new requireAdmin middleware, ensuring consistent access control for admin routes. * feat: add requireAdmin middleware for admin role verification - Introduced requireAdmin middleware to enforce admin role checks for authenticated users. - Implemented comprehensive error handling and logging for unauthorized access attempts. - Added unit tests to validate middleware functionality and ensure proper behavior for different user roles. - Updated middleware index to include the new requireAdmin export.
This commit is contained in:
parent
9cb9f42f52
commit
0e9d42a60b
15 changed files with 878 additions and 298 deletions
|
|
@ -64,21 +64,36 @@ jest.mock('openid-client', () => {
|
|||
});
|
||||
|
||||
jest.mock('openid-client/passport', () => {
|
||||
let verifyCallback;
|
||||
/** Store callbacks by strategy name - 'openid' and 'openidAdmin' */
|
||||
const verifyCallbacks = {};
|
||||
let lastVerifyCallback;
|
||||
|
||||
const mockStrategy = jest.fn((options, verify) => {
|
||||
verifyCallback = verify;
|
||||
lastVerifyCallback = verify;
|
||||
return { name: 'openid', options, verify };
|
||||
});
|
||||
|
||||
return {
|
||||
Strategy: mockStrategy,
|
||||
__getVerifyCallback: () => verifyCallback,
|
||||
/** Get the last registered callback (for backward compatibility) */
|
||||
__getVerifyCallback: () => lastVerifyCallback,
|
||||
/** Store callback by name when passport.use is called */
|
||||
__setVerifyCallback: (name, callback) => {
|
||||
verifyCallbacks[name] = callback;
|
||||
},
|
||||
/** Get callback by strategy name */
|
||||
__getVerifyCallbackByName: (name) => verifyCallbacks[name],
|
||||
};
|
||||
});
|
||||
|
||||
// Mock passport
|
||||
// Mock passport - capture strategy name and callback
|
||||
jest.mock('passport', () => ({
|
||||
use: jest.fn(),
|
||||
use: jest.fn((name, strategy) => {
|
||||
const passportMock = require('openid-client/passport');
|
||||
if (strategy && strategy.verify) {
|
||||
passportMock.__setVerifyCallback(name, strategy.verify);
|
||||
}
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('setupOpenId', () => {
|
||||
|
|
@ -159,9 +174,10 @@ describe('setupOpenId', () => {
|
|||
};
|
||||
fetch.mockResolvedValue(fakeResponse);
|
||||
|
||||
// Call the setup function and capture the verify callback
|
||||
// Call the setup function and capture the verify callback for the regular 'openid' strategy
|
||||
// (not 'openidAdmin' which requires existing users)
|
||||
await setupOpenId();
|
||||
verifyCallback = require('openid-client/passport').__getVerifyCallback();
|
||||
verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
|
||||
});
|
||||
|
||||
it('should create a new user with correct username when preferred_username claim exists', async () => {
|
||||
|
|
@ -389,7 +405,7 @@ describe('setupOpenId', () => {
|
|||
// Arrange
|
||||
process.env.OPENID_REQUIRED_ROLE = 'someRole,anotherRole,admin';
|
||||
await setupOpenId(); // Re-initialize the strategy
|
||||
verifyCallback = require('openid-client/passport').__getVerifyCallback();
|
||||
verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
|
||||
jwtDecode.mockReturnValue({
|
||||
roles: ['anotherRole', 'aThirdRole'],
|
||||
});
|
||||
|
|
@ -406,7 +422,7 @@ describe('setupOpenId', () => {
|
|||
// Arrange
|
||||
process.env.OPENID_REQUIRED_ROLE = 'someRole,anotherRole,admin';
|
||||
await setupOpenId(); // Re-initialize the strategy
|
||||
verifyCallback = require('openid-client/passport').__getVerifyCallback();
|
||||
verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
|
||||
jwtDecode.mockReturnValue({
|
||||
roles: ['aThirdRole', 'aFourthRole'],
|
||||
});
|
||||
|
|
@ -425,7 +441,7 @@ describe('setupOpenId', () => {
|
|||
// Arrange
|
||||
process.env.OPENID_REQUIRED_ROLE = ' someRole , anotherRole , admin ';
|
||||
await setupOpenId(); // Re-initialize the strategy
|
||||
verifyCallback = require('openid-client/passport').__getVerifyCallback();
|
||||
verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
|
||||
jwtDecode.mockReturnValue({
|
||||
roles: ['someRole'],
|
||||
});
|
||||
|
|
@ -560,7 +576,7 @@ describe('setupOpenId', () => {
|
|||
delete process.env.OPENID_ADMIN_ROLE_TOKEN_KIND;
|
||||
|
||||
await setupOpenId();
|
||||
verifyCallback = require('openid-client/passport').__getVerifyCallback();
|
||||
verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
|
||||
|
||||
// Simulate an existing admin user
|
||||
const existingAdminUser = {
|
||||
|
|
@ -611,7 +627,7 @@ describe('setupOpenId', () => {
|
|||
});
|
||||
|
||||
await setupOpenId();
|
||||
verifyCallback = require('openid-client/passport').__getVerifyCallback();
|
||||
verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
|
||||
|
||||
const { user } = await validate(tokenset);
|
||||
|
||||
|
|
@ -634,7 +650,7 @@ describe('setupOpenId', () => {
|
|||
});
|
||||
|
||||
await setupOpenId();
|
||||
verifyCallback = require('openid-client/passport').__getVerifyCallback();
|
||||
verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
|
||||
|
||||
const { user } = await validate(tokenset);
|
||||
|
||||
|
|
@ -655,14 +671,12 @@ describe('setupOpenId', () => {
|
|||
});
|
||||
|
||||
await setupOpenId();
|
||||
verifyCallback = require('openid-client/passport').__getVerifyCallback();
|
||||
verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
|
||||
|
||||
const { user, details } = await validate(tokenset);
|
||||
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
"Key 'resource_access.nonexistent.roles' not found or invalid type in id token!",
|
||||
),
|
||||
expect.stringContaining("Key 'resource_access.nonexistent.roles' not found in id token!"),
|
||||
);
|
||||
expect(user).toBe(false);
|
||||
expect(details.message).toContain('role to log in');
|
||||
|
|
@ -680,12 +694,12 @@ describe('setupOpenId', () => {
|
|||
});
|
||||
|
||||
await setupOpenId();
|
||||
verifyCallback = require('openid-client/passport').__getVerifyCallback();
|
||||
verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
|
||||
|
||||
const { user } = await validate(tokenset);
|
||||
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Key 'org.team.roles' not found or invalid type in id token!"),
|
||||
expect.stringContaining("Key 'org.team.roles' not found in id token!"),
|
||||
);
|
||||
expect(user).toBe(false);
|
||||
});
|
||||
|
|
@ -709,7 +723,7 @@ describe('setupOpenId', () => {
|
|||
});
|
||||
|
||||
await setupOpenId();
|
||||
verifyCallback = require('openid-client/passport').__getVerifyCallback();
|
||||
verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
|
||||
|
||||
const { user } = await validate(tokenset);
|
||||
|
||||
|
|
@ -739,7 +753,7 @@ describe('setupOpenId', () => {
|
|||
});
|
||||
|
||||
await setupOpenId();
|
||||
verifyCallback = require('openid-client/passport').__getVerifyCallback();
|
||||
verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
|
||||
|
||||
const { user } = await validate({
|
||||
...tokenset,
|
||||
|
|
@ -759,7 +773,7 @@ describe('setupOpenId', () => {
|
|||
});
|
||||
|
||||
await setupOpenId();
|
||||
verifyCallback = require('openid-client/passport').__getVerifyCallback();
|
||||
verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
|
||||
|
||||
const { user } = await validate(tokenset);
|
||||
|
||||
|
|
@ -776,7 +790,7 @@ describe('setupOpenId', () => {
|
|||
});
|
||||
|
||||
await setupOpenId();
|
||||
verifyCallback = require('openid-client/passport').__getVerifyCallback();
|
||||
verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
|
||||
|
||||
const { user } = await validate(tokenset);
|
||||
|
||||
|
|
@ -793,7 +807,7 @@ describe('setupOpenId', () => {
|
|||
});
|
||||
|
||||
await setupOpenId();
|
||||
verifyCallback = require('openid-client/passport').__getVerifyCallback();
|
||||
verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
|
||||
|
||||
const { user } = await validate(tokenset);
|
||||
|
||||
|
|
@ -810,7 +824,7 @@ describe('setupOpenId', () => {
|
|||
});
|
||||
|
||||
await setupOpenId();
|
||||
verifyCallback = require('openid-client/passport').__getVerifyCallback();
|
||||
verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
|
||||
|
||||
const { user } = await validate(tokenset);
|
||||
|
||||
|
|
@ -827,7 +841,7 @@ describe('setupOpenId', () => {
|
|||
});
|
||||
|
||||
await setupOpenId();
|
||||
verifyCallback = require('openid-client/passport').__getVerifyCallback();
|
||||
verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
|
||||
|
||||
const { user } = await validate(tokenset);
|
||||
|
||||
|
|
@ -847,7 +861,7 @@ describe('setupOpenId', () => {
|
|||
});
|
||||
|
||||
await setupOpenId();
|
||||
verifyCallback = require('openid-client/passport').__getVerifyCallback();
|
||||
verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
|
||||
|
||||
const { user } = await validate(tokenset);
|
||||
|
||||
|
|
@ -864,12 +878,12 @@ describe('setupOpenId', () => {
|
|||
});
|
||||
|
||||
await setupOpenId();
|
||||
verifyCallback = require('openid-client/passport').__getVerifyCallback();
|
||||
verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
|
||||
|
||||
const { user } = await validate(tokenset);
|
||||
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Key 'access.roles' not found or invalid type in id token!"),
|
||||
expect.stringContaining("Key 'access.roles' not found in id token!"),
|
||||
);
|
||||
expect(user).toBe(false);
|
||||
});
|
||||
|
|
@ -884,12 +898,12 @@ describe('setupOpenId', () => {
|
|||
});
|
||||
|
||||
await setupOpenId();
|
||||
verifyCallback = require('openid-client/passport').__getVerifyCallback();
|
||||
verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
|
||||
|
||||
const { user } = await validate(tokenset);
|
||||
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Key 'data.roles' not found or invalid type in id token!"),
|
||||
expect.stringContaining("Key 'data.roles' not found in id token!"),
|
||||
);
|
||||
expect(user).toBe(false);
|
||||
});
|
||||
|
|
@ -906,7 +920,7 @@ describe('setupOpenId', () => {
|
|||
});
|
||||
|
||||
await setupOpenId();
|
||||
verifyCallback = require('openid-client/passport').__getVerifyCallback();
|
||||
verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
|
||||
|
||||
await expect(validate(tokenset)).rejects.toThrow('Invalid admin role token kind');
|
||||
|
||||
|
|
@ -927,12 +941,12 @@ describe('setupOpenId', () => {
|
|||
});
|
||||
|
||||
await setupOpenId();
|
||||
verifyCallback = require('openid-client/passport').__getVerifyCallback();
|
||||
verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
|
||||
|
||||
const { user, details } = await validate(tokenset);
|
||||
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Key 'roles' not found or invalid type in id token!"),
|
||||
expect.stringContaining("Key 'roles' not found in id token!"),
|
||||
);
|
||||
expect(user).toBe(false);
|
||||
expect(details.message).toContain('role to log in');
|
||||
|
|
@ -948,12 +962,12 @@ describe('setupOpenId', () => {
|
|||
});
|
||||
|
||||
await setupOpenId();
|
||||
verifyCallback = require('openid-client/passport').__getVerifyCallback();
|
||||
verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
|
||||
|
||||
const { user } = await validate(tokenset);
|
||||
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Key 'roleCount' not found or invalid type in id token!"),
|
||||
expect.stringContaining("Key 'roleCount' not found in id token!"),
|
||||
);
|
||||
expect(user).toBe(false);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue