mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 17:00:15 +01:00
🔐 feat: Support Multiple Roles in OPENID_REQUIRED_ROLE (#9171)
* feat: support multiple roles in OPENID_REQUIRED_ROLE - Allow comma-separated roles in OPENID_REQUIRED_ROLE environment variable - User needs ANY of the specified roles to login (OR logic) - Maintain backward compatibility with single role configuration - Add comprehensive test coverage for multiple role scenarios * Add tests * Fix linter * Add missing closing brace * Add new line * Simplify tests * Refresh OpenID verify callback in tests * Fix OpenID spec and resolve linting errors * test: Add backward compatibility test for single required role in OpenID strategy --------- Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
parent
2153db2f5f
commit
d83826b604
2 changed files with 81 additions and 3 deletions
|
|
@ -371,6 +371,10 @@ async function setupOpenId() {
|
||||||
const fullName = getFullName(userinfo);
|
const fullName = getFullName(userinfo);
|
||||||
|
|
||||||
if (requiredRole) {
|
if (requiredRole) {
|
||||||
|
const requiredRoles = requiredRole
|
||||||
|
.split(',')
|
||||||
|
.map((role) => role.trim())
|
||||||
|
.filter(Boolean);
|
||||||
let decodedToken = '';
|
let decodedToken = '';
|
||||||
if (requiredRoleTokenKind === 'access') {
|
if (requiredRoleTokenKind === 'access') {
|
||||||
decodedToken = jwtDecode(tokenset.access_token);
|
decodedToken = jwtDecode(tokenset.access_token);
|
||||||
|
|
@ -393,9 +397,13 @@ async function setupOpenId() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!roles.includes(requiredRole)) {
|
if (!requiredRoles.some((role) => roles.includes(role))) {
|
||||||
|
const rolesList =
|
||||||
|
requiredRoles.length === 1
|
||||||
|
? `"${requiredRoles[0]}"`
|
||||||
|
: `one of: ${requiredRoles.map((r) => `"${r}"`).join(', ')}`;
|
||||||
return done(null, false, {
|
return done(null, false, {
|
||||||
message: `You must have the "${requiredRole}" role to log in.`,
|
message: `You must have ${rolesList} role to log in.`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -338,7 +338,25 @@ describe('setupOpenId', () => {
|
||||||
|
|
||||||
// Assert – verify that the strategy rejects login
|
// Assert – verify that the strategy rejects login
|
||||||
expect(user).toBe(false);
|
expect(user).toBe(false);
|
||||||
expect(details.message).toBe('You must have the "requiredRole" role to log in.');
|
expect(details.message).toBe('You must have "requiredRole" role to log in.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow login when single required role is present (backward compatibility)', async () => {
|
||||||
|
// Arrange – ensure single role configuration (as set in beforeEach)
|
||||||
|
// OPENID_REQUIRED_ROLE = 'requiredRole'
|
||||||
|
// Default jwtDecode mock in beforeEach already returns this role
|
||||||
|
jwtDecode.mockReturnValue({
|
||||||
|
roles: ['requiredRole', 'anotherRole'],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const { user } = await validate(tokenset);
|
||||||
|
|
||||||
|
// Assert – verify that login succeeds with single role configuration
|
||||||
|
expect(user).toBeTruthy();
|
||||||
|
expect(user.email).toBe(tokenset.claims().email);
|
||||||
|
expect(user.username).toBe(tokenset.claims().preferred_username);
|
||||||
|
expect(createUser).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should attempt to download and save the avatar if picture is provided', async () => {
|
it('should attempt to download and save the avatar if picture is provided', async () => {
|
||||||
|
|
@ -364,6 +382,58 @@ describe('setupOpenId', () => {
|
||||||
// Depending on your implementation, user.avatar may be undefined or an empty string.
|
// Depending on your implementation, user.avatar may be undefined or an empty string.
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should support comma-separated multiple roles', async () => {
|
||||||
|
// Arrange
|
||||||
|
process.env.OPENID_REQUIRED_ROLE = 'someRole,anotherRole,admin';
|
||||||
|
await setupOpenId(); // Re-initialize the strategy
|
||||||
|
verifyCallback = require('openid-client/passport').__getVerifyCallback();
|
||||||
|
jwtDecode.mockReturnValue({
|
||||||
|
roles: ['anotherRole', 'aThirdRole'],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const { user } = await validate(tokenset);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(user).toBeTruthy();
|
||||||
|
expect(user.email).toBe(tokenset.claims().email);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject login when user has none of the required multiple roles', async () => {
|
||||||
|
// Arrange
|
||||||
|
process.env.OPENID_REQUIRED_ROLE = 'someRole,anotherRole,admin';
|
||||||
|
await setupOpenId(); // Re-initialize the strategy
|
||||||
|
verifyCallback = require('openid-client/passport').__getVerifyCallback();
|
||||||
|
jwtDecode.mockReturnValue({
|
||||||
|
roles: ['aThirdRole', 'aFourthRole'],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const { user, details } = await validate(tokenset);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(user).toBe(false);
|
||||||
|
expect(details.message).toBe(
|
||||||
|
'You must have one of: "someRole", "anotherRole", "admin" role to log in.',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle spaces in comma-separated roles', async () => {
|
||||||
|
// Arrange
|
||||||
|
process.env.OPENID_REQUIRED_ROLE = ' someRole , anotherRole , admin ';
|
||||||
|
await setupOpenId(); // Re-initialize the strategy
|
||||||
|
verifyCallback = require('openid-client/passport').__getVerifyCallback();
|
||||||
|
jwtDecode.mockReturnValue({
|
||||||
|
roles: ['someRole'],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const { user } = await validate(tokenset);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(user).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
it('should default to usePKCE false when OPENID_USE_PKCE is not defined', async () => {
|
it('should default to usePKCE false when OPENID_USE_PKCE is not defined', async () => {
|
||||||
const OpenIDStrategy = require('openid-client/passport').Strategy;
|
const OpenIDStrategy = require('openid-client/passport').Strategy;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue