diff --git a/.env.example b/.env.example index 9eb117ae6..38652f6f9 100644 --- a/.env.example +++ b/.env.example @@ -372,6 +372,9 @@ LDAP_BIND_CREDENTIALS= LDAP_USER_SEARCH_BASE= LDAP_SEARCH_FILTER=mail={{username}} LDAP_CA_CERT_PATH= +# LDAP_ID= +# LDAP_USERNAME= +# LDAP_FULL_NAME= #========================# # Email Password Reset # diff --git a/api/server/index.js b/api/server/index.js index d99340a1f..fa7aba56a 100644 --- a/api/server/index.js +++ b/api/server/index.js @@ -61,7 +61,7 @@ const startServer = async () => { passport.use(passportLogin()); // LDAP Auth - if (process.env.LDAP_URL && process.env.LDAP_BIND_DN && process.env.LDAP_USER_SEARCH_BASE) { + if (process.env.LDAP_URL && process.env.LDAP_USER_SEARCH_BASE) { passport.use(ldapLogin); } diff --git a/api/server/middleware/requireLdapAuth.js b/api/server/middleware/requireLdapAuth.js index 4208050c0..fc9b15825 100644 --- a/api/server/middleware/requireLdapAuth.js +++ b/api/server/middleware/requireLdapAuth.js @@ -13,7 +13,7 @@ const requireLdapAuth = (req, res, next) => { console.log({ title: '(requireLdapAuth) Error: No user', }); - return res.status(422).send(info); + return res.status(404).send(info); } req.user = user; next(); diff --git a/api/server/routes/auth.js b/api/server/routes/auth.js index 946c6cd42..96f0a1e3f 100644 --- a/api/server/routes/auth.js +++ b/api/server/routes/auth.js @@ -21,8 +21,7 @@ const { const router = express.Router(); -const ldapAuth = - !!process.env.LDAP_URL && !!process.env.LDAP_BIND_DN && !!process.env.LDAP_USER_SEARCH_BASE; +const ldapAuth = !!process.env.LDAP_URL && !!process.env.LDAP_USER_SEARCH_BASE; //Local router.post('/logout', requireJwtAuth, logoutController); router.post( diff --git a/api/server/routes/config.js b/api/server/routes/config.js index 4c2fd28d2..113395939 100644 --- a/api/server/routes/config.js +++ b/api/server/routes/config.js @@ -33,8 +33,7 @@ router.get('/', async function (req, res) { const instanceProject = await getProjectByName('instance', '_id'); - const ldapLoginEnabled = - !!process.env.LDAP_URL && !!process.env.LDAP_BIND_DN && !!process.env.LDAP_USER_SEARCH_BASE; + const ldapLoginEnabled = !!process.env.LDAP_URL && !!process.env.LDAP_USER_SEARCH_BASE; try { /** @type {TStartupConfig} */ const payload = { diff --git a/api/strategies/ldapStrategy.js b/api/strategies/ldapStrategy.js index 344d20d83..7b6898666 100644 --- a/api/strategies/ldapStrategy.js +++ b/api/strategies/ldapStrategy.js @@ -1,17 +1,66 @@ +const fs = require('fs'); const LdapStrategy = require('passport-ldapauth'); const { findUser, createUser, updateUser } = require('~/models/userMethods'); -const fs = require('fs'); +const logger = require('~/utils/logger'); + +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, +} = process.env; + +// Check required environment variables +if (!LDAP_URL || !LDAP_USER_SEARCH_BASE) { + return 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); +} const ldapOptions = { server: { - url: process.env.LDAP_URL, - bindDN: process.env.LDAP_BIND_DN, - bindCredentials: process.env.LDAP_BIND_CREDENTIALS, - searchBase: process.env.LDAP_USER_SEARCH_BASE, - searchFilter: process.env.LDAP_SEARCH_FILTER || 'mail={{username}}', - searchAttributes: ['displayName', 'mail', 'uid', 'cn', 'name', 'commonname', 'givenName', 'sn'], - ...(process.env.LDAP_CA_CERT_PATH && { - tlsOptions: { ca: [fs.readFileSync(process.env.LDAP_CA_CERT_PATH)] }, + 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: { + ca: (() => { + try { + return [fs.readFileSync(LDAP_CA_CERT_PATH)]; + } catch (err) { + logger.error('[ldapStrategy]', 'Failed to read CA certificate', err); + throw err; + } + })(), + }, }), }, usernameField: 'email', @@ -23,45 +72,55 @@ const ldapLogin = new LdapStrategy(ldapOptions, async (userinfo, done) => { return done(null, false, { message: 'Invalid credentials' }); } - try { - const firstName = userinfo.givenName; - const familyName = userinfo.surname || userinfo.sn; - const fullName = - firstName && familyName - ? `${firstName} ${familyName}` - : userinfo.cn || - userinfo.name || - userinfo.commonname || - userinfo.displayName || - userinfo.mail; + if (!userinfo.mail) { + logger.warn( + '[ldapStrategy]', + 'No email attributes found in userinfo', + JSON.stringify(userinfo, null, 2), + ); + 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 }); + + 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; - const username = userinfo.givenName || userinfo.mail; - let user = await findUser({ email: userinfo.mail }); - if (user && user.provider !== 'ldap') { - return done(null, false, { message: 'Invalid credentials' }); - } if (!user) { user = { provider: 'ldap', - ldapId: userinfo.uid, + ldapId, username, - email: userinfo.mail || '', - emailVerified: true, + email: userinfo.mail, + emailVerified: true, // The ldap server administrator should verify the email name: fullName, }; const userId = await createUser(user); user._id = userId; } else { + // Users registered in LDAP are assumed to have their user information managed in LDAP, + // so update the user information with the values registered in LDAP user.provider = 'ldap'; - user.ldapId = userinfo.uid; + user.ldapId = ldapId; + user.email = userinfo.mail; user.username = username; user.name = fullName; } user = await updateUser(user._id, user); - done(null, user); } catch (err) { + logger.error('[ldapStrategy]', err); done(err); } });