Auth0 WIP #2: fail-fast config gate, JWKS rotation/outage handling, version-controlled Post-Login Action - #98
Open
jcschaff wants to merge 4 commits into
Open
Conversation
This was referenced Aug 17, 2026
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.
Summary
Auth0 WIP #2 — hardens the authentication path landed in #96 so that an
identity-provider hiccup, a rotated signing key, or a half-configured cluster
each produce a correct and legible outcome instead of a silent one.
Stacked on #97 (
chore/keycloak-tests) — this PR's base is that branch, sothe diff here is the four commits on top of it. Merge #97 first; GitHub will
retarget this to
mainautomatically.What changed
1. Startup gate instead of a startup warning (
e799d9a)_warn_if_auth0_misconfigured()logged a warning and let the pod start, so acluster with a missing
AUTH0_DOMAIN/AUTH0_AUDIENCEreported healthy and thenfailed every authenticated request. The warning also misdescribed the failure
(it promised a 401, which was never what happened).
Auth0Settings.configuration_errors()(config.py) — pure, side-effect-freeenumeration of every reason the settings could not verify a token. Accepts
both valid shapes: a bare
AUTH0_DOMAIN, or explicitAUTH0_ISSUERandAUTH0_JWKS_URIoverrides (how a non-Auth0 OIDC provider, e.g. the Keycloaktest realm, is configured). Half of the override pair is reported as an error.
_validate_auth0_configuration()(api/main.py) raises out oflifespan, souvicorn exits non-zero and Kubernetes shows
CrashLoopBackOffwith the reasonin
kubectl logs.AUTH_REQUIREDsetting (defaulttrue) is the escape hatch: set it falseto run a deployment deliberately without an identity provider — the API then
starts, logs what is missing, and every authenticated endpoint returns 503.
kustomize/config/biosim-{local,rke}/api.envgetAUTH0_DOMAIN+AUTH0_AUDIENCE(biosim-gke already had them from Auth0-Integration #96), so those overlays donot start crash-looping the moment the gate lands.
2. JWKS handling: rotation, outages, and malformed key sets (
e3db45b,de31e3e)_get_jwks()previously refetched on every miss, raised on any failure, andindexed
k["kty"]/["kid"]/["use"]/["n"]/["e"]directly — an entry missing theRFC 7517-optional
usefield raisedKeyError→ HTTP 500.kidforces one refresh (cooldown-guarded, 60s) before rejectingthe token. Auth0 rotates signing keys without notice; this turns a rotation
from an hour-long outage into one slow request. The cooldown is load-bearing:
without it a flood of bogus
kids is an amplification vector against the IdP.for up to 24h while refreshes fail, then refused. Well inside Auth0's rotation
overlap, so a key cached in that window is still a key the tenant published.
per process, not one per inbound request.
asyncio.Lockwith the double-checked patternalready used in
auth0_management.py. The lock is held across the fetch only,never across
jwt.decode, so validation stays parallel._select_rsa_key()guards every field access and defaults a missinguseto"sig".3. Error responses and diagnostics (
95c3383)Retry-After("Authentication temporarily unavailable"), not a 401 — the caller's token was
never the problem. Detail text names no URL, no exception, no token material.
get_optional_userno longer swallows the 503. Downgrading anauthenticated caller to anonymous during an Auth0 outage silently changes the
authorization outcome (ownership checks, role gates). 401s stay swallowed —
a bad token on an optional-auth endpoint is still just "not authenticated".
kidlog line deliberately does not echo thekid; it comesfrom an unverified, attacker-controlled header.
_warn_roles_claim_absent()— rate-limited (5 min) runtime assertion that thePost-Login Action is live. Without it, an absent Action means every
require_rolesendpoint 403s and no admin exists, presenting as a permissionsbug with no signal anywhere.
4. The Auth0 Action is now version-controlled (
e799d9a)auth0/actions/post-login.js+auth0/README.md. #96 depended on a Post-LoginAction that existed only as dashboard state; this is the reviewed source of
truth the dashboard is expected to match. The README documents the required
Roles, the M2M application and its exact scopes (
read:roles,create:role_members— kept separate from theupdate:users/delete:usersapplication
/api/v1/mewill need), the Action secrets, theauth0dependency,the flow binding, and a post-deploy smoke check. Nothing in
auth0/isdeployed by CI or
kubectl— applying it is a dashboard action.Backend
CLAUDE.mdand.env.examplegain matching Authentication sections.Tests
All new, all against real tokens or real HTTP behavior — no mocked JWT
verification:
tests/api/test_startup_auth_config.pyAUTH_REQUIREDmodes, each malformed-config shapetests/common/test_auth0_jwks.pytests/common/test_auth0_reliability.pytests/common/test_auth0_roles_claim.pytests/api/test_auth_error_responses.pyget_optional_userpropagationtests/fixtures/jwks_fixtures.pyReview notes
Two things worth a look before merging:
.mcp.jsonis committed at the repo root and points at a machine-localPyCharm MCP endpoint (
http://127.0.0.1:64462/stream). That port is specificto one developer's IDE session — it probably belongs in
.gitignore(as.vscode/.cursorare in this same commit) rather than in the repo._warn_if_auth0_misconfigured()is commented out rather than deleted —wrapped in a
"""block inapi/main.py, with the call site left as acomment. It is fully replaced by
_validate_auth0_configuration(); worthdeleting outright.