Auth0 WIP #2: fail-fast config gate, JWKS rotation/outage handling, version-controlled Post-Login Action - #98
Merged
Merged
Conversation
This was referenced Aug 17, 2026
Commits the previously-uncommitted P1 tail and the net-new P2 auth work, which were interleaved in the same files and could not be cleanly split. P2 items landed and verified (127 auth tests pass; ruff + mypy clean): - #21 signed token with no usable `sub` -> 401 (guarded, never 500) - #17 60s clock-skew leeway on jwt.decode (single commented constant) - #16 OIDC discovery (discovery.py) with explicit timeout + single-flight; best-effort lifespan warm-up, convention fallback, never fails startup - #22b python-jose floor bumped >=3.5.0 (lock re-resolved; specifier-only) - #22c update_auth0_user explicit signature; no **fields splat to Auth0 - #24 DI/testability seam: get_auth0_settings + get_jwks_cache + JwksCache; tests no longer mutate the settings singleton or a module-global cache (order-independent, both directions green) - #19a structured JSON logging + per-outcome events; sub SHA-256 hashed and truncated; parametrised leak guard proves no token/email/raw-sub in output - #19b optional-auth invalid-token visible at INFO, distinct from no-token - #19c /ready reports JWKS-cache health as a non-gating info.auth field (no outbound Auth0 call; ok stays MongoDB + Temporal) - #20 rbac_demo router gated behind ENABLE_RBAC_DEMO (default false; 404 + absent from OpenAPI in prod; test env enables it for the Keycloak suite) - #18 negative-path coverage traced; missing-sub + leeway gaps closed - Decisions register (backend/docs/auth0-p2-decisions.md): D-5/D-6/D-9/D-10 ratified + implemented; D-11 partial; D-1/D-2/D-3/D-4/D-7/D-8 left OPEN Deferred / not in this commit: - #23 retry/backoff (blocked on D-8), #23 config execution (D-1/D-7) - #22 account-deletion data lifecycle (blocked on D-2/D-3, data-protection) - #19 counters + histogram (blocked on D-4, no metrics backend) - #26 backend/docs/authentication.md (write last, after the above settle) Config: non-secret Auth0 + rate-limit values added to per-cluster ConfigMaps (no credential values). .junie added to .gitignore. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M4aSVUpjs9QXRtPzqSr4aQ
The three Management API resource calls previously did one attempt and collapsed every failure to 502, so a transient 429/5xx failed the request and a rate-limit was indistinguishable from an outage. _send_with_retry now wraps get/update/delete_auth0_user with bounded exponential backoff: - 3 attempts (1 + 2 retries), base 0.5s x2 full-jitter, 15s total deadline - retries 429, 5xx, and httpx transport errors only; any other 4xx is returned unretried so raise_for_status() still surfaces it - honours a 429 Retry-After verbatim, clamped to 30s and the deadline - exhausted 429 -> Auth0ManagementRateLimited -> HTTP 503 + Retry-After (matches the cold-cache JWKS "try again shortly" shape); exhausted 5xx/transport -> Auth0ManagementUnavailable -> HTTP 502 - WARN logs name only op/status/attempt/exception-type; never the bearer token, the client secret, or a response body (leak-guard test asserts it) The token cache/lock is left byte-for-byte unchanged (EH-11). D-8 was OPEN (team, no source steer); ratified as an engineering decision and recorded in backend/docs/auth0-p2-decisions.md. CLAUDE.md failure-mode table updated with the 503/502 rows. Tests: tests/common/test_auth0_management_retry.py (8 cases via httpx.MockTransport, no network, asyncio.sleep stubbed) + existing tests/users/test_router.py mapping tests. ruff + mypy clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M4aSVUpjs9QXRtPzqSr4aQ
… for verification endpoints and adding rate limiting for compatibility checks. Updated documentation to reflect changes in endpoint access and requirements. Introduced owner-sub tracking for workflows and refined error handling for archive URL validation to prevent SSRF vulnerabilities.
…tributes. Update database service methods to support owner-specific queries and improve logging for workflow initiation. Refactor file upload logic to ensure proper visibility assignment based on user authentication status. Update relevant models and API endpoints to accommodate these changes, ensuring backward compatibility with legacy records.
Resolve conflicts against current main (which now includes PR #97): - .gitignore: union of editor/local ignores and main's .postman rules - backend/CLAUDE.md: keep Auth0 outage, rendered-config and auth smoke guidance inside main's Flux two-PR release/deploy procedure - compatibility/router.py: keep the rate-limit Depends and SSRF checks; keep main's Optional import, still used by archive_url - simulations/database.py: _coerce_date(value: object) -> datetime, which raises InvalidDateFilterError on bad input - tests/api/test_main.py: keep the BiosimulatorVersion import (still used) - tests/simulations/test_router.py: keep ownership/visibility tests and add main's logs 404 test Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Stop tracking the machine-local root .mcp.json (PyCharm loopback MCP endpoint) and ignore it with a root-only /.mcp.json rule. - Delete the discarded triple-quoted _warn_if_auth0_misconfigured implementation and its commented-out lifespan call. The active _validate_auth0_configuration startup gate is unchanged. - smoke: start the backend with AUTH_REQUIRED=false. The job runs without an IdP, so the Auth0 startup gate refused to boot and every later step failed. Production kustomize config is unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Main's new tests assumed the verify endpoints were anonymous and did not know about the demo:read permission endpoint, so they failed once merged with this branch's auth contract: - test_main: authenticate test_get_output_not_found and test_verify_omex_unknown_simulator; their 404/400 assertions are kept. - test_openapi_endpoints: classify verify-omex, get-verify-output and verify-runs as auth-required and add demo-private-permission; run the verify-omex 422 validation probe as an authenticated caller; keep the verify-runs PENDING-workflow assertions as an authenticated test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Collaborator
|
Thanks Jim, both review notes are addressed.
I also resolved the conflicts after PR #97 was merged and the PR was retargeted to main. Validation is complete: the full backend suite passes with 801 tests passing, the Keycloak integration tests pass, and backend/frontend CI, smoke tests, GitGuardian, and Snyk are all green. |
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.