243 lines
6.6 KiB
TypeScript
243 lines
6.6 KiB
TypeScript
|
|
/**
|
||
|
|
* LDAP Authentication Strategy
|
||
|
|
* Handles LDAP/Active Directory authentication
|
||
|
|
*
|
||
|
|
* Note: This is a basic implementation. For production use with actual LDAP,
|
||
|
|
* you may need to integrate with a Deno-compatible LDAP library.
|
||
|
|
*/
|
||
|
|
|
||
|
|
import type { AuthProvider } from '../../../models/auth.provider.ts';
|
||
|
|
import type { CryptoService } from '../../crypto.service.ts';
|
||
|
|
import type {
|
||
|
|
IExternalUserInfo,
|
||
|
|
IConnectionTestResult,
|
||
|
|
} from '../../../interfaces/auth.interfaces.ts';
|
||
|
|
import type { IAuthStrategy } from './auth.strategy.interface.ts';
|
||
|
|
|
||
|
|
// LDAP entry type (simplified)
|
||
|
|
interface ILdapEntry {
|
||
|
|
dn: string;
|
||
|
|
[key: string]: unknown;
|
||
|
|
}
|
||
|
|
|
||
|
|
export class LdapStrategy implements IAuthStrategy {
|
||
|
|
constructor(
|
||
|
|
private provider: AuthProvider,
|
||
|
|
private cryptoService: CryptoService
|
||
|
|
) {}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Authenticate user with LDAP credentials
|
||
|
|
*/
|
||
|
|
public async authenticateCredentials(
|
||
|
|
username: string,
|
||
|
|
password: string
|
||
|
|
): Promise<IExternalUserInfo> {
|
||
|
|
const config = this.provider.ldapConfig;
|
||
|
|
if (!config) {
|
||
|
|
throw new Error('LDAP config not found');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Escape username to prevent LDAP injection
|
||
|
|
const escapedUsername = this.escapeLdap(username);
|
||
|
|
|
||
|
|
// Build user search filter
|
||
|
|
const userFilter = config.userSearchFilter.replace('{{username}}', escapedUsername);
|
||
|
|
|
||
|
|
// Decrypt bind password
|
||
|
|
const bindPassword = await this.cryptoService.decrypt(config.bindPasswordEncrypted);
|
||
|
|
|
||
|
|
// Perform LDAP authentication
|
||
|
|
// This is a placeholder - actual implementation would use an LDAP library
|
||
|
|
const userEntry = await this.ldapBind(
|
||
|
|
config.serverUrl,
|
||
|
|
config.bindDn,
|
||
|
|
bindPassword,
|
||
|
|
config.baseDn,
|
||
|
|
userFilter,
|
||
|
|
password
|
||
|
|
);
|
||
|
|
|
||
|
|
// Map LDAP attributes to user info
|
||
|
|
return this.mapAttributes(userEntry);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Test LDAP connection
|
||
|
|
*/
|
||
|
|
public async testConnection(): Promise<IConnectionTestResult> {
|
||
|
|
const start = Date.now();
|
||
|
|
const config = this.provider.ldapConfig;
|
||
|
|
|
||
|
|
if (!config) {
|
||
|
|
return {
|
||
|
|
success: false,
|
||
|
|
latencyMs: Date.now() - start,
|
||
|
|
error: 'LDAP config not found',
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
// Decrypt bind password
|
||
|
|
const bindPassword = await this.cryptoService.decrypt(config.bindPasswordEncrypted);
|
||
|
|
|
||
|
|
// Test connection by binding with service account
|
||
|
|
await this.testLdapConnection(
|
||
|
|
config.serverUrl,
|
||
|
|
config.bindDn,
|
||
|
|
bindPassword,
|
||
|
|
config.baseDn
|
||
|
|
);
|
||
|
|
|
||
|
|
return {
|
||
|
|
success: true,
|
||
|
|
latencyMs: Date.now() - start,
|
||
|
|
serverInfo: {
|
||
|
|
serverUrl: config.serverUrl,
|
||
|
|
baseDn: config.baseDn,
|
||
|
|
tlsEnabled: config.tlsEnabled,
|
||
|
|
},
|
||
|
|
};
|
||
|
|
} catch (error) {
|
||
|
|
return {
|
||
|
|
success: false,
|
||
|
|
latencyMs: Date.now() - start,
|
||
|
|
error: error instanceof Error ? error.message : String(error),
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Escape special LDAP characters to prevent injection
|
||
|
|
*/
|
||
|
|
private escapeLdap(value: string): string {
|
||
|
|
return value
|
||
|
|
.replace(/\\/g, '\\5c')
|
||
|
|
.replace(/\*/g, '\\2a')
|
||
|
|
.replace(/\(/g, '\\28')
|
||
|
|
.replace(/\)/g, '\\29')
|
||
|
|
.replace(/\x00/g, '\\00');
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Perform LDAP bind and search
|
||
|
|
* This is a placeholder implementation - actual LDAP would require a library
|
||
|
|
*/
|
||
|
|
private async ldapBind(
|
||
|
|
serverUrl: string,
|
||
|
|
bindDn: string,
|
||
|
|
bindPassword: string,
|
||
|
|
baseDn: string,
|
||
|
|
userFilter: string,
|
||
|
|
userPassword: string
|
||
|
|
): Promise<ILdapEntry> {
|
||
|
|
// In a real implementation, this would:
|
||
|
|
// 1. Connect to LDAP server
|
||
|
|
// 2. Bind with service account (bindDn/bindPassword)
|
||
|
|
// 3. Search for user with userFilter
|
||
|
|
// 4. Re-bind with user's DN and password to verify
|
||
|
|
// 5. Return user entry if successful
|
||
|
|
|
||
|
|
// For now, we throw an error indicating LDAP needs to be configured
|
||
|
|
// This allows the structure to be in place while the actual LDAP library
|
||
|
|
// integration can be done separately
|
||
|
|
|
||
|
|
console.log('[LdapStrategy] LDAP auth attempt:', {
|
||
|
|
serverUrl,
|
||
|
|
baseDn,
|
||
|
|
userFilter,
|
||
|
|
});
|
||
|
|
|
||
|
|
throw new Error(
|
||
|
|
'LDAP authentication is not yet fully implemented. ' +
|
||
|
|
'Please integrate with a Deno-compatible LDAP library (e.g., ldapts via npm compatibility).'
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Test LDAP connection
|
||
|
|
*/
|
||
|
|
private async testLdapConnection(
|
||
|
|
serverUrl: string,
|
||
|
|
bindDn: string,
|
||
|
|
bindPassword: string,
|
||
|
|
baseDn: string
|
||
|
|
): Promise<void> {
|
||
|
|
// Similar to ldapBind, this is a placeholder
|
||
|
|
// Would connect and bind with service account to verify connectivity
|
||
|
|
|
||
|
|
console.log('[LdapStrategy] Testing LDAP connection:', {
|
||
|
|
serverUrl,
|
||
|
|
bindDn,
|
||
|
|
baseDn,
|
||
|
|
});
|
||
|
|
|
||
|
|
// For now, check if server URL is valid
|
||
|
|
if (!serverUrl.startsWith('ldap://') && !serverUrl.startsWith('ldaps://')) {
|
||
|
|
throw new Error('Invalid LDAP server URL. Must start with ldap:// or ldaps://');
|
||
|
|
}
|
||
|
|
|
||
|
|
// In a real implementation, we would actually connect here
|
||
|
|
// For now, we just validate the configuration
|
||
|
|
if (!bindDn || !bindPassword || !baseDn) {
|
||
|
|
throw new Error('Missing required LDAP configuration');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Return success for configuration validation
|
||
|
|
// Actual connectivity test would happen with LDAP library
|
||
|
|
console.log('[LdapStrategy] LDAP configuration is valid (actual connection test requires LDAP library)');
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Map LDAP attributes to standard user info
|
||
|
|
*/
|
||
|
|
private mapAttributes(entry: ILdapEntry): IExternalUserInfo {
|
||
|
|
const mapping = this.provider.attributeMapping;
|
||
|
|
|
||
|
|
// Get external ID (typically uid or sAMAccountName)
|
||
|
|
const externalId = String(entry[mapping.username] || entry.dn);
|
||
|
|
|
||
|
|
// Get email
|
||
|
|
const email = entry[mapping.email];
|
||
|
|
if (!email || typeof email !== 'string') {
|
||
|
|
throw new Error('Email not found in LDAP entry');
|
||
|
|
}
|
||
|
|
|
||
|
|
return {
|
||
|
|
externalId,
|
||
|
|
email,
|
||
|
|
username: entry[mapping.username]
|
||
|
|
? String(entry[mapping.username])
|
||
|
|
: undefined,
|
||
|
|
displayName: entry[mapping.displayName]
|
||
|
|
? String(entry[mapping.displayName])
|
||
|
|
: undefined,
|
||
|
|
groups: mapping.groups
|
||
|
|
? this.parseGroups(entry[mapping.groups])
|
||
|
|
: undefined,
|
||
|
|
rawAttributes: entry as Record<string, unknown>,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Parse LDAP group membership
|
||
|
|
*/
|
||
|
|
private parseGroups(memberOf: unknown): string[] {
|
||
|
|
if (!memberOf) return [];
|
||
|
|
|
||
|
|
if (Array.isArray(memberOf)) {
|
||
|
|
return memberOf.map((dn) => this.extractCnFromDn(String(dn)));
|
||
|
|
}
|
||
|
|
|
||
|
|
return [this.extractCnFromDn(String(memberOf))];
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Extract CN (Common Name) from a DN (Distinguished Name)
|
||
|
|
*/
|
||
|
|
private extractCnFromDn(dn: string): string {
|
||
|
|
const match = dn.match(/^CN=([^,]+)/i);
|
||
|
|
return match ? match[1] : dn;
|
||
|
|
}
|
||
|
|
}
|