🚀 feat(LDAP): Add Flexible Configuration Options (#3124)

* chore: add detailed logs

* feat: added a variable to specify which attributes to be stored

* chore: Add new optiona variables

* refactor: change BIND_DN as an option

* chore: revert commits that fail testing

* refactor: use ldapid to retrieve users

* chore: remove unused variable

* chore: reverting unintended changes

* fix: return 404 if authentication fails, in accordance with requireLocalAuth.

* fix: handling when ldap settings do not exist

* chore: remove unnecessary check
This commit is contained in:
Yuichi Oneda 2024-06-21 07:14:53 -07:00 committed by GitHub
parent a53312bbd4
commit a8c874267f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 96 additions and 36 deletions

View file

@ -372,6 +372,9 @@ LDAP_BIND_CREDENTIALS=
LDAP_USER_SEARCH_BASE= LDAP_USER_SEARCH_BASE=
LDAP_SEARCH_FILTER=mail={{username}} LDAP_SEARCH_FILTER=mail={{username}}
LDAP_CA_CERT_PATH= LDAP_CA_CERT_PATH=
# LDAP_ID=
# LDAP_USERNAME=
# LDAP_FULL_NAME=
#========================# #========================#
# Email Password Reset # # Email Password Reset #

View file

@ -61,7 +61,7 @@ const startServer = async () => {
passport.use(passportLogin()); passport.use(passportLogin());
// LDAP Auth // 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); passport.use(ldapLogin);
} }

View file

@ -13,7 +13,7 @@ const requireLdapAuth = (req, res, next) => {
console.log({ console.log({
title: '(requireLdapAuth) Error: No user', title: '(requireLdapAuth) Error: No user',
}); });
return res.status(422).send(info); return res.status(404).send(info);
} }
req.user = user; req.user = user;
next(); next();

View file

@ -21,8 +21,7 @@ const {
const router = express.Router(); const router = express.Router();
const ldapAuth = const ldapAuth = !!process.env.LDAP_URL && !!process.env.LDAP_USER_SEARCH_BASE;
!!process.env.LDAP_URL && !!process.env.LDAP_BIND_DN && !!process.env.LDAP_USER_SEARCH_BASE;
//Local //Local
router.post('/logout', requireJwtAuth, logoutController); router.post('/logout', requireJwtAuth, logoutController);
router.post( router.post(

View file

@ -33,8 +33,7 @@ router.get('/', async function (req, res) {
const instanceProject = await getProjectByName('instance', '_id'); const instanceProject = await getProjectByName('instance', '_id');
const ldapLoginEnabled = const ldapLoginEnabled = !!process.env.LDAP_URL && !!process.env.LDAP_USER_SEARCH_BASE;
!!process.env.LDAP_URL && !!process.env.LDAP_BIND_DN && !!process.env.LDAP_USER_SEARCH_BASE;
try { try {
/** @type {TStartupConfig} */ /** @type {TStartupConfig} */
const payload = { const payload = {

View file

@ -1,17 +1,66 @@
const fs = require('fs');
const LdapStrategy = require('passport-ldapauth'); const LdapStrategy = require('passport-ldapauth');
const { findUser, createUser, updateUser } = require('~/models/userMethods'); 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 = { const ldapOptions = {
server: { server: {
url: process.env.LDAP_URL, url: LDAP_URL,
bindDN: process.env.LDAP_BIND_DN, bindDN: LDAP_BIND_DN,
bindCredentials: process.env.LDAP_BIND_CREDENTIALS, bindCredentials: LDAP_BIND_CREDENTIALS,
searchBase: process.env.LDAP_USER_SEARCH_BASE, searchBase: LDAP_USER_SEARCH_BASE,
searchFilter: process.env.LDAP_SEARCH_FILTER || 'mail={{username}}', searchFilter: LDAP_SEARCH_FILTER || 'mail={{username}}',
searchAttributes: ['displayName', 'mail', 'uid', 'cn', 'name', 'commonname', 'givenName', 'sn'], searchAttributes: [...new Set(searchAttributes)],
...(process.env.LDAP_CA_CERT_PATH && { ...(LDAP_CA_CERT_PATH && {
tlsOptions: { ca: [fs.readFileSync(process.env.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', usernameField: 'email',
@ -23,45 +72,55 @@ const ldapLogin = new LdapStrategy(ldapOptions, async (userinfo, done) => {
return done(null, false, { message: 'Invalid credentials' }); return done(null, false, { message: 'Invalid credentials' });
} }
try { if (!userinfo.mail) {
const firstName = userinfo.givenName; logger.warn(
const familyName = userinfo.surname || userinfo.sn; '[ldapStrategy]',
const fullName = 'No email attributes found in userinfo',
firstName && familyName JSON.stringify(userinfo, null, 2),
? `${firstName} ${familyName}` );
: userinfo.cn ||
userinfo.name ||
userinfo.commonname ||
userinfo.displayName ||
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' }); 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;
if (!user) { if (!user) {
user = { user = {
provider: 'ldap', provider: 'ldap',
ldapId: userinfo.uid, ldapId,
username, username,
email: userinfo.mail || '', email: userinfo.mail,
emailVerified: true, emailVerified: true, // The ldap server administrator should verify the email
name: fullName, name: fullName,
}; };
const userId = await createUser(user); const userId = await createUser(user);
user._id = userId; user._id = userId;
} else { } 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.provider = 'ldap';
user.ldapId = userinfo.uid; user.ldapId = ldapId;
user.email = userinfo.mail;
user.username = username; user.username = username;
user.name = fullName; user.name = fullName;
} }
user = await updateUser(user._id, user); user = await updateUser(user._id, user);
done(null, user); done(null, user);
} catch (err) { } catch (err) {
logger.error('[ldapStrategy]', err);
done(err); done(err);
} }
}); });