All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 33s
Completes KEY-WP-0019. Client.Verify now delegates JWK-set parsing and RS256 signature checking to internal/jose, so the caller path and upstream provider verification share one implementation rather than two copies that drift. Its claim policy stays put: audience and nonce bindings belong to the caller. The existing authclient tests pass unchanged, which is the evidence the migration preserved behaviour. One deliberate strictness increase: a key set containing any malformed RSA signing key is refused outright rather than used alongside a good key. KeyCape's /jwks publishes a single key, so no current deployment is affected. Adds direct tests for internal/jose. It is now the single verifier behind both paths, and testing it only through its callers would leave duplicate key ids, crit headers, even exponents and undersized moduli covered by accident. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NV9oijZukGyGbRQGGKnK4P Assistant: claude-code Assistant-Model: opus Assistant-Process: 713576@bnt-lap001 Assistant-Session: 384c511d-9bce-4cb8-a676-2aef6c0c8df6
145 lines
6.3 KiB
Markdown
145 lines
6.3 KiB
Markdown
---
|
|
id: KEY-WP-0019
|
|
type: workplan
|
|
title: "Verify upstream Authelia ID tokens"
|
|
domain: infotech
|
|
repo: key-cape
|
|
status: finished
|
|
owner: claude
|
|
topic_slug: upstream-provider-token-verification
|
|
created: "2026-09-07"
|
|
updated: "2026-09-07"
|
|
state_hub_workstream_id: "87ea18ea-dc60-52ce-baf4-476b46aa06f9"
|
|
---
|
|
|
|
Closes the remaining half of gap G01 in
|
|
`history/2026-09-05-011726-scope-intent-assessment.md`. The Authelia adapter
|
|
decodes upstream ID-token claims without verifying the signature, issuer,
|
|
audience or expiry, justified in a comment by a server-to-server TLS boundary.
|
|
|
|
**Operator decision (2026-09-07, Bernd):** verify the token itself. The
|
|
KeyCape→Authelia hop is to be HTTPS as defence in depth, but KeyCape will not
|
|
monitor, check or gate on that — transport enforcement is friction without
|
|
protection value here, and a check that must be configured correctly to help is
|
|
itself a failure mode. Verification is chosen precisely because it does not
|
|
depend on the transport being what we believe it is. No HTTPS validation, opt-in
|
|
flag or transport telemetry is to be added.
|
|
|
|
## Extract a shared RS256/JWKS verifier
|
|
|
|
```task
|
|
id: KEY-WP-0019-T01
|
|
status: done
|
|
priority: high
|
|
state_hub_task_id: "68c1b099-96b4-5065-a4be-8592438a0ca9"
|
|
```
|
|
|
|
`internal/authclient` already contains a strict RS256 verifier — kid required,
|
|
no `crit`, RSA-only keys of at least 2048 bits with an odd exponent. The upstream
|
|
verification needs the same rules. Duplicating security-critical verification is
|
|
how two copies drift and one silently misses a fix, so extract the reusable half
|
|
into `internal/jose`: strict JWK-set parsing and signature verification returning
|
|
claims. Claim policy stays with each caller, whose audience, nonce and issuer
|
|
rules genuinely differ.
|
|
|
|
Added `internal/jose`: strict JWK-set parsing (RSA signing keys only, at least
|
|
2048 bits, odd exponent 3 or greater, no duplicate key ids) and RS256 signature
|
|
verification returning claims. All failures collapse to one `ErrVerification`
|
|
so a caller cannot leak which check failed back to whoever supplied the token.
|
|
`authclient` is not yet migrated onto it — see T05.
|
|
|
|
## Verify the Authelia ID token
|
|
|
|
```task
|
|
id: KEY-WP-0019-T02
|
|
status: done
|
|
priority: high
|
|
state_hub_task_id: "8105030b-c91f-5a40-a221-4a38c8531e1c"
|
|
```
|
|
|
|
Before trusting any claim from the upstream ID token: verify the RS256 signature
|
|
against Authelia's published keys, require the issuer Authelia advertises, require
|
|
KeyCape's own client ID in the audience, and reject expired or not-yet-valid
|
|
tokens. Fail closed — an unavailable or unparseable key set denies the login
|
|
rather than falling back to unverified claims. Resolve Authelia's metadata and
|
|
key set from the server-side token base URL so a split-horizon deployment, where
|
|
the advertised public URL is not reachable from KeyCape, still verifies; allow
|
|
explicit issuer and JWKS overrides for deployments where that inference is wrong.
|
|
Refresh the key set once on an unknown key id so provider key rotation does not
|
|
require a KeyCape restart. Replace the comment claiming the TLS boundary as
|
|
justification.
|
|
|
|
`idTokenVerifier` in the adapter resolves the issuer and JWKS path from the
|
|
provider's discovery document, rebasing the advertised `jwks_uri` path onto the
|
|
server-side token base URL so split-horizon deployments resolve, with
|
|
`issuer`/`jwksUrl` config overrides where that inference is wrong. Key sets are
|
|
cached and refetched once on an unknown key id. `parseIDTokenClaims` remains for
|
|
diagnostics but is documented as outside the authentication path.
|
|
|
|
## Prove the rejections
|
|
|
|
```task
|
|
id: KEY-WP-0019-T03
|
|
status: done
|
|
priority: high
|
|
state_hub_task_id: "163f64dd-aa87-5ce3-b3be-d134fa993eb6"
|
|
```
|
|
|
|
Negative tests per condition: forged signature, `alg: none` and other algorithms,
|
|
unknown key id, wrong issuer, missing and wrong audience, expired token, future
|
|
`iat`/`nbf`, malformed key set, and an unreachable JWKS endpoint. A positive case
|
|
must confirm a genuine token still authenticates, and a rotation case that a new
|
|
key id is picked up after one refresh.
|
|
|
|
Thirteen rejection cases in `verify_test.go` plus the algorithm and rotation
|
|
cases. Confirmed they catch the original defect rather than merely passing: with
|
|
the unverified parse restored, all fifteen fail. The existing adapter tests now
|
|
build genuinely signed tokens against a fixture provider, so they exercise the
|
|
real verification path instead of being routed around it.
|
|
|
|
## Reconcile the records
|
|
|
|
```task
|
|
id: KEY-WP-0019-T04
|
|
status: done
|
|
priority: medium
|
|
state_hub_task_id: "8a208092-7e85-5ad9-a8c4-0dddc66aaaee"
|
|
```
|
|
|
|
Update `SCOPE.md` and G01's status in the assessment to state that upstream
|
|
provider tokens are now verified independently of transport, and record the
|
|
operator decision and its reasoning so the absence of transport enforcement reads
|
|
as a choice rather than an oversight. G01 closes with this; complete profile
|
|
conformance is still not claimed.
|
|
|
|
SCOPE.md and G01's status record the verification and the operator decision,
|
|
including why no transport enforcement exists, so its absence reads as a choice.
|
|
|
|
## Consolidate the caller verifier
|
|
|
|
```task
|
|
id: KEY-WP-0019-T05
|
|
status: done
|
|
priority: medium
|
|
state_hub_task_id: "0dc909fe-1772-5816-9e11-dbe7664a4e1f"
|
|
```
|
|
|
|
Move `internal/authclient`'s inline verification onto `internal/jose` so one
|
|
implementation serves both paths. Kept separate from T01/T02 deliberately: the
|
|
caller verifier is a tested security path, and destabilising it in the same
|
|
change that introduces upstream verification would confuse the evidence for both.
|
|
|
|
`Client.Verify` now fetches the raw key set and delegates parsing and signature
|
|
checking to `internal/jose`, keeping its own claim policy — audience and nonce
|
|
bindings are the caller's and cannot live in a shared verifier. Its existing
|
|
tests pass unchanged, which is the point: the migration preserved behaviour.
|
|
|
|
One deliberate strictness increase: a key set containing any malformed RSA
|
|
signing key is now refused outright, where the previous code would have used a
|
|
good key alongside a bad one. KeyCape's own `/jwks` publishes a single key, so
|
|
this affects no current deployment.
|
|
|
|
Also added direct tests for `internal/jose` (17 cases). It is now the single
|
|
verifier behind both paths, so testing it only through its callers would leave
|
|
its edges — duplicate key ids, `crit`, even exponents, undersized moduli — to be
|
|
covered by accident.
|