Julien Gautier 18f26dc2b9
CI / scan (pull_request) Successful in 2m55s
CI / commits (pull_request) Successful in 2m56s
CI / check (pull_request) Successful in 3m7s
CI / a11y (pull_request) Successful in 1m55s
CI / perf (pull_request) Successful in 5m47s
feat(portal-bff): obo strategy + encrypted downstream token cache
First half of the DownstreamApiClient + OBO chantier per ADR-0014.
Per the ADR's own §"Consequences" note — "writing the framework code
only in the same iteration as the first concrete integration; until
then, this ADR plus mock-driven unit tests on the strategies (OBO,
signed-assertion) keep the design honest" — this PR ships the OBO
strategy and its encrypted-at-rest token cache as testable primitives,
NOT the full DownstreamApiClientFactory + cockatiel + audience
pre-check framework. The framework gets assembled when the first real
downstream integration arrives.

What lands

- assertOboCacheEncryptionKey (config/check-obo-cache-encryption-key.ts):
  - 32-byte AES-256-GCM key validator, mirrors the SESSION key
    validator pattern.
  - Refuses placeholder, wrong length, non-base64url.
  - Refuses a value identical to SESSION_ENCRYPTION_KEY — defense
    in depth against the copy-paste regression that would silently
    downgrade the trust boundary ADR-0014 §"Token cache (for OBO)"
    explicitly draws.
  - Wired in main.ts alongside the other assertX() boot validators.

- DownstreamTokenCache (downstream/downstream-token-cache.service.ts):
  - Redis-backed cache, key shape `obo:{actorIdHash}:{resource}`.
  - Encrypts each entry via the shared AES-256-GCM helpers from
    `session-crypto` but under a DEDICATED key (OBO_CACHE_KEY) so
    a cache-key leak cannot decrypt sessions and vice versa.
  - TTL = upstream `expiresAt - 60s` (the safety buffer ADR-0014
    mandates). A token already inside the buffer is NOT written —
    the strategy re-acquires next call.
  - Never throws on read: Redis hiccups, tampered ciphertext, wrong
    key, malformed shape — all collapse to a miss with a structured
    Pino warn. The strategy then re-acquires from Entra.
  - Writes are best-effort: a Redis write failure is logged but
    doesn't fail the request — the call already has its token.

- OboStrategy (downstream/strategies/obo.strategy.ts):
  - Wraps MSAL Node's acquireTokenOnBehalfOf with the cache.
  - Flow: cache.get → if fresh, return verbatim; else MSAL call →
    cache.set → return.
  - Cache hit applies a second 60 s buffer beyond Redis's TTL —
    load-bearing under clock skew where a token survives Redis's
    expiry but is already inside its safety window.
  - MSAL refusal / null result throws OboAcquireError with a typed
    `reason` discriminator the future framework will translate to a
    502 + `auth.token.validation.failed` audit event per ADR-0014.
  - Takes the user's access token as a PARAMETER for now — ADR-0014
    says "read from session via CLS", but v1 sessions don't
    persist the user's access token (ADR-0009 omits offline_access
    deliberately). The framework integration that fetches it from
    CLS lands with the first real consumer, per the ADR's own
    "until then" clause.

- DownstreamModule (downstream/downstream.module.ts): provides
  OBO_CACHE_KEY (via assertOboCacheEncryptionKey at factory time),
  DownstreamTokenCache, OboStrategy. Imports AuthModule for the
  shared MSAL_CLIENT and RedisModule for the shared ioredis. Wired
  into AppModule though the strategy has no runtime consumer yet —
  the registration makes the strategy injectable for the future
  integration without that integration having to also touch the
  module graph.

Env

- New: OBO_CACHE_ENCRYPTION_KEY (mandatory at boot).
- .env.example placeholder + the stale "future" note collapsed to
  point at the now-wired key.

Tests: +26 specs (env validator 8, token cache 9, OBO strategy 9).

Out of scope (deferred per ADR-0014 "until then"):
- DownstreamApiClientFactory + per-service typed config.
- cockatiel resilience composition (timeout, retry, breaker, bulkhead).
- Audience pre-check at the call site.
- Error translation tables.
- OTel custom spans `downstream.<service>.<verb>.<path>`.
- audit events `auth.token.validation.failed` / `authz.deny`.
- The framework wiring that reads the user access token from
  session/CLS instead of accepting it as a parameter.

These land alongside the first concrete integration so the framework
shape is validated against a real consumer, not speculative.
2026-05-14 18:08:22 +02:00

Documentation index

This is the entry point to all project documentation. It is maintained automatically: any addition, rename, or removal of a .md file under docs/ must be reflected here in the same change.

Conventions

  • Documentation is written in English.
  • One topic per file. Group related files into a folder when there are three or more.
  • Cross-reference with relative links so they keep working in GitHub, IDEs, and exported sites.
  • For architectural decisions, do not add them here — they belong in decisions/ as MADR 4.0.0 ADRs.

Sections

Daily development

  • development.md — repo layout, prerequisites, initial setup, daily commands, observability dev-loop (Jaeger UI, log ↔ trace correlation), dependency updates (Renovate), conventional commit cycle. Day-to-day reference for working on the project.

Architecture

  • architecture.md — cross-cutting Mermaid diagrams: C4 system context, C4 containers, Nx module boundaries, CI/CD pipeline. Single-decision diagrams (auth sequence, ERD, etc.) live inline in their ADR.

Onboarding & environment

Setup guides for new contributors:

Operations & runbooks

Empty — to be populated when we deploy.

Security, performance, accessibility

Empty — placeholders to be filled with rationale docs alongside their corresponding ADRs.

S
Description
No description provided
Readme 42 MiB
Languages
TypeScript 85.3%
JavaScript 5.4%
SCSS 4.3%
HTML 3.9%
Shell 0.8%
Other 0.3%