Skip to content

feat(frontdoor): real SSO — OIDC + WebAuthn/FIDO2 verification, fail-closed, stdlib-only - #40

Merged
mdheller merged 1 commit into
mainfrom
feat/sso-oidc-webauthn
Aug 4, 2026
Merged

feat(frontdoor): real SSO — OIDC + WebAuthn/FIDO2 verification, fail-closed, stdlib-only#40
mdheller merged 1 commit into
mainfrom
feat/sso-oidc-webauthn

Conversation

@mdheller

@mdheller mdheller commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

What

login.py is the session core, but 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 — 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/cryptography verifier. 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)

  • OIDC ID-token (verify_id_token): 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). Stdlib, no network I/O.
  • WebAuthn assertion (verify_assertion, §7.2): type, challenge (constant-time), origin, rpIdHash, UP/UV flags, signature-counter clone detection; credential signature is the injected step.
  • Bridge: session_from_identity — a valid verification mints a login.py session; 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 sso
  • docs/PAAS_GAP_REGISTER.md: login/SSO ○ → ◑ (verify core done; live IdP wiring is the remaining honest gap) + notes the front-door + deploy trio now complete

Tests

+25 = 234 tools tests green — every rejection path: alg:none, wrong HS256 secret, iss/aud, expired, missing iat, nonce mismatch, asymmetric-without-verifier, PKCE tamper, CSRF state, and WebAuthn type/challenge/origin/rpId/UP/UV/clone/bad-signature.

🤖 Generated with Claude Code

…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).
@mdheller
mdheller merged commit f4c872b into main Aug 4, 2026
1 check passed
@mdheller
mdheller deleted the feat/sso-oidc-webauthn branch August 4, 2026 07:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant