feat(portal-bff): msal confidential client provider in AuthModule #104

Merged
julien merged 1 commits from feat/portal-bff/auth-msal-client into main 2026-05-12 10:34:24 +02:00
Owner

Summary

Second step of ADR-0009 wiring. AuthModule now exposes the @azure/msal-node confidential client alongside the parsed Entra config from PR #102 — the building block the upcoming OIDC routes inject to issue the auth-code URL, exchange the callback code for tokens, and acquire downstream tokens on behalf of the user.

What lands

  • @azure/msal-node added as a direct dependency (^5.2.1).
  • msal-client.token.tsMSAL_CLIENT string token + ConfidentialClientApplication type re-export. Same shape as the ENTRA_CONFIG token.
  • auth.module.ts grows a factory provider for MSAL_CLIENT:
    • Injects ENTRA_CONFIG + nestjs-pino Logger.
    • Builds a ConfidentialClientApplication with clientId, authority, clientSecret from the parsed config.
    • Wires system.loggerOptions.loggerCallback to forward MSAL's internal log lines into the Pino stream (per ADR-0012) — Error → logger.error, Warning → logger.warn, Verbose / Trace → logger.debug, Info → logger.log. PII logging disabled so tokens / user identifiers never leak.
    • MSAL logLevel set to Info; Pino's own threshold re-filters from there.
    • Every MSAL log line carries the msal Pino context for log-query isolation.
  • AuthModule.exports extended to include MSAL_CLIENT.

Decisions worth flagging

  • Logger injection in tests. The new spec imports nestjs-pino's LoggerModule.forRoot({ pinoHttp: { level: 'silent' } }) to register the same Logger provider the production app exposes via ObservabilityModule, without flooding test stdout. Adding a stub to the TestingModule's providers does not work — Nest scoping rules mean providers declared at the test-module level are not visible to imported modules' factories.
  • Eager construction. MSAL Node defers authority discovery to the first auth call, so the client is built eagerly at module init with no network round-trip. No reason to lazy-build.
  • No public service wrapper yet (e.g. no EntraAuthService exposing buildAuthCodeUrl() etc.). Lands with the routes PR — premature abstraction otherwise.
  • PII logging stays off. ADR-0012 prefers structured logs without sensitive payloads. If we ever need to debug a token exchange end-to-end, we'll flip a per-environment env-var-gated escape hatch, not the default.

Verification

  • nx run-many -t lint test build --projects=portal-bff — green.
  • 30/30 specs (was 29; +1 asserting the ConfidentialClientApplication is constructed).
  • The existing missing-env failure mode still propagates through the new factory chain.

Next PRs on the auth track

  1. OIDC routes: /api/auth/login (PKCE-initiated redirect to Entra), /api/auth/callback (token exchange + ID-token validation: issuer, audience, signature, exp, nonce, amr).
  2. Session persistence per ADR-0010 (Redis + AES-GCM, __Host-portal_session cookie). Closes the auth loop.
  3. RP-initiated logout, CSRF protection, route guards (@RequireMfa, @RequireRole('admin')).
