| | const fs = require('fs'); |
| | const LdapStrategy = require('passport-ldapauth'); |
| | const { logger } = require('@librechat/data-schemas'); |
| | const { SystemRoles, ErrorTypes } = require('librechat-data-provider'); |
| | const { isEnabled, getBalanceConfig, isEmailDomainAllowed } = require('@librechat/api'); |
| | const { createUser, findUser, updateUser, countUsers } = require('~/models'); |
| | const { getAppConfig } = require('~/server/services/Config'); |
| |
|
| | const { |
| | LDAP_URL, |
| | LDAP_BIND_DN, |
| | LDAP_BIND_CREDENTIALS, |
| | LDAP_USER_SEARCH_BASE, |
| | LDAP_SEARCH_FILTER, |
| | LDAP_CA_CERT_PATH, |
| | LDAP_FULL_NAME, |
| | LDAP_ID, |
| | LDAP_USERNAME, |
| | LDAP_EMAIL, |
| | LDAP_TLS_REJECT_UNAUTHORIZED, |
| | LDAP_STARTTLS, |
| | } = process.env; |
| |
|
| | |
| | if (!LDAP_URL || !LDAP_USER_SEARCH_BASE) { |
| | module.exports = null; |
| | } |
| |
|
| | const searchAttributes = [ |
| | 'displayName', |
| | 'mail', |
| | 'uid', |
| | 'cn', |
| | 'name', |
| | 'commonname', |
| | 'givenName', |
| | 'sn', |
| | 'sAMAccountName', |
| | ]; |
| |
|
| | if (LDAP_FULL_NAME) { |
| | searchAttributes.push(...LDAP_FULL_NAME.split(',')); |
| | } |
| | if (LDAP_ID) { |
| | searchAttributes.push(LDAP_ID); |
| | } |
| | if (LDAP_USERNAME) { |
| | searchAttributes.push(LDAP_USERNAME); |
| | } |
| | if (LDAP_EMAIL) { |
| | searchAttributes.push(LDAP_EMAIL); |
| | } |
| | const rejectUnauthorized = isEnabled(LDAP_TLS_REJECT_UNAUTHORIZED); |
| | const startTLS = isEnabled(LDAP_STARTTLS); |
| |
|
| | const ldapOptions = { |
| | server: { |
| | url: LDAP_URL, |
| | bindDN: LDAP_BIND_DN, |
| | bindCredentials: LDAP_BIND_CREDENTIALS, |
| | searchBase: LDAP_USER_SEARCH_BASE, |
| | searchFilter: LDAP_SEARCH_FILTER || 'mail={{username}}', |
| | searchAttributes: [...new Set(searchAttributes)], |
| | ...(LDAP_CA_CERT_PATH && { |
| | tlsOptions: { |
| | rejectUnauthorized, |
| | ca: (() => { |
| | try { |
| | return [fs.readFileSync(LDAP_CA_CERT_PATH)]; |
| | } catch (err) { |
| | logger.error('[ldapStrategy]', 'Failed to read CA certificate', err); |
| | throw err; |
| | } |
| | })(), |
| | }, |
| | }), |
| | ...(startTLS && { starttls: true }), |
| | }, |
| | usernameField: 'email', |
| | passwordField: 'password', |
| | }; |
| |
|
| | const ldapLogin = new LdapStrategy(ldapOptions, async (userinfo, done) => { |
| | if (!userinfo) { |
| | return done(null, false, { message: 'Invalid credentials' }); |
| | } |
| |
|
| | try { |
| | const ldapId = |
| | (LDAP_ID && userinfo[LDAP_ID]) || userinfo.uid || userinfo.sAMAccountName || userinfo.mail; |
| |
|
| | let user = await findUser({ ldapId }); |
| | if (user && user.provider !== 'ldap') { |
| | logger.info( |
| | `[ldapStrategy] User ${user.email} already exists with provider ${user.provider}`, |
| | ); |
| | return done(null, false, { |
| | message: ErrorTypes.AUTH_FAILED, |
| | }); |
| | } |
| |
|
| | const fullNameAttributes = LDAP_FULL_NAME && LDAP_FULL_NAME.split(','); |
| | const fullName = |
| | fullNameAttributes && fullNameAttributes.length > 0 |
| | ? fullNameAttributes.map((attr) => userinfo[attr]).join(' ') |
| | : userinfo.cn || userinfo.name || userinfo.commonname || userinfo.displayName; |
| |
|
| | const username = |
| | (LDAP_USERNAME && userinfo[LDAP_USERNAME]) || userinfo.givenName || userinfo.mail; |
| |
|
| | 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])) { |
| | logger.warn( |
| | '[ldapStrategy]', |
| | `No valid email attribute found in LDAP userinfo. Using fallback email: ${username}@ldap.local`, |
| | `LDAP_EMAIL env var: ${LDAP_EMAIL || 'not set'}`, |
| | `Available userinfo attributes: ${Object.keys(userinfo).join(', ')}`, |
| | 'Full userinfo:', |
| | JSON.stringify(userinfo, null, 2), |
| | ); |
| | } |
| |
|
| | const appConfig = await getAppConfig(); |
| | if (!isEmailDomainAllowed(mail, appConfig?.registration?.allowedDomains)) { |
| | logger.error( |
| | `[LDAP Strategy] Authentication blocked - email domain not allowed [Email: ${mail}]`, |
| | ); |
| | return done(null, false, { message: 'Email domain not allowed' }); |
| | } |
| |
|
| | if (!user) { |
| | const isFirstRegisteredUser = (await countUsers()) === 0; |
| | const role = isFirstRegisteredUser ? SystemRoles.ADMIN : SystemRoles.USER; |
| |
|
| | user = { |
| | provider: 'ldap', |
| | ldapId, |
| | username, |
| | email: mail, |
| | emailVerified: true, |
| | name: fullName, |
| | role, |
| | }; |
| | const balanceConfig = getBalanceConfig(appConfig); |
| | const userId = await createUser(user, balanceConfig); |
| | user._id = userId; |
| | } else { |
| | |
| | |
| | user.provider = 'ldap'; |
| | user.ldapId = ldapId; |
| | user.email = mail; |
| | user.username = username; |
| | user.name = fullName; |
| | } |
| |
|
| | user = await updateUser(user._id, user); |
| | done(null, user); |
| | } catch (err) { |
| | logger.error('[ldapStrategy]', err); |
| | done(err); |
| | } |
| | }); |
| |
|
| | module.exports = ldapLogin; |
| |
|