key-cape/workplans/ADHOC-2026-09-27.md

102 lines
5 KiB
Markdown
Raw Permalink Normal View History

---
id: ADHOC-2026-09-27
type: workplan
title: "Fresh MFA no longer backdated to a stale kc_login cookie (INFD-IN-0005)"
domain: infotech
repo: key-cape
status: finished
owner: claude
created: "2026-09-27"
updated: "2026-09-27"
state_hub_workstream_id: "5217dd06-73cb-50f8-ba64-528e421b29de"
---
## Stop completeAuthorization from reusing a stale prior session's IssuedAt after real MFA
```task
id: ADHOC-2026-09-27-T01
status: done
priority: high
state_hub_task_id: "ce98e4af-cfaa-5620-8f21-15ade18ee9e8"
```
`informed-decision` intake `INFD-IN-0005` (open, `origin_ref: SECRETS-WP-0010`):
platform-root's approval of `infd-20260927-b01` was refused by flex-auth's
`compact_sitting` policy with `policy_denied`, despite platform-root having just
completed a fresh key-cape MFA sign-in. flex-auth's 900-second review-freshness
check reads `assurance.at`, which key-cape derives from the PKCE session's
`AuthTime`.
Root cause in `src/internal/server/oidc/authorize.go`,
`AuthorizeHandler.completeAuthorization`: whenever a `kc_login` cookie already
existed for the same username, its `IssuedAt` was unconditionally reused as
`authTime` — even on the two call sites that had just called
`MFA.ValidateMFAToken` successfully in this exact request (the callback's
`mfaToken != ""` branch and `serveMFASubmission`). A user who had an older
browser session and then stepped up with a fresh MFA challenge got the *old*
session's timestamp stamped onto the *new*, just-proven authentication —
understating freshness for every downstream binding-grade check, exactly as
`INFD-IN-0005` describes ("completeAuthorization currently copies a prior
timestamp... only after actual reauthentication").
The reuse itself is correct when MFA was **not** just performed in this
request (`decision.MFAVerified` carried forward from an existing AAL2 session,
via `domain.DecideAssurance`) — reporting mint time there would overstate
freshness (`TestAssuranceReportsAuthenticationTimeNotMintTime`). The two cases
were conflated under a single `mfaVerified` bool.
Fix: added a `freshMFA bool` parameter to `completeAuthorization`, `true` only
at the two call sites that just validated an MFA token in this request, `false`
at the carried-forward-assurance call site. `authTime` now falls back to the
prior session's `IssuedAt` only when `!freshMFA`.
Added `TestAuthorizeCallback_MFASubmission_FreshEvenWithStalePriorLoginCookie`,
which seeds a 4-hour-old `kc_login` cookie for the same user, then submits a
valid MFA token and asserts the resulting `AuthTime` is within a minute of now.
Confirmed it fails against the pre-fix code (`AuthTime` ~4h old) and passes
after. Full suite (`go test ./...`) green, no other test needed adjustment —
the carried-forward-assurance case (`TestPolicy_...`, `TestLogout_...`,
`assurance_time_test.go`) is unaffected since none of it exercises a fresh MFA
submission with a pre-existing cookie for the same user.
Does not touch flex-auth (its 900s window is correct; it was consuming a bad
timestamp) or informed-decision (correctly refuses to interpret the PDP's
denial). `INFD-IN-0005` can close once informed-decision confirms platform-root
can approve after a fresh sign-in against this build.
## Discovery/JWKS fetch was missing the split-horizon origin headers
```task
id: ADHOC-2026-09-27-T02
status: done
priority: high
state_hub_task_id: "7fa0bfc7-0772-57c7-8d6f-641615cab82a"
```
Rolling out T01 (image digest `sha256:248e449a...`) broke every KeyCape login,
including unrelated clients (`user-engine-portal`), with `auth_failure` /
`provider_metadata_unavailable` at `/api/oidc/token` on the KeyCape side and
Authelia logging `invalid X-Forwarded-Proto header value 'http'` on
`GET /.well-known/openid-configuration` from KeyCape's pod IP.
Root cause, unrelated to T01's change: `AutheliaAdapter.setPublicOriginHeaders`
(the NK-WP-0041-T02/path-B fix, key-cape@3b0446e) annotates only the token
exchange (`newTokenRequest`) with `X-Forwarded-Proto`/`X-Forwarded-Host` so
Authelia can derive the public issuer for a split-horizon (in-cluster HTTP,
public HTTPS) deployment. `idTokenVerifier.get` in `verify.go` — used for the
provider-metadata and JWKS fetches that verify every upstream ID token — never
got the same treatment; it built a bare request with only an `Accept` header.
Authelia 4.38 fails closed on the resulting unmarked in-cluster request. This
gap predates today's session (present since 3b0446e); it surfaced now because
this was the first login attempted since that image went live.
Fix: extracted `setPublicOriginHeaders` from an `AutheliaAdapter` method to a
package-level function taking the target public base URL explicitly, and gave
`idTokenVerifier` its own resolved `publicBaseURL` (same
`BrowserBaseURL`-else-`BaseURL` fallback as the adapter) so `get()` applies
the same headers. Added `TestDiscoveryFetch_CarriesPublicOriginHeaders`
(confirmed it fails without the fix, passes with it). Full suite green.
New image `key-cape@<pending>` still needs a rollout to `sso-mfa/k8s/keycape/deployment.yaml`
before platform-root can sign in again.