fix(auth): validate every OIDC provider response at the boundary #216
Inga granskare
Etiketter
Inga etiketter
area/backups
area/ci
area/control-panel
area/identity
area/infra
area/observability
area/payments
area/security
area/storage
area/web
blocked
needs-info
needs-triage
ready-for-implementation
type
bug
type
chore
type
docs
type
epic
type
feature
type
task
wontfix
Ingen milstolpe
Inget projekt
Inga tilldelade
1 deltagare
Notiser
Förfallodatum
Inget förfallodatum satt.
Beroenden
Inga beroenden satta
Referens
bitborg/bitborg-web!216
Läser in…
Hänvisa till i nytt ärende
Ingen beskrivning angiven.
Ta bort grenen "fix/validate-oidc-boundary"
Borttagning av en gren är permanent. Även om den borttagna grenen kan fortsätta existera en kort tid innan den faktiskt tas bort, kan det INTE ångras i de flesta fall. Vill du fortsätta?
The security review in #36 listed three controls as follow-up work. None of them are in
main.This is that follow-up. Two commits.
Validate every OIDC provider response at the boundary
The discovery document, the JWKS, the token-endpoint response, the id_token header and claims,
and the userinfo body were each parsed and then asserted into a type nobody had checked
(
as Discovery). An assertion is not a check. It makes the code read as though the shape wereknown while leaving it as unknown as before, in the path that decides who is logged in.
All five are now validated at runtime, once, at the edge, with zod (
src/lib/auth/oidc-schemas.ts).oidc.tsworks only with the validated results. The exported types are inferred from the schemas,so the two cannot drift.
Schemas are strict about the fields the client depends on and lenient about the rest. Unknown
fields are dropped rather than rejected, since the live provider document carries 24 fields and 5
are read. A JWKS entry the client cannot use is skipped during key selection instead of failing the
whole set. Failures fail closed, naming the offending field but never its value, because these
messages are logged and the values include tokens. Bodies are read as text and parsed here rather
than via
res.json(), whose SyntaxError quotes a fragment of the body.Three fail-open defects the validation exposed, fixed here:
userinfo_endpointwas assumed present, though the spec makes it only RECOMMENDED. A provideromitting it would have produced a fetch against
undefinedmid-login. Discovery now rejects sucha document up front.
expnever expired.typeof exp === "number"admits Infinity, andInfinity * 1000 <= nowis false, so1e999passed the expiry check for ever.expnever expired either, for the same reason. The HMACproves a cookie was minted with our secret but says nothing about its shape. The payload is now
shape-checked after the MAC check. A mismatch yields no session, exactly like a bad signature.
Endpoints from the discovery document must now be absolute https URLs, with http allowed only on a
loopback host for a locally-run provider. The client secret travels to
token_endpointand theaccess token to
userinfo_endpoint.Read environment values as checked strings
The last untyped values feeding logic came from
import.meta.env, which Astro types with ananyindex signature. Every key it does not know about arrives as
anyand flows on unchecked. One had.trim()called on it, another was handed toencodeURIComponent, another toTextEncoder.encodeas an HMAC secret.src/lib/env.tsnow does that check once.envStringreadsprocess.envfirst and falls back toimport.meta.envfor server-only runtime values.stringOrtakes the value rather than a key, soconsts.tskeeps the staticimport.meta.env.PUBLIC_*access Vite needs to inline at build time.An empty
PUBLIC_*value now falls back to the default rather than being used verbatim.With that, the four
no-unsafe-*rules go back on and gate the repo. The last of 39 findings aregone, and gone because the values are checked, not because a type assertion claimed they were.
Review notes
zod4.4.3, MIT.docs/design/oidc-security-review.mdis updated. The three follow-up rows now read as done.over a credential-bearing body does not echo the credential. The ES256/EC P-256 path the live
provider actually signs with is covered.
OIDC_*andSESSION_SECRETare set and abitborg-webOAuth2client exists in Kanidm. Merging this exposes nothing on its own.
Verified
pnpm test: 361 passed, up 56 from main's 305pnpm check: 0 errors, 0 warningspnpm eslint,pnpm stylelint,pnpm format:check,pnpm mdlint,pnpm lang-check: all cleanCloses #36