import { ConfidentialClientApplication, LogLevel } from '@azure/msal-node'; import { Module } from '@nestjs/common'; import { Logger } from 'nestjs-pino'; import { assertEntraConfig } from '../config/check-entra-config'; import { SessionModule } from '../session/session.module'; import { AuthController } from './auth.controller'; import { AuthService } from './auth.service'; import { ENTRA_CONFIG, type EntraConfig } from './entra-config.token'; import { MSAL_CLIENT } from './msal-client.token'; /** * Auth module — owns the Entra ID configuration, the MSAL Node * confidential client, and (in subsequent PRs) the OIDC routes, the * session integration, and the route guards. Per ADR-0009. * * v1 providers: * * - `ENTRA_CONFIG` — the parsed, validated Entra app-registration * config (PR #102). * - `MSAL_CLIENT` — a `ConfidentialClientApplication` instance * wired with that config plus a logger callback that forwards * MSAL's internal log lines into the Pino stream so auth flow * diagnostics land alongside the rest of the BFF logs (per * ADR-0012). PII logging is disabled by default — MSAL won't * include tokens or user identifiers in the messages it emits. * - `AuthService` — first-leg-of-the-flow logic: PKCE + state * generation, MSAL auth-code URL building. The controller * stays a thin shell around it. * * v1 routes (per ADR-0009): * * - `GET /api/auth/login` — 302 to Entra's authorize endpoint * with the freshly-generated state + code challenge; sets a * short-lived signed cookie carrying the state + PKCE * verifier so the next-PR callback can verify the round-trip. * * The module stays non-global: modules state "I depend on auth" by * importing it. Re-exports both tokens so a single * `imports: [AuthModule]` is enough to consume either. */ @Module({ imports: [SessionModule], controllers: [AuthController], providers: [ AuthService, { provide: ENTRA_CONFIG, useFactory: () => assertEntraConfig(), }, { provide: MSAL_CLIENT, inject: [ENTRA_CONFIG, Logger], useFactory: (config: EntraConfig, logger: Logger) => new ConfidentialClientApplication({ auth: { clientId: config.clientId, authority: config.authority, clientSecret: config.clientSecret, }, system: { loggerOptions: { piiLoggingEnabled: false, logLevel: LogLevel.Info, loggerCallback: (level, message) => { // MSAL levels: 0=Error, 1=Warning, 2=Info, 3=Verbose, 4=Trace. // Forward to Pino with the matching level. The whole // payload lives under the `msal` context key so a log // search filtering on `msal` returns every MSAL line // without false positives from app code. const ctx = 'msal'; switch (level) { case LogLevel.Error: logger.error(message, ctx); break; case LogLevel.Warning: logger.warn(message, ctx); break; case LogLevel.Verbose: case LogLevel.Trace: logger.debug(message, ctx); break; default: logger.log(message, ctx); } }, }, }, }), }, ], exports: [ENTRA_CONFIG, MSAL_CLIENT], }) export class AuthModule {}