feat(frontdoor): real SSO — OIDC + WebAuthn/FIDO2 verification, fail-closed, stdlib-only - #40
Merged
Merged
Conversation
…closed, stdlib-only
login.py is the session core; the honest gap was that nothing real decided *who is this* before a
session was minted (just a lambda credential_check). This adds the human front door — the two SSO
mechanisms the cloud-shell-fog spec names — done honestly with only the standard library. It is the
human analogue of the push webhook: the webhook is the HMAC-verified MACHINE door; this is the
OIDC/WebAuthn-verified HUMAN door. Same posture — a zero-trust, fail-closed gate.
Honest boundary, stated in code + docs + CapD: verifying an ASYMMETRIC signature (RS256/ES256 JWTs,
WebAuthn credential keys) needs crypto the stdlib doesn't ship and must never be hand-rolled, so that
ONE step is a marked injection point (verify_signature=…) wired in production to a JWKS/cryptography
verifier — asymmetric algs are refused, never silently trusted, without it. Everything else is real:
tools/sso.py:
- verify_id_token — OIDC ID-token validation, signature FIRST then every claim: alg 'none' always
refused; HS256 via hmac; iss exact; aud/azp; exp/nbf/iat (leeway); nonce binding (replay/CSRF).
- PKCE (S256) + auth-code: pkce_pair / build_authorization_request / verify_callback (state=CSRF) /
build_token_exchange (sends code_verifier). All stdlib, no network I/O.
- verify_assertion — WebAuthn §7.2: type, challenge (const-time), origin, rpIdHash, UP/UV flags,
signature-counter clone detection; credential signature is the injected step.
- session_from_identity — a valid verification mints a login.py session; invalid mints nothing.
Wired: capd/sso-front-door.mesh.capd.json (caps.dev.sso-front-door); docs/SSO_FRONT_DOOR.md;
validate.py; Makefile `sso`. PAAS_GAP_REGISTER: login/SSO ○→◑ (verify core done, live IdP wiring the
remaining honest gap); notes the front-door + deploy trio now complete.
Tests: +25 = 234 tools tests green — every rejection path (alg none, wrong secret, iss/aud, expired,
missing iat, nonce, no-asymmetric-verifier, PKCE tamper, CSRF state, WebAuthn type/challenge/origin/
rpId/UP/UV/clone/bad-sig).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
login.pyis the session core, but nothing real decided who is this before a session was minted — just a lambdacredential_check. This adds the human front door: the two SSO mechanisms the cloud-shell-fog spec names — OIDC and WebAuthn/FIDO2 — done honestly with only the standard library.It's the human analogue of the push webhook: the webhook is the HMAC-verified machine door; this is the OIDC/WebAuthn-verified human door. Same posture — a zero-trust, fail-closed gate.
The honest boundary (stated in code, docs, and the CapD)
Verifying an asymmetric signature (RS256/ES256 ID tokens, WebAuthn credential keys) needs public-key crypto the stdlib doesn't ship, and hand-rolling RSA/ECDSA is exactly what you must never do. So that one step is a marked injection point (
verify_signature=…), wired in production to a JWKS/cryptographyverifier. Without it, asymmetric algs are refused, never silently trusted. Everything else — the majority of what these protocols get wrong in the field — is real and stdlib-doable.What's verified (all fail-closed)
verify_id_token): signature first, then every claim.alg:nonealways refused; HS256 viahmac;issexact;aud/azp;exp/nbf/iat(leeway);noncebinding (replay/CSRF).pkce_pair/build_authorization_request/verify_callback(state = CSRF) /build_token_exchange(sendscode_verifier). Stdlib, no network I/O.verify_assertion, §7.2): type, challenge (constant-time), origin,rpIdHash, UP/UV flags, signature-counter clone detection; credential signature is the injected step.session_from_identity— a valid verification mints alogin.pysession; an invalid one mints nothing.Wired in
capd/sso-front-door.mesh.capd.json(caps.dev.sso-front-door)docs/SSO_FRONT_DOOR.md,validate.py,Makefile ssodocs/PAAS_GAP_REGISTER.md: login/SSO ○ → ◑ (verify core done; live IdP wiring is the remaining honest gap) + notes the front-door + deploy trio now completeTests
+25 = 234 tools tests green — every rejection path:
alg:none, wrong HS256 secret,iss/aud, expired, missingiat,noncemismatch, asymmetric-without-verifier, PKCE tamper, CSRFstate, and WebAuthn type/challenge/origin/rpId/UP/UV/clone/bad-signature.🤖 Generated with Claude Code