fix: ldap filters (#2033)
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user