import { ForbiddenException, Injectable, UnauthorizedException } from '@nestjs/common'; import { Client } from 'ldapts'; import { AuthConfigService } from '../auth.config'; import { AuthTokenService } from '../auth-token.service'; import { AuthResult, LoginData } from '../auth.types'; @Injectable() export class LdapAuthProvider { constructor( private readonly authConfig: AuthConfigService, private readonly authToken: AuthTokenService, ) {} async authenticate({ username, password }: LoginData): Promise { const config = this.authConfig.getConfig(); if (!config.ldap.enabled) { throw new ForbiddenException('Login AD/LDAP desabilitado'); } if (!config.ldap.url) { throw new Error('LDAP_URL nao configurado'); } if (!username || !password) { throw new UnauthorizedException('Usuario e senha sao obrigatorios'); } const client = new Client({ url: config.ldap.url, timeout: config.ldap.timeoutMs, connectTimeout: config.ldap.timeoutMs, }); try { if (config.ldap.bindDn && config.ldap.bindPassword) { await client.bind(config.ldap.bindDn, config.ldap.bindPassword); } const userPrincipal = this.buildUserPrincipal(username); await client.bind(userPrincipal, password); const directoryUser = await this.searchUser(client, username); const user = { id: directoryUser?.email || userPrincipal, name: directoryUser?.name || username, email: directoryUser?.email || (config.ldap.domain ? `${username}@${config.ldap.domain}` : null), username: directoryUser?.username || username, provider: 'ldap' as const, }; return { token: this.authToken.issueToken(user), user, }; } catch (_error) { throw new UnauthorizedException('Autenticacao AD/LDAP falhou'); } finally { await client.unbind().catch(() => undefined); } } private buildUserPrincipal(username: string) { const config = this.authConfig.getConfig(); if (config.ldap.userDnTemplate) { return config.ldap.userDnTemplate.replaceAll('{{username}}', username); } if (config.ldap.domain) { return `${username}@${config.ldap.domain}`; } return username; } private async searchUser(client: Client, username: string) { const config = this.authConfig.getConfig(); if (!config.ldap.searchBase) { return null; } const filter = config.ldap.searchFilter.replaceAll('{{username}}', username); const { searchEntries } = await client.search(config.ldap.searchBase, { scope: 'sub', filter, attributes: [ 'cn', 'displayName', 'givenName', 'sn', 'mail', 'userPrincipalName', 'sAMAccountName', ], sizeLimit: 1, }); const entry = searchEntries[0] as Record | undefined; if (!entry) { return null; } const givenName = this.getFirstValue(entry.givenName); const surname = this.getFirstValue(entry.sn); const fullName = [givenName, surname].filter(Boolean).join(' '); return { name: this.getFirstValue(entry.displayName) || fullName || this.getFirstValue(entry.cn) || username, email: this.getFirstValue(entry.mail) || this.getFirstValue(entry.userPrincipalName) || null, username: this.getFirstValue(entry.sAMAccountName) || username, }; } private getFirstValue(value: unknown): string | null { if (Array.isArray(value)) { return String(value[0] || '') || null; } return value ? String(value) : null; } }