diff --git a/backend/src/auth/auth.module.ts b/backend/src/auth/auth.module.ts index 13083cb6..05a6e11f 100644 --- a/backend/src/auth/auth.module.ts +++ b/backend/src/auth/auth.module.ts @@ -20,4 +20,4 @@ import { UserModule } from "../user/user.module"; providers: [AuthService, AuthTotpService, JwtStrategy, LdapService], exports: [AuthService], }) -export class AuthModule { } +export class AuthModule {} diff --git a/backend/src/auth/auth.service.ts b/backend/src/auth/auth.service.ts index aa4ab031..aff0ccb5 100644 --- a/backend/src/auth/auth.service.ts +++ b/backend/src/auth/auth.service.ts @@ -29,7 +29,7 @@ export class AuthService { private emailService: EmailService, private ldapService: LdapService, private userService: UserSevice, - ) { } + ) {} private readonly logger = new Logger(AuthService.name); async signUp(dto: AuthRegisterDTO, ip: string, isAdmin?: boolean) { @@ -76,18 +76,28 @@ export class AuthService { }, }); - if (user?.password && await argon.verify(user.password, dto.password)) { - this.logger.log(`Successful password login for user ${user.email} from IP ${ip}`); + if (user?.password && (await argon.verify(user.password, dto.password))) { + this.logger.log( + `Successful password login for user ${user.email} from IP ${ip}`, + ); return this.generateToken(user); } } if (this.config.get("ldap.enabled")) { this.logger.debug(`Trying LDAP login for user ${dto.username}`); - const ldapUser = await this.ldapService.authenticateUser(dto.username, dto.password); + const ldapUser = await this.ldapService.authenticateUser( + dto.username, + dto.password, + ); if (ldapUser) { - const user = await this.userService.findOrCreateFromLDAP(dto.username, ldapUser); - this.logger.log(`Successful LDAP login for user ${user.email} from IP ${ip}`); + const user = await this.userService.findOrCreateFromLDAP( + dto.username, + ldapUser, + ); + this.logger.log( + `Successful LDAP login for user ${user.email} from IP ${ip}`, + ); return this.generateToken(user); } } diff --git a/backend/src/auth/ldap.service.ts b/backend/src/auth/ldap.service.ts index 9a6511f4..cfcd90ed 100644 --- a/backend/src/auth/ldap.service.ts +++ b/backend/src/auth/ldap.service.ts @@ -1,154 +1,194 @@ import { Inject, Injectable, Logger } from "@nestjs/common"; import * as ldap from "ldapjs"; -import { AttributeJson, InvalidCredentialsError, SearchCallbackResponse, SearchOptions } from "ldapjs"; +import { + AttributeJson, + InvalidCredentialsError, + SearchCallbackResponse, + SearchOptions, +} from "ldapjs"; import { inspect } from "node:util"; import { ConfigService } from "../config/config.service"; type LdapSearchEntry = { - objectName: string, - attributes: AttributeJson[], + objectName: string; + attributes: AttributeJson[]; }; -async function ldapExecuteSearch(client: ldap.Client, base: string, options: SearchOptions): Promise { - const searchResponse = await new Promise((resolve, reject) => { - client.search(base, options, (err, res) => { - if (err) { - reject(err); - } else { - resolve(res); - } - }); - }); +async function ldapExecuteSearch( + client: ldap.Client, + base: string, + options: SearchOptions, +): Promise { + const searchResponse = await new Promise( + (resolve, reject) => { + client.search(base, options, (err, res) => { + if (err) { + reject(err); + } else { + resolve(res); + } + }); + }, + ); - return await new Promise((resolve, reject) => { - const entries: LdapSearchEntry[] = []; - searchResponse.on("searchEntry", entry => entries.push({ attributes: entry.pojo.attributes, objectName: entry.pojo.objectName })); - searchResponse.once("error", reject); - searchResponse.once("end", () => resolve(entries)); - }); + return await new Promise((resolve, reject) => { + const entries: LdapSearchEntry[] = []; + searchResponse.on("searchEntry", (entry) => + entries.push({ + attributes: entry.pojo.attributes, + objectName: entry.pojo.objectName, + }), + ); + searchResponse.once("error", reject); + searchResponse.once("end", () => resolve(entries)); + }); } -async function ldapBindUser(client: ldap.Client, dn: string, password: string): Promise { - return new Promise((resolve, reject) => { - client.bind(dn, password, error => { - if (error) { - reject(error); - } else { - resolve(); - } - }); - }) +async function ldapBindUser( + client: ldap.Client, + dn: string, + password: string, +): Promise { + return new Promise((resolve, reject) => { + client.bind(dn, password, (error) => { + if (error) { + reject(error); + } else { + resolve(); + } + }); + }); } -async function ldapCreateConnection(logger: Logger, url: string): Promise { - const ldapClient = ldap.createClient({ - url: url.split(","), - connectTimeout: 10_000, - timeout: 10_000 - }); +async function ldapCreateConnection( + logger: Logger, + url: string, +): Promise { + const ldapClient = ldap.createClient({ + url: url.split(","), + connectTimeout: 10_000, + timeout: 10_000, + }); - await new Promise((resolve, reject) => { - ldapClient.once("error", reject); - ldapClient.on("setupError", reject); - ldapClient.on("socketTimeout", reject); - ldapClient.on("connectRefused", () => reject(new Error("connection has been refused"))); - ldapClient.on("connectTimeout", () => reject(new Error("connect timed out"))); - ldapClient.on("connectError", reject); + await new Promise((resolve, reject) => { + ldapClient.once("error", reject); + ldapClient.on("setupError", reject); + ldapClient.on("socketTimeout", reject); + ldapClient.on("connectRefused", () => + reject(new Error("connection has been refused")), + ); + ldapClient.on("connectTimeout", () => + reject(new Error("connect timed out")), + ); + ldapClient.on("connectError", reject); - ldapClient.on("connect", resolve); - }).catch(error => { - logger.error(`Connect error: ${inspect(error)}`); - ldapClient.destroy(); - throw error; - }); + ldapClient.on("connect", resolve); + }).catch((error) => { + logger.error(`Connect error: ${inspect(error)}`); + ldapClient.destroy(); + throw error; + }); - return ldapClient; + return ldapClient; } export type LdapAuthenticateResult = { - userDn: string, - attributes: Record + userDn: string; + attributes: Record; }; @Injectable() export class LdapService { - private readonly logger = new Logger(LdapService.name); - constructor( - @Inject(ConfigService) - private readonly serviceConfig: ConfigService, - ) { } + private readonly logger = new Logger(LdapService.name); + constructor( + @Inject(ConfigService) + private readonly serviceConfig: ConfigService, + ) {} - private async createLdapConnection(): Promise { - const ldapUrl = this.serviceConfig.get("ldap.url"); - if (!ldapUrl) { - throw new Error("LDAP server URL is not defined"); - } - - const ldapClient = await ldapCreateConnection(this.logger, ldapUrl); - try { - const bindDn = this.serviceConfig.get("ldap.bindDn") || null; - if (bindDn) { - try { - await ldapBindUser(ldapClient, bindDn, this.serviceConfig.get("ldap.bindPassword")) - } catch (error) { - this.logger.warn(`Failed to bind to default user: ${error}`); - throw new Error("failed to bind to default user"); - } - } - - return ldapClient; - } catch (error) { - ldapClient.destroy(); - throw error; - } + private async createLdapConnection(): Promise { + const ldapUrl = this.serviceConfig.get("ldap.url"); + if (!ldapUrl) { + throw new Error("LDAP server URL is not defined"); } - public async authenticateUser(username: string, password: string): Promise { - if (!username.match(/^[a-zA-Z0-0]+$/)) { - return null; - } - - const searchBase = this.serviceConfig.get("ldap.searchBase"); - const searchQuery = this.serviceConfig.get("ldap.searchQuery") - .replaceAll("%username%", username); - - const ldapClient = await this.createLdapConnection(); + const ldapClient = await ldapCreateConnection(this.logger, ldapUrl); + try { + const bindDn = this.serviceConfig.get("ldap.bindDn") || null; + if (bindDn) { try { - const [result] = await ldapExecuteSearch(ldapClient, searchBase, { - filter: searchQuery, - scope: "sub" - }); - - if (!result) { - /* user not found */ - return null; - } - - try { - await ldapBindUser(ldapClient, result.objectName, password); - - /* - * In theory we could query the user attributes now, - * but as we must query the user attributes for validation anyways - * we'll create a second ldap server connection. - */ - return { - userDn: result.objectName, - attributes: Object.fromEntries(result.attributes.map(attribute => [attribute.type, attribute.values])), - }; - } catch (error) { - if (error instanceof InvalidCredentialsError) { - return null; - } - - this.logger.warn(`LDAP user bind failure: ${inspect(error)}`); - return null; - } finally { - ldapClient.destroy(); - } + await ldapBindUser( + ldapClient, + bindDn, + this.serviceConfig.get("ldap.bindPassword"), + ); } catch (error) { - this.logger.warn(`LDAP connect error: ${inspect(error)}`); - return null; + this.logger.warn(`Failed to bind to default user: ${error}`); + throw new Error("failed to bind to default user"); } + } + + return ldapClient; + } catch (error) { + ldapClient.destroy(); + throw error; } -} \ No newline at end of file + } + + public async authenticateUser( + username: string, + password: string, + ): Promise { + if (!username.match(/^[a-zA-Z0-0]+$/)) { + return null; + } + + const searchBase = this.serviceConfig.get("ldap.searchBase"); + const searchQuery = this.serviceConfig + .get("ldap.searchQuery") + .replaceAll("%username%", username); + + const ldapClient = await this.createLdapConnection(); + try { + const [result] = await ldapExecuteSearch(ldapClient, searchBase, { + filter: searchQuery, + scope: "sub", + }); + + if (!result) { + /* user not found */ + return null; + } + + try { + await ldapBindUser(ldapClient, result.objectName, password); + + /* + * In theory we could query the user attributes now, + * but as we must query the user attributes for validation anyways + * we'll create a second ldap server connection. + */ + return { + userDn: result.objectName, + attributes: Object.fromEntries( + result.attributes.map((attribute) => [ + attribute.type, + attribute.values, + ]), + ), + }; + } catch (error) { + if (error instanceof InvalidCredentialsError) { + return null; + } + + this.logger.warn(`LDAP user bind failure: ${inspect(error)}`); + return null; + } finally { + ldapClient.destroy(); + } + } catch (error) { + this.logger.warn(`LDAP connect error: ${inspect(error)}`); + return null; + } + } +} diff --git a/backend/src/user/dto/user.dto.ts b/backend/src/user/dto/user.dto.ts index 7d5da722..b3dec6dc 100644 --- a/backend/src/user/dto/user.dto.ts +++ b/backend/src/user/dto/user.dto.ts @@ -34,7 +34,9 @@ export class UserDTO { totpVerified: boolean; from(partial: Partial) { - const result = plainToClass(UserDTO, partial, { excludeExtraneousValues: true }); + const result = plainToClass(UserDTO, partial, { + excludeExtraneousValues: true, + }); result.isLdap = partial.ldapDN?.length > 0; return result; } diff --git a/backend/src/user/user.module.ts b/backend/src/user/user.module.ts index b9fb497e..b4e3d2f1 100644 --- a/backend/src/user/user.module.ts +++ b/backend/src/user/user.module.ts @@ -8,6 +8,6 @@ import { FileModule } from "src/file/file.module"; imports: [EmailModule, FileModule], providers: [UserSevice], controllers: [UserController], - exports: [UserSevice] + exports: [UserSevice], }) -export class UserModule { } +export class UserModule {} diff --git a/backend/src/user/user.service.ts b/backend/src/user/user.service.ts index 93d2cc45..505a7e09 100644 --- a/backend/src/user/user.service.ts +++ b/backend/src/user/user.service.ts @@ -17,7 +17,7 @@ export class UserSevice { private emailService: EmailService, private fileService: FileService, private configService: ConfigService, - ) { } + ) {} async list() { return await this.prisma.user.findMany(); @@ -94,7 +94,9 @@ export class UserSevice { async findOrCreateFromLDAP(username: string, ldap: LdapAuthenticateResult) { const passwordHash = await argon.hash(crypto.randomUUID()); - const userEmail = ldap.attributes["userPrincipalName"]?.at(0) ?? `${crypto.randomUUID()}@ldap.local`; + const userEmail = + ldap.attributes["userPrincipalName"]?.at(0) ?? + `${crypto.randomUUID()}@ldap.local`; const adminGroup = this.configService.get("ldap.adminGroups"); const isAdmin = ldap.attributes["memberOf"]?.includes(adminGroup) ?? false; try { @@ -114,8 +116,8 @@ export class UserSevice { ldapDN: ldap.userDn, }, where: { - ldapDN: ldap.userDn - } + ldapDN: ldap.userDn, + }, }); } catch (e) { if (e instanceof PrismaClientKnownRequestError) { diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx index 282224df..72db822c 100644 --- a/frontend/src/pages/index.tsx +++ b/frontend/src/pages/index.tsx @@ -162,9 +162,7 @@ export default function Home() { size="md" className={classes.control} > - +