feat(auth): LDAP login — authenticate imported users against the directory
LDAP-imported users have no local passwordHash, and validateUser only checked the local password, so they could never log in. Now a passwordless user with an ldapDn is authenticated by binding as their OWN DN with the entered password against the tenant's active LDAP config (reusing the ldaps TLS-skip option). Empty passwords are rejected before binding to avoid AD's unauthenticated-bind bypass. Local-password users are unchanged. LdapService.verifyUserCredentials added; LdapModule now exports LdapConfigService; AuthModule imports LdapModule (no circular dep). 8 new specs (bind success/fail, empty-password guard, login via bind, wrong pw, no config, no ldapDn, inactive). API 226 green, tsc clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,8 @@ import { JwtService } from '@nestjs/jwt';
|
||||
import * as argon2 from 'argon2';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { Response } from 'express';
|
||||
import { LdapConfigService } from '../ldap/ldap-config.service';
|
||||
import { LdapService } from '../ldap/ldap.service';
|
||||
import { MailService } from '../mail/mail.service';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
|
||||
@@ -21,6 +23,8 @@ export class AuthService {
|
||||
private jwtService: JwtService,
|
||||
private configService: ConfigService,
|
||||
private mailService: MailService,
|
||||
private ldapService: LdapService,
|
||||
private ldapConfigService: LdapConfigService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -40,9 +44,35 @@ export class AuthService {
|
||||
return null;
|
||||
}
|
||||
|
||||
// LDAP users without local password cannot log in via local auth
|
||||
// LDAP users have no local password — authenticate them against the
|
||||
// directory by binding as their OWN DN with the password they entered.
|
||||
if (!user.passwordHash) {
|
||||
return null;
|
||||
if (!user.ldapDn) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const config = await this.ldapConfigService.getConfig(user.tenantId);
|
||||
if (!config || !config.isActive) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ok = await this.ldapService.verifyUserCredentials(
|
||||
{
|
||||
serverUrl: config.serverUrl,
|
||||
tlsRejectUnauthorized: config.tlsRejectUnauthorized,
|
||||
},
|
||||
user.ldapDn,
|
||||
password,
|
||||
);
|
||||
if (!ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
await this.prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: { lastLoginAt: new Date() },
|
||||
});
|
||||
return user;
|
||||
}
|
||||
|
||||
const isPasswordValid = await argon2.verify(user.passwordHash, password);
|
||||
|
||||
Reference in New Issue
Block a user