mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-21 21:50:49 +02:00
🔐 fix: Handle Multiple Email Addresses in LDAP Auth (#9729)
This commit is contained in:
parent
2489670f54
commit
a5195a57a4
2 changed files with 188 additions and 1 deletions
|
@ -109,7 +109,8 @@ const ldapLogin = new LdapStrategy(ldapOptions, async (userinfo, done) => {
|
||||||
const username =
|
const username =
|
||||||
(LDAP_USERNAME && userinfo[LDAP_USERNAME]) || userinfo.givenName || userinfo.mail;
|
(LDAP_USERNAME && userinfo[LDAP_USERNAME]) || userinfo.givenName || userinfo.mail;
|
||||||
|
|
||||||
const mail = (LDAP_EMAIL && userinfo[LDAP_EMAIL]) || userinfo.mail || username + '@ldap.local';
|
let mail = (LDAP_EMAIL && userinfo[LDAP_EMAIL]) || userinfo.mail || username + '@ldap.local';
|
||||||
|
mail = Array.isArray(mail) ? mail[0] : mail;
|
||||||
|
|
||||||
if (!userinfo.mail && !(LDAP_EMAIL && userinfo[LDAP_EMAIL])) {
|
if (!userinfo.mail && !(LDAP_EMAIL && userinfo[LDAP_EMAIL])) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
|
|
186
api/strategies/ldapStrategy.spec.js
Normal file
186
api/strategies/ldapStrategy.spec.js
Normal file
|
@ -0,0 +1,186 @@
|
||||||
|
// --- Mocks ---
|
||||||
|
jest.mock('@librechat/data-schemas', () => ({
|
||||||
|
logger: {
|
||||||
|
info: jest.fn(),
|
||||||
|
warn: jest.fn(),
|
||||||
|
debug: jest.fn(),
|
||||||
|
error: jest.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('@librechat/api', () => ({
|
||||||
|
// isEnabled used for TLS flags
|
||||||
|
isEnabled: jest.fn(() => false),
|
||||||
|
getBalanceConfig: jest.fn(() => ({ enabled: false })),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('~/models', () => ({
|
||||||
|
findUser: jest.fn(),
|
||||||
|
createUser: jest.fn(),
|
||||||
|
updateUser: jest.fn(),
|
||||||
|
countUsers: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('~/server/services/Config', () => ({
|
||||||
|
getAppConfig: jest.fn().mockResolvedValue({}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('~/server/services/domains', () => ({
|
||||||
|
isEmailDomainAllowed: jest.fn(() => true),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock passport-ldapauth to capture verify callback
|
||||||
|
let verifyCallback;
|
||||||
|
jest.mock('passport-ldapauth', () => {
|
||||||
|
return jest.fn().mockImplementation((options, verify) => {
|
||||||
|
verifyCallback = verify; // capture the strategy verify function
|
||||||
|
return { name: 'ldap', options, verify };
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const { ErrorTypes } = require('librechat-data-provider');
|
||||||
|
const { findUser, createUser, updateUser, countUsers } = require('~/models');
|
||||||
|
const { isEmailDomainAllowed } = require('~/server/services/domains');
|
||||||
|
|
||||||
|
// Helper to call the verify callback and wrap in a Promise for convenience
|
||||||
|
const callVerify = (userinfo) =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
verifyCallback(userinfo, (err, user, info) => {
|
||||||
|
if (err) return reject(err);
|
||||||
|
resolve({ user, info });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ldapStrategy', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
|
||||||
|
// minimal required env for ldapStrategy module to export
|
||||||
|
process.env.LDAP_URL = 'ldap://example.com';
|
||||||
|
process.env.LDAP_USER_SEARCH_BASE = 'ou=users,dc=example,dc=com';
|
||||||
|
|
||||||
|
// Unset optional envs to exercise defaults
|
||||||
|
delete process.env.LDAP_CA_CERT_PATH;
|
||||||
|
delete process.env.LDAP_FULL_NAME;
|
||||||
|
delete process.env.LDAP_ID;
|
||||||
|
delete process.env.LDAP_USERNAME;
|
||||||
|
delete process.env.LDAP_EMAIL;
|
||||||
|
delete process.env.LDAP_TLS_REJECT_UNAUTHORIZED;
|
||||||
|
delete process.env.LDAP_STARTTLS;
|
||||||
|
|
||||||
|
// Default model/domain mocks
|
||||||
|
findUser.mockReset().mockResolvedValue(null);
|
||||||
|
createUser.mockReset().mockResolvedValue('newUserId');
|
||||||
|
updateUser.mockReset().mockImplementation(async (id, user) => ({ _id: id, ...user }));
|
||||||
|
countUsers.mockReset().mockResolvedValue(0);
|
||||||
|
isEmailDomainAllowed.mockReset().mockReturnValue(true);
|
||||||
|
|
||||||
|
// Ensure requiring the strategy sets up the verify callback
|
||||||
|
jest.isolateModules(() => {
|
||||||
|
require('./ldapStrategy');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses the first email when LDAP returns multiple emails (array)', async () => {
|
||||||
|
const userinfo = {
|
||||||
|
uid: 'uid123',
|
||||||
|
givenName: 'Alice',
|
||||||
|
cn: 'Alice Doe',
|
||||||
|
mail: ['first@example.com', 'second@example.com'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const { user } = await callVerify(userinfo);
|
||||||
|
|
||||||
|
expect(user.email).toBe('first@example.com');
|
||||||
|
expect(createUser).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
provider: 'ldap',
|
||||||
|
ldapId: 'uid123',
|
||||||
|
username: 'Alice',
|
||||||
|
email: 'first@example.com',
|
||||||
|
emailVerified: true,
|
||||||
|
name: 'Alice Doe',
|
||||||
|
}),
|
||||||
|
expect.any(Object),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('blocks login if an existing user has a different provider', async () => {
|
||||||
|
findUser.mockResolvedValue({ _id: 'u1', email: 'first@example.com', provider: 'google' });
|
||||||
|
|
||||||
|
const userinfo = {
|
||||||
|
uid: 'uid123',
|
||||||
|
mail: 'first@example.com',
|
||||||
|
givenName: 'Alice',
|
||||||
|
cn: 'Alice Doe',
|
||||||
|
};
|
||||||
|
|
||||||
|
const { user, info } = await callVerify(userinfo);
|
||||||
|
|
||||||
|
expect(user).toBe(false);
|
||||||
|
expect(info).toEqual({ message: ErrorTypes.AUTH_FAILED });
|
||||||
|
expect(createUser).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates an existing ldap user with current LDAP info', async () => {
|
||||||
|
const existing = {
|
||||||
|
_id: 'u2',
|
||||||
|
provider: 'ldap',
|
||||||
|
email: 'old@example.com',
|
||||||
|
ldapId: 'uid123',
|
||||||
|
username: 'olduser',
|
||||||
|
name: 'Old Name',
|
||||||
|
};
|
||||||
|
findUser.mockResolvedValue(existing);
|
||||||
|
|
||||||
|
const userinfo = {
|
||||||
|
uid: 'uid123',
|
||||||
|
mail: 'new@example.com',
|
||||||
|
givenName: 'NewFirst',
|
||||||
|
cn: 'NewFirst NewLast',
|
||||||
|
};
|
||||||
|
|
||||||
|
const { user } = await callVerify(userinfo);
|
||||||
|
|
||||||
|
expect(createUser).not.toHaveBeenCalled();
|
||||||
|
expect(updateUser).toHaveBeenCalledWith(
|
||||||
|
'u2',
|
||||||
|
expect.objectContaining({
|
||||||
|
provider: 'ldap',
|
||||||
|
ldapId: 'uid123',
|
||||||
|
email: 'new@example.com',
|
||||||
|
username: 'NewFirst',
|
||||||
|
name: 'NewFirst NewLast',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(user.email).toBe('new@example.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to username@ldap.local when no email attributes are present', async () => {
|
||||||
|
const userinfo = {
|
||||||
|
uid: 'uid999',
|
||||||
|
givenName: 'John',
|
||||||
|
cn: 'John Doe',
|
||||||
|
// no mail and no custom LDAP_EMAIL
|
||||||
|
};
|
||||||
|
|
||||||
|
const { user } = await callVerify(userinfo);
|
||||||
|
|
||||||
|
expect(user.email).toBe('John@ldap.local');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('denies login if email domain is not allowed', async () => {
|
||||||
|
isEmailDomainAllowed.mockReturnValue(false);
|
||||||
|
|
||||||
|
const userinfo = {
|
||||||
|
uid: 'uid123',
|
||||||
|
mail: 'notallowed@blocked.com',
|
||||||
|
givenName: 'Alice',
|
||||||
|
cn: 'Alice Doe',
|
||||||
|
};
|
||||||
|
|
||||||
|
const { user, info } = await callVerify(userinfo);
|
||||||
|
expect(user).toBe(false);
|
||||||
|
expect(info).toEqual({ message: 'Email domain not allowed' });
|
||||||
|
});
|
||||||
|
});
|
Loading…
Add table
Add a link
Reference in a new issue