🔐 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:
Danny Avila 2026-01-11 14:46:23 -05:00 committed by GitHub
parent 9cb9f42f52
commit 0e9d42a60b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 878 additions and 298 deletions

View file

@ -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);
});