import crypto from "node:crypto"; import type { SanitizedConfig } from "payload"; import type { IAuthenticationService } from "../../application/services/authentication.service.interface"; import type { Cookie } from "../../entities/models/cookie"; import type { Session } from "../../entities/models/session"; import type { User } from "../../entities/models/user"; // --------------------------------------------------------------------------- // Deferred methods // --------------------------------------------------------------------------- // `createSession`, `validateSession`, and `invalidateSession` require Payload's // internal JWT-based auth session machinery, which does not map cleanly to a // generic session interface without deep integration with Payload's REST/local // API and cookie infrastructure. // // TODO: Implement these three methods once the session // cookie strategy is settled. Until then they throw NotImplementedError to // keep the production-shaped file in place without silently no-oping. // // The mock (`authentication.service.mock.ts`) handles all test paths. class NotImplementedError extends Error { constructor(method: string) { super(`NotImplemented: AuthenticationService.${method}`); this.name = "NotImplementedError"; } } const SALT_LENGTH = 16; const KEY_LENGTH = 64; const ITERATIONS = 100_000; const DIGEST = "sha512"; const SEPARATOR = ":"; export class AuthenticationService implements IAuthenticationService { constructor(private _config: SanitizedConfig) {} generateUserId(): string { return crypto.randomUUID(); } async hashPassword(password: string): Promise { const salt = crypto.randomBytes(SALT_LENGTH).toString("hex"); const hash = await new Promise((resolve, reject) => { crypto.pbkdf2( password, salt, ITERATIONS, KEY_LENGTH, DIGEST, (err, derivedKey) => { if (err) reject(err); else resolve(derivedKey.toString("hex")); }, ); }); return `${salt}${SEPARATOR}${hash}`; } async verifyPassword(storedHash: string, password: string): Promise { const parts = storedHash.split(SEPARATOR); if (parts.length !== 2) return false; const salt = parts[0]!; const expectedHash = parts[1]!; const actualHash = await new Promise((resolve, reject) => { crypto.pbkdf2( password, salt, ITERATIONS, KEY_LENGTH, DIGEST, (err, derivedKey) => { if (err) reject(err); else resolve(derivedKey.toString("hex")); }, ); }); return crypto.timingSafeEqual( Buffer.from(expectedHash, "hex"), Buffer.from(actualHash, "hex"), ); } // TODO: Implement using Payload's local.login / JWT session issuance. // Payload creates sessions via its REST auth endpoint; mapping that to a // generic { session: Session; cookie: Cookie } shape requires understanding // the JWT payload structure and the cookie name/attributes Payload uses. async createSession( _user: User, ): Promise<{ session: Session; cookie: Cookie }> { throw new NotImplementedError("createSession"); } // TODO: Implement using Payload's JWT verify mechanism. // Need to call Payload's local API to verify the token and retrieve the user. async validateSession( _sessionId: string, ): Promise<{ user: User; session: Session }> { throw new NotImplementedError("validateSession"); } // TODO: Implement by clearing the session token. // Payload does not have a server-side session store by default; invalidation // is typically done client-side by clearing the cookie. async invalidateSession( _sessionId: string, ): Promise<{ blankCookie: Cookie }> { throw new NotImplementedError("invalidateSession"); } }