mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 00:40:14 +01:00
🔐 feat: Enhance OpenID User Info Handling (#4561)
* oidc-changes Initial attempt at testing openidStrategy and adding OPENID_USERNAME_CLAIM setting * oidc-changes Add OPENID_NAME_CLAIM * oidc-changes cleanup oidc test code * oidc-changes using mongo memory server for test * oidc-changes Change tests to expect username all lowercase * oidc-changes Add more tests * chore: linting * refactor: Simplify OpenID full name retrieval logic * refactor: Simplify OpenID user info retrieval logic * refactor: move helper to openidStrategy.js --------- Co-authored-by: alihacks <alihacks@pm.me>
This commit is contained in:
parent
600d21780b
commit
a1647d76e0
3 changed files with 219 additions and 13 deletions
175
api/strategies/openidStrategy.spec.js
Normal file
175
api/strategies/openidStrategy.spec.js
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
const jwtDecode = require('jsonwebtoken/decode');
|
||||
const { Issuer, Strategy: OpenIDStrategy } = require('openid-client');
|
||||
const mongoose = require('mongoose');
|
||||
const { MongoMemoryServer } = require('mongodb-memory-server');
|
||||
const User = require('~/models/User');
|
||||
const setupOpenId = require('./openidStrategy');
|
||||
|
||||
jest.mock('jsonwebtoken/decode');
|
||||
jest.mock('openid-client');
|
||||
|
||||
jest.mock('~/server/services/Files/strategies', () => ({
|
||||
getStrategyFunctions: jest.fn(() => ({
|
||||
saveBuffer: jest.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
Issuer.discover = jest.fn().mockResolvedValue({
|
||||
Client: jest.fn(),
|
||||
});
|
||||
|
||||
describe('setupOpenId', () => {
|
||||
const OLD_ENV = process.env;
|
||||
describe('OpenIDStrategy', () => {
|
||||
let validateFn, mongoServer;
|
||||
|
||||
beforeAll(async () => {
|
||||
mongoServer = await MongoMemoryServer.create();
|
||||
const mongoUri = mongoServer.getUri();
|
||||
await mongoose.connect(mongoUri);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
process.env = OLD_ENV;
|
||||
await mongoose.disconnect();
|
||||
await mongoServer.stop();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
await User.deleteMany({});
|
||||
process.env = {
|
||||
...OLD_ENV,
|
||||
OPENID_ISSUER: 'https://fake-issuer.com',
|
||||
OPENID_CLIENT_ID: 'fake_client_id',
|
||||
OPENID_CLIENT_SECRET: 'fake_client_secret',
|
||||
DOMAIN_SERVER: 'https://example.com',
|
||||
OPENID_CALLBACK_URL: '/callback',
|
||||
OPENID_SCOPE: 'openid profile email',
|
||||
OPENID_REQUIRED_ROLE: 'requiredRole',
|
||||
OPENID_REQUIRED_ROLE_PARAMETER_PATH: 'roles',
|
||||
OPENID_REQUIRED_ROLE_TOKEN_KIND: 'id',
|
||||
};
|
||||
|
||||
jwtDecode.mockReturnValue({
|
||||
roles: ['requiredRole'],
|
||||
});
|
||||
|
||||
//call setup so we can grab a reference to the validate function
|
||||
await setupOpenId();
|
||||
validateFn = OpenIDStrategy.mock.calls[0][1];
|
||||
});
|
||||
|
||||
const tokenset = {
|
||||
id_token: 'fake_id_token',
|
||||
};
|
||||
|
||||
const userinfo = {
|
||||
sub: '1234',
|
||||
email: 'test@example.com',
|
||||
email_verified: true,
|
||||
given_name: 'First',
|
||||
family_name: 'Last',
|
||||
name: 'My Full',
|
||||
username: 'flast',
|
||||
};
|
||||
|
||||
const userModel = {
|
||||
openidId: userinfo.sub,
|
||||
email: userinfo.email,
|
||||
};
|
||||
|
||||
it('should set username correctly for a new user when username claim exists', async () => {
|
||||
const expectUsername = userinfo.username.toLowerCase();
|
||||
await validateFn(tokenset, userinfo, (err, user) => {
|
||||
expect(err).toBe(null);
|
||||
expect(user.username).toBe(expectUsername);
|
||||
});
|
||||
|
||||
await expect(User.exists({ username: expectUsername })).resolves.not.toBeNull();
|
||||
});
|
||||
|
||||
it('should set username correctly for a new user when given_name claim exists, but username does not', async () => {
|
||||
let userinfo_modified = { ...userinfo };
|
||||
delete userinfo_modified.username;
|
||||
const expectUsername = userinfo.given_name.toLowerCase();
|
||||
|
||||
await validateFn(tokenset, userinfo_modified, (err, user) => {
|
||||
expect(err).toBe(null);
|
||||
expect(user.username).toBe(expectUsername);
|
||||
});
|
||||
await expect(User.exists({ username: expectUsername })).resolves.not.toBeNull();
|
||||
});
|
||||
|
||||
it('should set username correctly for a new user when email claim exists, but username and given_name do not', async () => {
|
||||
let userinfo_modified = { ...userinfo };
|
||||
delete userinfo_modified.username;
|
||||
delete userinfo_modified.given_name;
|
||||
const expectUsername = userinfo.email.toLowerCase();
|
||||
|
||||
await validateFn(tokenset, userinfo_modified, (err, user) => {
|
||||
expect(err).toBe(null);
|
||||
expect(user.username).toBe(expectUsername);
|
||||
});
|
||||
await expect(User.exists({ username: expectUsername })).resolves.not.toBeNull();
|
||||
});
|
||||
|
||||
it('should set username correctly for a new user when using OPENID_USERNAME_CLAIM', async () => {
|
||||
process.env.OPENID_USERNAME_CLAIM = 'sub';
|
||||
const expectUsername = userinfo.sub.toLowerCase();
|
||||
|
||||
await validateFn(tokenset, userinfo, (err, user) => {
|
||||
expect(err).toBe(null);
|
||||
expect(user.username).toBe(expectUsername);
|
||||
});
|
||||
await expect(User.exists({ username: expectUsername })).resolves.not.toBeNull();
|
||||
});
|
||||
|
||||
it('should set name correctly for a new user with first and last names', async () => {
|
||||
const expectName = userinfo.given_name + ' ' + userinfo.family_name;
|
||||
await validateFn(tokenset, userinfo, (err, user) => {
|
||||
expect(err).toBe(null);
|
||||
expect(user.name).toBe(expectName);
|
||||
});
|
||||
await expect(User.exists({ name: expectName })).resolves.not.toBeNull();
|
||||
});
|
||||
|
||||
it('should set name correctly for a new user using OPENID_NAME_CLAIM', async () => {
|
||||
const expectName = 'Custom Name';
|
||||
process.env.OPENID_NAME_CLAIM = 'name';
|
||||
let userinfo_modified = { ...userinfo, name: expectName };
|
||||
|
||||
await validateFn(tokenset, userinfo_modified, (err, user) => {
|
||||
expect(err).toBe(null);
|
||||
expect(user.name).toBe(expectName);
|
||||
});
|
||||
await expect(User.exists({ name: expectName })).resolves.not.toBeNull();
|
||||
});
|
||||
|
||||
it('should should update existing user after login', async () => {
|
||||
const expectUsername = userinfo.username.toLowerCase();
|
||||
await User.create(userModel);
|
||||
|
||||
await validateFn(tokenset, userinfo, (err) => {
|
||||
expect(err).toBe(null);
|
||||
});
|
||||
const newUser = await User.findOne({ openidId: userModel.openidId });
|
||||
await expect(newUser.provider).toBe('openid');
|
||||
await expect(newUser.username).toBe(expectUsername);
|
||||
await expect(newUser.name).toBe(userinfo.given_name + ' ' + userinfo.family_name);
|
||||
});
|
||||
|
||||
it('should should enforce required role', async () => {
|
||||
jwtDecode.mockReturnValue({
|
||||
roles: ['SomeOtherRole'],
|
||||
});
|
||||
await validateFn(tokenset, userinfo, (err, user, details) => {
|
||||
expect(err).toBe(null);
|
||||
expect(user).toBe(false);
|
||||
expect(details.message).toBe(
|
||||
'You must have the "' + process.env.OPENID_REQUIRED_ROLE + '" role to log in.',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue