From f12dd52208c4ed8fdc92da01c2223de366e3f06a Mon Sep 17 00:00:00 2001 From: SeDemal Date: Tue, 7 May 2024 19:43:51 +0200 Subject: [PATCH] fix: ldap filters (#2033) --- src/env.js | 8 +++++- src/utils/auth/ldap.ts | 57 +++++++++++++++++++++++++++++++++--------- 2 files changed, 52 insertions(+), 13 deletions(-) diff --git a/src/env.js b/src/env.js index e0080ca9d3b..ef6039f9b62 100644 --- a/src/env.js +++ b/src/env.js @@ -83,9 +83,12 @@ const env = createEnv({ AUTH_LDAP_BASE: z.string(), AUTH_LDAP_SEARCH_SCOPE: z.enum(['base', 'one', 'sub']).default('base'), AUTH_LDAP_USERNAME_ATTRIBUTE: z.string().default('uid'), + AUTH_LDAP_USER_MAIL_ATTRIBUTE: z.string().default('mail'), + AUTH_LDAP_USERNAME_FILTER_EXTRA_ARG: z.string().optional(), AUTH_LDAP_GROUP_CLASS: z.string().default('groupOfUniqueNames'), AUTH_LDAP_GROUP_MEMBER_ATTRIBUTE: z.string().default('member'), AUTH_LDAP_GROUP_MEMBER_USER_ATTRIBUTE: z.string().default('dn'), + AUTH_LDAP_GROUP_FILTER_EXTRA_ARG: z.string().optional(), AUTH_LDAP_ADMIN_GROUP: z.string().default('admin'), AUTH_LDAP_OWNER_GROUP: z.string().default('admin'), } @@ -102,7 +105,7 @@ const env = createEnv({ AUTH_OIDC_OWNER_GROUP: z.string().default('admin'), AUTH_OIDC_AUTO_LOGIN: zodParsedBoolean(), AUTH_OIDC_SCOPE_OVERWRITE: z.string().default('openid email profile groups'), - AUTH_OIDC_TIMEOUT: numberSchema.default('3500'), + AUTH_OIDC_TIMEOUT: numberSchema.default(3500), } : {}), }, @@ -149,9 +152,12 @@ const env = createEnv({ AUTH_LDAP_BASE: process.env.AUTH_LDAP_BASE, AUTH_LDAP_SEARCH_SCOPE: process.env.AUTH_LDAP_SEARCH_SCOPE?.toLowerCase(), AUTH_LDAP_USERNAME_ATTRIBUTE: process.env.AUTH_LDAP_USERNAME_ATTRIBUTE, + AUTH_LDAP_USER_MAIL_ATTRIBUTE: process.env.AUTH_LDAP_USER_MAIL_ATTRIBUTE, + AUTH_LDAP_USERNAME_FILTER_EXTRA_ARG: process.env.AUTH_LDAP_USERNAME_FILTER_EXTRA_ARG, AUTH_LDAP_GROUP_CLASS: process.env.AUTH_LDAP_GROUP_CLASS, AUTH_LDAP_GROUP_MEMBER_ATTRIBUTE: process.env.AUTH_LDAP_GROUP_MEMBER_ATTRIBUTE, AUTH_LDAP_GROUP_MEMBER_USER_ATTRIBUTE: process.env.AUTH_LDAP_GROUP_MEMBER_USER_ATTRIBUTE, + AUTH_LDAP_GROUP_FILTER_EXTRA_ARG: process.env.AUTH_LDAP_GROUP_FILTER_EXTRA_ARG, AUTH_LDAP_ADMIN_GROUP: process.env.AUTH_LDAP_ADMIN_GROUP, AUTH_LDAP_OWNER_GROUP: process.env.AUTH_LDAP_OWNER_GROUP, AUTH_OIDC_CLIENT_ID: process.env.AUTH_OIDC_CLIENT_ID, diff --git a/src/utils/auth/ldap.ts b/src/utils/auth/ldap.ts index 879e0ccd8ab..e8ba1dd0c03 100644 --- a/src/utils/auth/ldap.ts +++ b/src/utils/auth/ldap.ts @@ -1,6 +1,7 @@ import Consola from 'consola'; import ldap from 'ldapjs'; import Credentials from 'next-auth/providers/credentials'; +import { z } from 'zod'; import { env } from '~/env'; import { signInSchema } from '~/validations/user'; @@ -61,6 +62,15 @@ const ldapSearch = async < reject('error: ' + err.message); }); res.on('searchEntry', (entry) => { + let userDn; + try { + //dn is the only attribute returned with special characters formatted in UTF-8 (Bad for any letters with an accent) + //Regex replaces any backslash followed by 2 hex characters with a percentage unless said backslash is preceded by another backslash. + //That can then be processed by decodeURIComponent which will turn back characters to normal. + userDn = decodeURIComponent( + entry.pojo.objectName.replace(/(?>( (obj, attr) => { @@ -70,7 +80,10 @@ const ldapSearch = async < : attr.values[0]; return obj; }, - { dn: entry.pojo.objectName } + { + // Assume userDn since there's a reject if not set + dn: userDn!, + } ) as SearchResult ); }); @@ -95,27 +108,47 @@ export default Credentials({ try { const data = await signInSchema.parseAsync(credentials); - Consola.log(`user ${data.name} is trying to log in using LDAP. Signing in...`); + Consola.log(`user ${data.name} is trying to log in using LDAP. Connecting to LDAP server...`); const client = await ldapLogin(env.AUTH_LDAP_BIND_DN, env.AUTH_LDAP_BIND_PASSWORD); + Consola.log(`Connection established. Searching User...`); const ldapUser = ( await ldapSearch(client, env.AUTH_LDAP_BASE, { - filter: `(uid=${data.name})`, + filter: env.AUTH_LDAP_USERNAME_FILTER_EXTRA_ARG + ? `(&(${env.AUTH_LDAP_USERNAME_ATTRIBUTE}=${data.name})${env.AUTH_LDAP_USERNAME_FILTER_EXTRA_ARG})` + : `(${env.AUTH_LDAP_USERNAME_ATTRIBUTE}=${data.name})`, scope: env.AUTH_LDAP_SEARCH_SCOPE, // as const for inference - attributes: ['uid', 'mail'] as const, + attributes: [ + env.AUTH_LDAP_USERNAME_ATTRIBUTE, + env.AUTH_LDAP_USER_MAIL_ATTRIBUTE, + ] as const, }) )[0]; if (!ldapUser) throw new Error('User not found in LDAP'); + try { + z.string().email().parse(ldapUser[env.AUTH_LDAP_USER_MAIL_ATTRIBUTE]); + } catch { + throw new Error( + `User found but with invalid or non-existing Email. Not Supported: "${ + ldapUser[env.AUTH_LDAP_USER_MAIL_ATTRIBUTE] ?? ' ' + }"` + ); + } + + Consola.log(`User found. Logging in...`); await ldapLogin(ldapUser.dn, data.password).then((client) => client.destroy()); + Consola.log(`User logged in. Retrieving groups...`); const userGroups = ( await ldapSearch(client, env.AUTH_LDAP_BASE, { - filter: `(&(objectclass=${env.AUTH_LDAP_GROUP_CLASS})(${ + filter: `(&(objectClass=${env.AUTH_LDAP_GROUP_CLASS})(${ env.AUTH_LDAP_GROUP_MEMBER_ATTRIBUTE - }=${ldapUser[env.AUTH_LDAP_GROUP_MEMBER_USER_ATTRIBUTE as 'dn' | 'uid']}))`, + }=${ldapUser[env.AUTH_LDAP_GROUP_MEMBER_USER_ATTRIBUTE as 'dn' | 'uid']})${ + env.AUTH_LDAP_GROUP_FILTER_EXTRA_ARG ?? '' + })`, scope: env.AUTH_LDAP_SEARCH_SCOPE, // as const for inference attributes: 'cn', @@ -126,15 +159,15 @@ export default Credentials({ Consola.log(`user ${data.name} successfully authorized`); - let user = await adapter.getUserByEmail!(ldapUser.mail); - const isAdmin = userGroups.includes(env.AUTH_LDAP_ADMIN_GROUP); + let user = await adapter.getUserByEmail!(ldapUser[env.AUTH_LDAP_USER_MAIL_ATTRIBUTE]); const isOwner = userGroups.includes(env.AUTH_LDAP_OWNER_GROUP); + const isAdmin = isOwner || userGroups.includes(env.AUTH_LDAP_ADMIN_GROUP); if (!user) { // CreateUser will create settings in event - user = await adapter.createUser({ - name: ldapUser.uid, - email: ldapUser.mail, + user = adapter.createUser({ + name: ldapUser[env.AUTH_LDAP_USERNAME_ATTRIBUTE], + email: ldapUser[env.AUTH_LDAP_USER_MAIL_ATTRIBUTE], emailVerified: new Date(), // assume ldap email is verified isAdmin: isAdmin, isOwner: isOwner, @@ -153,7 +186,7 @@ export default Credentials({ return { id: user?.id || ldapUser.dn, - name: user?.name || ldapUser.uid, + name: user?.name || ldapUser[env.AUTH_LDAP_USERNAME_ATTRIBUTE], isAdmin: isAdmin, isOwner: isOwner, };