mirror of
https://github.com/ajnart/homarr.git
synced 2026-01-24 00:09:12 +01:00
fix: ldap filters (#2033)
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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(/(?<!\\)\\([0-9a-fA-F]{2})/g, '%$1')
|
||||
)
|
||||
} catch { reject(new Error ('Cannot resolve distinguishedName for the user')) }
|
||||
results.push(
|
||||
entry.pojo.attributes.reduce<Record<string, string | string[]>>(
|
||||
(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<Attributes, ArrayAttributes>
|
||||
);
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user