+12








fcd7c979df
Co-authored-by: Artur <145776561+arturict@users.noreply.github.com> Co-authored-by: arturict <arturict@users.noreply.github.com> Co-authored-by: Thomas <thomas@ajnart.fr> Co-authored-by: Afonso Jorge Ramos <afonsojorgeramos@gmail.com> Co-authored-by: Danura30082 <danura30082+github@gmail.com> Co-authored-by: Kushie <122986913+kushiemoon-dev@users.noreply.github.com> Co-authored-by: ajnart <thomas@ajnart.fr> Co-authored-by: ajnart <thomas@ajnart.dev> Co-authored-by: chenjun <chenjun@foxmail.com> Co-authored-by: Wikibear <wikibear@users.noreply.github.com> Co-authored-by: Bergasha <bergasha@gmail.com> Co-authored-by: leafyelin <43306196+leafyelin@users.noreply.github.com> Co-authored-by: Lucas Araujo <lucas.lra@gmail.com> Co-authored-by: GreenMonito <86085570+GreenMonito@users.noreply.github.com> Co-authored-by: Nerdy-Technician <roffo@nerdy-technician.social> Co-authored-by: Jun <34545896+jcstrong@users.noreply.github.com> Co-authored-by: Edmond Abraham <54119952+gitedmond@users.noreply.github.com> Co-authored-by: thrawnx <tight.bulb9384@fastmail.com> Co-authored-by: Chris <58918656+nordada@users.noreply.github.com> Co-authored-by: yi111 <153097222+Yi-111-a@users.noreply.github.com> Co-authored-by: Yanis <naamaneyanis@gmail.com> Co-authored-by: Wikibear <me@sej.de> Co-authored-by: alessiottavo <93441886+alessiottavo@users.noreply.github.com>
161 lines
5.5 KiB
TypeScript
161 lines
5.5 KiB
TypeScript
import { CredentialsSignin } from "@auth/core/errors";
|
|
import { EqualityFilter } from "ldapts";
|
|
import type { z } from "zod/v4";
|
|
|
|
import { createId } from "@homarr/common";
|
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
|
import type { Database, InferInsertModel } from "@homarr/db";
|
|
import { and, eq } from "@homarr/db";
|
|
import { users } from "@homarr/db/schema";
|
|
import type { ldapSignInSchema } from "@homarr/validation/user";
|
|
import { utf8EmailSchema } from "@homarr/validation/email";
|
|
|
|
import { env } from "../../../env";
|
|
import { LdapClient } from "../ldap-client";
|
|
|
|
const logger = createLogger({ module: "ldapAuthorization" });
|
|
|
|
export const authorizeWithLdapCredentialsAsync = async (
|
|
db: Database,
|
|
credentials: z.infer<typeof ldapSignInSchema>,
|
|
) => {
|
|
logger.info("User is trying to log in using LDAP. Connecting to LDAP server...", { userName: credentials.name });
|
|
const client = new LdapClient();
|
|
await client
|
|
.bindAsync({
|
|
distinguishedName: env.AUTH_LDAP_BIND_DN,
|
|
password: env.AUTH_LDAP_BIND_PASSWORD,
|
|
})
|
|
.catch((error) => {
|
|
throw new CredentialsSignin("Failed to connect to LDAP server", { cause: error });
|
|
});
|
|
|
|
logger.info("Connected to LDAP server. Searching for user...");
|
|
|
|
const ldapUser = await client
|
|
.searchAsync({
|
|
base: env.AUTH_LDAP_BASE,
|
|
options: {
|
|
filter: createLdapUserFilter(credentials.name),
|
|
scope: env.AUTH_LDAP_SEARCH_SCOPE,
|
|
attributes: [
|
|
...new Set([
|
|
env.AUTH_LDAP_USERNAME_ATTRIBUTE,
|
|
env.AUTH_LDAP_USER_MAIL_ATTRIBUTE,
|
|
env.AUTH_LDAP_GROUP_MEMBER_USER_ATTRIBUTE,
|
|
]),
|
|
],
|
|
},
|
|
})
|
|
.then((entries) => {
|
|
if (entries.length > 1) {
|
|
logger.warn(`Multiple LDAP users found for ${credentials.name}, expected only one.`);
|
|
throw new CredentialsSignin();
|
|
}
|
|
|
|
return entries.at(0);
|
|
});
|
|
|
|
if (!ldapUser) {
|
|
throw new CredentialsSignin(`User not found in LDAP username="${credentials.name}"`);
|
|
}
|
|
|
|
const mailResult = await utf8EmailSchema.safeParseAsync(ldapUser[env.AUTH_LDAP_USER_MAIL_ATTRIBUTE]);
|
|
|
|
if (!mailResult.success) {
|
|
logger.error("User found in LDAP but with invalid or non-existing Email", {
|
|
userName: credentials.name,
|
|
emailValue: ldapUser[env.AUTH_LDAP_USER_MAIL_ATTRIBUTE],
|
|
});
|
|
throw new CredentialsSignin("User found in LDAP but with invalid or non-existing Email");
|
|
}
|
|
|
|
logger.info("User found in LDAP. Logging in...", { userName: credentials.name });
|
|
|
|
// Bind with user credentials to check if the password is correct
|
|
const userClient = new LdapClient();
|
|
await userClient
|
|
.bindAsync({
|
|
distinguishedName: ldapUser.dn,
|
|
password: credentials.password,
|
|
})
|
|
.catch(() => {
|
|
logger.warn("Wrong credentials for user", { userName: credentials.name });
|
|
throw new CredentialsSignin();
|
|
});
|
|
await userClient.disconnectAsync();
|
|
|
|
logger.info("User credentials are correct. Retrieving user groups...", { userName: credentials.name });
|
|
|
|
const groupClassFilter = new EqualityFilter({ attribute: "objectClass", value: env.AUTH_LDAP_GROUP_CLASS });
|
|
const groupMemberFilter = new EqualityFilter({
|
|
attribute: env.AUTH_LDAP_GROUP_MEMBER_ATTRIBUTE,
|
|
value: ldapUser[env.AUTH_LDAP_GROUP_MEMBER_USER_ATTRIBUTE],
|
|
});
|
|
const userGroups = await client
|
|
.searchAsync({
|
|
base: env.AUTH_LDAP_BASE,
|
|
options: {
|
|
// For example, if the user is doejohn, the filter will be (&(objectClass=group)(uid=doejohn)) or (&(objectClass=group)(uid=doejohn)(sAMAccountType=1234))
|
|
filter: `(&${groupClassFilter.toString()}${groupMemberFilter.toString()}${env.AUTH_LDAP_GROUP_FILTER_EXTRA_ARG ?? ""})`,
|
|
scope: env.AUTH_LDAP_SEARCH_SCOPE,
|
|
attributes: ["cn"],
|
|
},
|
|
})
|
|
.then((entries) => entries.map((entry) => entry.cn).filter((group): group is string => group !== undefined));
|
|
|
|
logger.info("User groups retrieved", { userName: credentials.name, groups: userGroups.length });
|
|
|
|
await client.disconnectAsync();
|
|
|
|
// Create or update user in the database
|
|
let user = await db.query.users.findFirst({
|
|
columns: {
|
|
id: true,
|
|
name: true,
|
|
image: true,
|
|
email: true,
|
|
emailVerified: true,
|
|
provider: true,
|
|
},
|
|
where: and(eq(users.email, mailResult.data), eq(users.provider, "ldap")),
|
|
});
|
|
|
|
if (!user) {
|
|
logger.info("User not found in the database. Creating...", { userName: credentials.name });
|
|
|
|
const insertUser = {
|
|
id: createId(),
|
|
name: credentials.name,
|
|
email: mailResult.data,
|
|
emailVerified: new Date(), // assume email is verified
|
|
image: null,
|
|
provider: "ldap",
|
|
} satisfies InferInsertModel<typeof users>;
|
|
|
|
await db.insert(users).values(insertUser);
|
|
|
|
user = insertUser;
|
|
|
|
logger.info("User created successfully", { userName: credentials.name });
|
|
}
|
|
|
|
return {
|
|
id: user.id,
|
|
name: credentials.name,
|
|
// Groups is used in events.ts to synchronize groups with external systems
|
|
groups: userGroups,
|
|
};
|
|
};
|
|
|
|
const createLdapUserFilter = (username: string) => {
|
|
const userFilter = new EqualityFilter({ attribute: env.AUTH_LDAP_USERNAME_ATTRIBUTE, value: username }).toString();
|
|
if (env.AUTH_LDAP_USERNAME_FILTER_EXTRA_ARG) {
|
|
// For example, if the username is doejohn and the extra arg is (sAMAccountType=1234), the filter will be (&(uid=doejohn)(sAMAccountType=1234))
|
|
return `(&${userFilter}${env.AUTH_LDAP_USERNAME_FILTER_EXTRA_ARG})`;
|
|
}
|
|
|
|
// For example, if the username is doejohn, the filter will be (uid=doejohn)
|
|
return userFilter;
|
|
};
|