## Summary Second step of ADR-0009 wiring. `AuthModule` now exposes the `@azure/msal-node` confidential client alongside the parsed Entra config from PR #102 — the building block the upcoming OIDC routes inject to issue the auth-code URL, exchange the callback code for tokens, and acquire downstream tokens on behalf of the user. ## What lands - **`@azure/msal-node` added** as a direct dependency (^5.2.1). - **[`msal-client.token.ts`](apps/portal-bff/src/auth/msal-client.token.ts)** — `MSAL_CLIENT` string token + `ConfidentialClientApplication` type re-export. Same shape as the `ENTRA_CONFIG` token. - **[`auth.module.ts`](apps/portal-bff/src/auth/auth.module.ts)** grows a factory provider for `MSAL_CLIENT`: - Injects `ENTRA_CONFIG` + nestjs-pino `Logger`. - Builds a `ConfidentialClientApplication` with `clientId`, `authority`, `clientSecret` from the parsed config. - Wires `system.loggerOptions.loggerCallback` to forward MSAL's internal log lines into the Pino stream (per ADR-0012) — Error → `logger.error`, Warning → `logger.warn`, Verbose / Trace → `logger.debug`, Info → `logger.log`. **PII logging disabled** so tokens / user identifiers never leak. - MSAL `logLevel` set to `Info`; Pino's own threshold re-filters from there. - Every MSAL log line carries the `msal` Pino context for log-query isolation. - `AuthModule.exports` extended to include `MSAL_CLIENT`. ## Decisions worth flagging - **Logger injection in tests.** The new spec imports `nestjs-pino`'s `LoggerModule.forRoot({ pinoHttp: { level: 'silent' } })` to register the same `Logger` provider the production app exposes via `ObservabilityModule`, without flooding test stdout. Adding a stub to the TestingModule's `providers` does not work — Nest scoping rules mean providers declared at the test-module level are not visible to imported modules' factories. - **Eager construction.** MSAL Node defers authority discovery to the first auth call, so the client is built eagerly at module init with no network round-trip. No reason to lazy-build. - **No public service wrapper yet** (e.g. no `EntraAuthService` exposing `buildAuthCodeUrl()` etc.). Lands with the routes PR — premature abstraction otherwise. - **PII logging stays off.** ADR-0012 prefers structured logs without sensitive payloads. If we ever need to debug a token exchange end-to-end, we'll flip a per-environment env-var-gated escape hatch, not the default. ## Verification - `nx run-many -t lint test build --projects=portal-bff` — green. - **30/30 specs** (was 29; +1 asserting the `ConfidentialClientApplication` is constructed). - The existing missing-env failure mode still propagates through the new factory chain. ## Next PRs on the auth track 1. **OIDC routes**: `/api/auth/login` (PKCE-initiated redirect to Entra), `/api/auth/callback` (token exchange + ID-token validation: issuer, audience, signature, exp, nonce, amr). 2. **Session persistence** per ADR-0010 (Redis + AES-GCM, `__Host-portal_session` cookie). Closes the auth loop. 3. RP-initiated logout, CSRF protection, route guards (`@RequireMfa`, `@RequireRole('admin')`).
julien added 1 commit 2026-05-12 10:32:24 +02:00
feat(portal-bff): msal confidential client provider in AuthModule
CI / commits (pull_request) Successful in 2m13s
CI / scan (pull_request) Successful in 2m35s
CI / check (pull_request) Successful in 3m23s
CI / a11y (pull_request) Successful in 1m40s
CI / perf (pull_request) Successful in 4m46s
aeda239b1c
Second step of ADR-0009 wiring. AuthModule now exposes the
`@azure/msal-node` confidential client alongside the parsed Entra
config — the building block the upcoming OIDC routes inject to issue
the auth-code URL, exchange the callback code for tokens, and
acquire downstream tokens on behalf of the user.

What lands:

- `@azure/msal-node` added as a direct dependency (^5.2.1).
- `apps/portal-bff/src/auth/msal-client.token.ts` — `MSAL_CLIENT`
  string token + `ConfidentialClientApplication` type re-export.
  Mirrors the `ENTRA_CONFIG` token shape from PR #102.
- AuthModule grows a factory provider for `MSAL_CLIENT`:
  - Injects `ENTRA_CONFIG` + nestjs-pino `Logger`.
  - Builds a `ConfidentialClientApplication` with `clientId`,
    `authority`, `clientSecret` from the parsed config.
  - Wires `system.loggerOptions.loggerCallback` to forward MSAL's
    internal log lines into the Pino stream (per ADR-0012) — Error
    → logger.error, Warning → logger.warn, Verbose / Trace →
    logger.debug, Info → logger.log. PII logging is disabled by
    default so tokens / user identifiers never leak into our
    structured log records.
  - Sets MSAL's `logLevel` to Info — Pino's own threshold
    re-filters from there.
  - All MSAL log lines carry the `msal` Pino context for easy
    isolation in log queries.
- `AuthModule.exports` extended to include `MSAL_CLIENT`.

Verification:

- `nx run-many -t lint test build --projects=portal-bff` — green.
- 30/30 specs (was 29; +1 covering MSAL client construction).
- New spec imports `nestjs-pino`'s `LoggerModule.forRoot({ pinoHttp:
  { level: 'silent' } })` to provide the same Logger the production
  app supplies via `ObservabilityModule`, without flooding test
  stdout. Two tests assert the provider tree resolves correctly
  (ENTRA_CONFIG + MSAL_CLIENT) and one re-checks the missing-env
  failure mode still propagates through the new factory.

Construction is cheap — MSAL Node defers authority discovery to the
first auth call — so the client is built eagerly at module init.
The factory is injection-only; no MSAL methods get invoked yet.
Routes land in the next PR.
julien merged commit b7093d61de into main 2026-05-12 10:34:24 +02:00
julien deleted branch feat/portal-bff/auth-msal-client 2026-05-12 10:34:26 +02:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: julien/apf_portal#104