feat(portal-bff): /auth/login route — pkce flow start + signed cookie
Third step of ADR-0009 wiring. Adds the first OIDC route:
`GET /api/auth/login` 302s to Entra's authorize endpoint with a
freshly-generated state + PKCE challenge, and stashes the matching
`{state, codeVerifier}` payload in a short-lived signed cookie so
the next-PR callback can verify the round-trip.
What lands:
- `cookie-parser` + `@types/express` added as deps. main.ts mounts
the cookie parser middleware with the `SESSION_SECRET` signing
key — signed cookies become available via `req.signedCookies` for
the upcoming callback.
- `.env.example` promotes `SESSION_SECRET` from a future-vars
comment into an active variable, with a one-liner showing how to
generate a 32-byte base64url value.
- `apps/portal-bff/src/config/check-session-secret.ts` — same family
of boot-time guard as `check-database-url.ts` /
`check-entra-config.ts`: refuses to start if `SESSION_SECRET` is
unset, still the .env.example placeholder, or decodes below 32
bytes of entropy.
- `apps/portal-bff/src/auth/auth.service.ts` —
`AuthService.beginAuthCodeFlow()` uses MSAL Node's `CryptoProvider`
for canonical PKCE verifier / challenge generation and state
nonce (fresh GUID per call), calls `msal.getAuthCodeUrl()` with
the configured redirect URI + OIDC scopes
(`openid profile email` — no `offline_access`; sessions are
short-lived and re-auth via Entra rather than refresh-token
shenanigans, per ADR-0010), and returns the URL + pre-auth
payload to the controller.
- `apps/portal-bff/src/auth/auth.cookie.ts` — pre-auth cookie name
(`portal_pre_auth`), 5-minute TTL, and shared `CookieOptions`:
`signed: true`, `httpOnly: true`, `sameSite: 'lax'` (allows
Entra's cross-site top-level redirect back), `secure` toggled by
`NODE_ENV`. The `__Host-` prefix waits for the prod TLS hardening
ADR.
- `apps/portal-bff/src/auth/auth.controller.ts` —
`@Controller('auth') GET login`: writes the cookie via Express
`res.cookie()` (using `@Res()`) then 302s to the auth URL. Thin
shell around `AuthService`.
- `AuthModule` registers the controller + service alongside the
existing `ENTRA_CONFIG` and `MSAL_CLIENT` providers.
Verification:
- `nx run-many -t lint test build --projects=portal-bff` — green.
- 39/39 specs (was 30; +9 across the new validator spec, service
spec, and controller spec).
- The service spec mocks MSAL's `getAuthCodeUrl` and asserts the
redirect URI / scopes / S256 method, the state-verifier identity
between the cookie payload and what's sent to Entra, and the
fresh-per-call replay protection.
- The controller spec asserts the cookie name + options + payload
serialization and the 302 redirect.
What this PR explicitly does NOT do:
- The callback. `GET /auth/callback` reads the cookie, exchanges
the code for tokens via `acquireTokenByCode`, validates the ID
token, and (next PR after) creates a session. If you click
`/auth/login` today, you'll round-trip through Entra and land on
a 404 — by design until the callback ships.
- Session persistence (waits on ADR-0010 Redis wiring).
- Logout, CSRF protection, route guards.
This commit is contained in:
Generated
+60
-1
@@ -116,6 +116,9 @@ importers:
|
||||
class-validator:
|
||||
specifier: ^0.15.1
|
||||
version: 0.15.1
|
||||
cookie-parser:
|
||||
specifier: ^1.4.7
|
||||
version: 1.4.7
|
||||
lucide-angular:
|
||||
specifier: ^1.0.0
|
||||
version: 1.0.0(@angular/common@21.2.12(@angular/core@21.2.12(@angular/compiler@21.2.12)(rxjs@7.8.2)(zone.js@0.16.2))(rxjs@7.8.2))(@angular/core@21.2.12(@angular/compiler@21.2.12)(rxjs@7.8.2)(zone.js@0.16.2))
|
||||
@@ -243,6 +246,12 @@ importers:
|
||||
'@tailwindcss/postcss':
|
||||
specifier: ^4.2.4
|
||||
version: 4.3.0
|
||||
'@types/cookie-parser':
|
||||
specifier: ^1.4.10
|
||||
version: 1.4.10(@types/express@5.0.6)
|
||||
'@types/express':
|
||||
specifier: ^5.0.6
|
||||
version: 5.0.6
|
||||
'@types/jest':
|
||||
specifier: ^30.0.0
|
||||
version: 30.0.0
|
||||
@@ -4270,6 +4279,11 @@ packages:
|
||||
'@types/connect@3.4.38':
|
||||
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
|
||||
|
||||
'@types/cookie-parser@1.4.10':
|
||||
resolution: {integrity: sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==}
|
||||
peerDependencies:
|
||||
'@types/express': '*'
|
||||
|
||||
'@types/deep-eql@4.0.2':
|
||||
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
|
||||
|
||||
@@ -4291,9 +4305,15 @@ packages:
|
||||
'@types/express-serve-static-core@4.19.8':
|
||||
resolution: {integrity: sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==}
|
||||
|
||||
'@types/express-serve-static-core@5.1.1':
|
||||
resolution: {integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==}
|
||||
|
||||
'@types/express@4.17.25':
|
||||
resolution: {integrity: sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==}
|
||||
|
||||
'@types/express@5.0.6':
|
||||
resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==}
|
||||
|
||||
'@types/http-errors@2.0.5':
|
||||
resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==}
|
||||
|
||||
@@ -4357,6 +4377,9 @@ packages:
|
||||
'@types/serve-static@1.15.10':
|
||||
resolution: {integrity: sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==}
|
||||
|
||||
'@types/serve-static@2.2.0':
|
||||
resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==}
|
||||
|
||||
'@types/sockjs@0.3.36':
|
||||
resolution: {integrity: sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==}
|
||||
|
||||
@@ -5422,6 +5445,13 @@ packages:
|
||||
convert-source-map@2.0.0:
|
||||
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
||||
|
||||
cookie-parser@1.4.7:
|
||||
resolution: {integrity: sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
cookie-signature@1.0.6:
|
||||
resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==}
|
||||
|
||||
cookie-signature@1.0.7:
|
||||
resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==}
|
||||
|
||||
@@ -14812,6 +14842,10 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/node': 24.12.4
|
||||
|
||||
'@types/cookie-parser@1.4.10(@types/express@5.0.6)':
|
||||
dependencies:
|
||||
'@types/express': 5.0.6
|
||||
|
||||
'@types/deep-eql@4.0.2': {}
|
||||
|
||||
'@types/eslint-scope@3.7.7':
|
||||
@@ -14839,6 +14873,13 @@ snapshots:
|
||||
'@types/range-parser': 1.2.7
|
||||
'@types/send': 1.2.1
|
||||
|
||||
'@types/express-serve-static-core@5.1.1':
|
||||
dependencies:
|
||||
'@types/node': 24.12.4
|
||||
'@types/qs': 6.15.0
|
||||
'@types/range-parser': 1.2.7
|
||||
'@types/send': 1.2.1
|
||||
|
||||
'@types/express@4.17.25':
|
||||
dependencies:
|
||||
'@types/body-parser': 1.19.6
|
||||
@@ -14846,6 +14887,12 @@ snapshots:
|
||||
'@types/qs': 6.15.0
|
||||
'@types/serve-static': 1.15.10
|
||||
|
||||
'@types/express@5.0.6':
|
||||
dependencies:
|
||||
'@types/body-parser': 1.19.6
|
||||
'@types/express-serve-static-core': 5.1.1
|
||||
'@types/serve-static': 2.2.0
|
||||
|
||||
'@types/http-errors@2.0.5': {}
|
||||
|
||||
'@types/http-proxy@1.17.17':
|
||||
@@ -14910,7 +14957,7 @@ snapshots:
|
||||
|
||||
'@types/serve-index@1.9.4':
|
||||
dependencies:
|
||||
'@types/express': 4.17.25
|
||||
'@types/express': 5.0.6
|
||||
|
||||
'@types/serve-static@1.15.10':
|
||||
dependencies:
|
||||
@@ -14918,6 +14965,11 @@ snapshots:
|
||||
'@types/node': 24.12.4
|
||||
'@types/send': 0.17.6
|
||||
|
||||
'@types/serve-static@2.2.0':
|
||||
dependencies:
|
||||
'@types/http-errors': 2.0.5
|
||||
'@types/node': 24.12.4
|
||||
|
||||
'@types/sockjs@0.3.36':
|
||||
dependencies:
|
||||
'@types/node': 24.12.4
|
||||
@@ -16084,6 +16136,13 @@ snapshots:
|
||||
|
||||
convert-source-map@2.0.0: {}
|
||||
|
||||
cookie-parser@1.4.7:
|
||||
dependencies:
|
||||
cookie: 0.7.2
|
||||
cookie-signature: 1.0.6
|
||||
|
||||
cookie-signature@1.0.6: {}
|
||||
|
||||
cookie-signature@1.0.7: {}
|
||||
|
||||
cookie-signature@1.2.2: {}
|
||||
|
||||
Reference in New Issue
Block a user