fix: ldap filters (#2033)

This commit is contained in:
SeDemal
2024-05-07 19:43:51 +02:00
committed by GitHub
parent 452304b471
commit f12dd52208
2 changed files with 52 additions and 13 deletions

View File

@@ -83,9 +83,12 @@ const env = createEnv({
AUTH_LDAP_BASE: z.string(), AUTH_LDAP_BASE: z.string(),
AUTH_LDAP_SEARCH_SCOPE: z.enum(['base', 'one', 'sub']).default('base'), AUTH_LDAP_SEARCH_SCOPE: z.enum(['base', 'one', 'sub']).default('base'),
AUTH_LDAP_USERNAME_ATTRIBUTE: z.string().default('uid'), 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_CLASS: z.string().default('groupOfUniqueNames'),
AUTH_LDAP_GROUP_MEMBER_ATTRIBUTE: z.string().default('member'), AUTH_LDAP_GROUP_MEMBER_ATTRIBUTE: z.string().default('member'),
AUTH_LDAP_GROUP_MEMBER_USER_ATTRIBUTE: z.string().default('dn'), 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_ADMIN_GROUP: z.string().default('admin'),
AUTH_LDAP_OWNER_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_OWNER_GROUP: z.string().default('admin'),
AUTH_OIDC_AUTO_LOGIN: zodParsedBoolean(), AUTH_OIDC_AUTO_LOGIN: zodParsedBoolean(),
AUTH_OIDC_SCOPE_OVERWRITE: z.string().default('openid email profile groups'), 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_BASE: process.env.AUTH_LDAP_BASE,
AUTH_LDAP_SEARCH_SCOPE: process.env.AUTH_LDAP_SEARCH_SCOPE?.toLowerCase(), AUTH_LDAP_SEARCH_SCOPE: process.env.AUTH_LDAP_SEARCH_SCOPE?.toLowerCase(),
AUTH_LDAP_USERNAME_ATTRIBUTE: process.env.AUTH_LDAP_USERNAME_ATTRIBUTE, 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_CLASS: process.env.AUTH_LDAP_GROUP_CLASS,
AUTH_LDAP_GROUP_MEMBER_ATTRIBUTE: process.env.AUTH_LDAP_GROUP_MEMBER_ATTRIBUTE, 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_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_ADMIN_GROUP: process.env.AUTH_LDAP_ADMIN_GROUP,
AUTH_LDAP_OWNER_GROUP: process.env.AUTH_LDAP_OWNER_GROUP, AUTH_LDAP_OWNER_GROUP: process.env.AUTH_LDAP_OWNER_GROUP,
AUTH_OIDC_CLIENT_ID: process.env.AUTH_OIDC_CLIENT_ID, AUTH_OIDC_CLIENT_ID: process.env.AUTH_OIDC_CLIENT_ID,

View File

@@ -1,6 +1,7 @@
import Consola from 'consola'; import Consola from 'consola';
import ldap from 'ldapjs'; import ldap from 'ldapjs';
import Credentials from 'next-auth/providers/credentials'; import Credentials from 'next-auth/providers/credentials';
import { z } from 'zod';
import { env } from '~/env'; import { env } from '~/env';
import { signInSchema } from '~/validations/user'; import { signInSchema } from '~/validations/user';
@@ -61,6 +62,15 @@ const ldapSearch = async <
reject('error: ' + err.message); reject('error: ' + err.message);
}); });
res.on('searchEntry', (entry) => { 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(/(?<!\\)\\([0-9a-fA-F]{2})/g, '%$1')
)
} catch { reject(new Error ('Cannot resolve distinguishedName for the user')) }
results.push( results.push(
entry.pojo.attributes.reduce<Record<string, string | string[]>>( entry.pojo.attributes.reduce<Record<string, string | string[]>>(
(obj, attr) => { (obj, attr) => {
@@ -70,7 +80,10 @@ const ldapSearch = async <
: attr.values[0]; : attr.values[0];
return obj; return obj;
}, },
{ dn: entry.pojo.objectName } {
// Assume userDn since there's a reject if not set
dn: userDn!,
}
) as SearchResult<Attributes, ArrayAttributes> ) as SearchResult<Attributes, ArrayAttributes>
); );
}); });
@@ -95,27 +108,47 @@ export default Credentials({
try { try {
const data = await signInSchema.parseAsync(credentials); 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); const client = await ldapLogin(env.AUTH_LDAP_BIND_DN, env.AUTH_LDAP_BIND_PASSWORD);
Consola.log(`Connection established. Searching User...`);
const ldapUser = ( const ldapUser = (
await ldapSearch(client, env.AUTH_LDAP_BASE, { 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, scope: env.AUTH_LDAP_SEARCH_SCOPE,
// as const for inference // as const for inference
attributes: ['uid', 'mail'] as const, attributes: [
env.AUTH_LDAP_USERNAME_ATTRIBUTE,
env.AUTH_LDAP_USER_MAIL_ATTRIBUTE,
] as const,
}) })
)[0]; )[0];
if (!ldapUser) throw new Error('User not found in LDAP'); 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()); await ldapLogin(ldapUser.dn, data.password).then((client) => client.destroy());
Consola.log(`User logged in. Retrieving groups...`);
const userGroups = ( const userGroups = (
await ldapSearch(client, env.AUTH_LDAP_BASE, { 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 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, scope: env.AUTH_LDAP_SEARCH_SCOPE,
// as const for inference // as const for inference
attributes: 'cn', attributes: 'cn',
@@ -126,15 +159,15 @@ export default Credentials({
Consola.log(`user ${data.name} successfully authorized`); Consola.log(`user ${data.name} successfully authorized`);
let user = await adapter.getUserByEmail!(ldapUser.mail); let user = await adapter.getUserByEmail!(ldapUser[env.AUTH_LDAP_USER_MAIL_ATTRIBUTE]);
const isAdmin = userGroups.includes(env.AUTH_LDAP_ADMIN_GROUP);
const isOwner = userGroups.includes(env.AUTH_LDAP_OWNER_GROUP); const isOwner = userGroups.includes(env.AUTH_LDAP_OWNER_GROUP);
const isAdmin = isOwner || userGroups.includes(env.AUTH_LDAP_ADMIN_GROUP);
if (!user) { if (!user) {
// CreateUser will create settings in event // CreateUser will create settings in event
user = await adapter.createUser({ user = adapter.createUser({
name: ldapUser.uid, name: ldapUser[env.AUTH_LDAP_USERNAME_ATTRIBUTE],
email: ldapUser.mail, email: ldapUser[env.AUTH_LDAP_USER_MAIL_ATTRIBUTE],
emailVerified: new Date(), // assume ldap email is verified emailVerified: new Date(), // assume ldap email is verified
isAdmin: isAdmin, isAdmin: isAdmin,
isOwner: isOwner, isOwner: isOwner,
@@ -153,7 +186,7 @@ export default Credentials({
return { return {
id: user?.id || ldapUser.dn, id: user?.id || ldapUser.dn,
name: user?.name || ldapUser.uid, name: user?.name || ldapUser[env.AUTH_LDAP_USERNAME_ATTRIBUTE],
isAdmin: isAdmin, isAdmin: isAdmin,
isOwner: isOwner, isOwner: isOwner,
}; };