/** * 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 { 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 { 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 { // 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 { // 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, }; } /** * 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; } }