omnichannel-backend/src/modules/auth/providers/ldap-auth.provider.ts

134 lines
3.8 KiB
TypeScript
Raw Normal View History

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';
import { UserAccessService } from '../user-access.service';
@Injectable()
export class LdapAuthProvider {
constructor(
private readonly authConfig: AuthConfigService,
private readonly authToken: AuthTokenService,
private readonly userAccess: UserAccessService,
) {}
async authenticate({ username, password }: LoginData): Promise<AuthResult> {
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 providerUser = {
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,
};
const user = await this.userAccess.syncAuthenticatedUser(providerUser);
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<string, unknown> | 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;
}
}