Admin routes validate an Authentik OIDC JWT: issuer, audience, JWKS signature and expiry are all enforced. A request with no token returns 401; a malformed, expired, wrong-issuer, wrong-audience or bad-signature token returns 401; a valid token returns 200 on a probe admin route. The JWKS is fetched from the configured Authentik provider and cached. Config (issuer, audience, JWKS URL) comes from api config. Tests use a locally generated key pair plus a stubbed JWKS so signature/expiry/audience are proven without network access.
Changes
api/auth.py (new) — the ONE reusable admin-plane auth dependency:
require_admin_jwt extracts the Bearer credential (missing/malformed header → 401) and returns the verified claims for the route to use; the nestquest-admins group check is the NEXT task (0f2afb29) and extends this dependency without a rewrite.
verify_admin_token enforces RS256-only alg, requires kid, selects the RSA key named by kid from the JWKS, and decodes with PyJWT enforcing issuer, audience, required exp, and nbf when present. Every rejection carries the SAME uniform 401 detail (WWW-Authenticate: Bearer); a JWKS fetch failure is deliberately not mapped to 401 (server-side fault propagates, cache never poisoned).
JwksCache — per-app cache on app.state.jwks_cache (installed by create_app): fetched via httpx, 5-minute TTL, asyncio-lock-guarded double-checked fetch, failed fetches never cached. Tests stub the fetcher — zero network access.
api/routes_admin.py (new) — thin admin router at /api/v1/admin with the probe route GET /api/v1/admin/ping → 200 {"status": "ok", "sub": <verified subject>}; the JWT check is attached at router level and the route reuses the per-request cached dependency result.
api/config.py — ApiConfig grows oidc_issuer / oidc_audience / oidc_jwks_url, read from NESTQUEST_OIDC_ISSUER / NESTQUEST_OIDC_AUDIENCE / NESTQUEST_OIDC_JWKS_URL with the same fail-fast discipline as NESTQUEST_DB_PATH/NESTQUEST_PANEL_TOKEN (unset/empty → ConfigError — a default issuer must not silently leave the admin plane open).
api/app.py — wires the admin router and the per-app JwksCache.
pyproject.toml / uv.lock — pyjwt[crypto]>=2.9 added to the dev group (2.14.0 locked); runtime deps move to the API deployable's own manifest in Feature 17, per the existing comment.
tests/test_api_admin_auth.py (new, 22 tests) — locally generated RSA key pair + JWKS built from its public key + counting stub fetcher: no token 401; malformed headers 401; garbage 401; expired 401; wrong issuer 401; wrong audience 401; future nbf 401; missing exp 401; unknown kid 401; bad signature (second local key pair) 401; valid token 200 with the subject; all rejections share one detail; JWKS fetched once across requests and refetched on TTL lapse; from_env requires each OIDC var (missing + empty).
Existing tests updated mechanically only: the four direct ApiConfig(...) constructions and three from_env dicts gained the new required fields (no assertion weakened, none skipped).
Test results (TZ=UTC)
TZ=UTC uv run pytest -q
1634 passed in 8.87s
TZ=UTC uv run pytest -q tests/test_api_admin_auth.py
22 passed in 0.38s
Baseline was 1612 passed; nothing skipped, weakened, or disabled (1612 + 22 new = 1634).
Scope notes
JWT validation only — group membership (nestquest-admins) is task 0f2afb29.
Not merged; awaiting independent review.
## Maestro task
`e70e6bfb-085a-4781-a3f4-276355bbbd19` — Admin auth: Authentik JWT validation (Feature 16 — NestQuest API Service, admin plane D-012)
## Done-condition
Admin routes validate an Authentik OIDC JWT: issuer, audience, JWKS signature and expiry are all enforced. A request with no token returns 401; a malformed, expired, wrong-issuer, wrong-audience or bad-signature token returns 401; a valid token returns 200 on a probe admin route. The JWKS is fetched from the configured Authentik provider and cached. Config (issuer, audience, JWKS URL) comes from api config. Tests use a locally generated key pair plus a stubbed JWKS so signature/expiry/audience are proven without network access.
## Changes
- **api/auth.py (new)** — the ONE reusable admin-plane auth dependency:
- `require_admin_jwt` extracts the Bearer credential (missing/malformed header → 401) and returns the **verified claims** for the route to use; the nestquest-admins group check is the NEXT task (0f2afb29) and extends this dependency without a rewrite.
- `verify_admin_token` enforces RS256-only `alg`, requires `kid`, selects the RSA key named by `kid` from the JWKS, and decodes with PyJWT enforcing issuer, audience, required `exp`, and `nbf` when present. Every rejection carries the SAME uniform 401 detail (`WWW-Authenticate: Bearer`); a JWKS fetch failure is deliberately not mapped to 401 (server-side fault propagates, cache never poisoned).
- `JwksCache` — per-app cache on `app.state.jwks_cache` (installed by `create_app`): fetched via httpx, 5-minute TTL, asyncio-lock-guarded double-checked fetch, failed fetches never cached. Tests stub the fetcher — zero network access.
- **api/routes_admin.py (new)** — thin admin router at `/api/v1/admin` with the probe route `GET /api/v1/admin/ping` → `200 {"status": "ok", "sub": <verified subject>}`; the JWT check is attached at router level and the route reuses the per-request cached dependency result.
- **api/config.py** — `ApiConfig` grows `oidc_issuer` / `oidc_audience` / `oidc_jwks_url`, read from `NESTQUEST_OIDC_ISSUER` / `NESTQUEST_OIDC_AUDIENCE` / `NESTQUEST_OIDC_JWKS_URL` with the same fail-fast discipline as `NESTQUEST_DB_PATH`/`NESTQUEST_PANEL_TOKEN` (unset/empty → `ConfigError` — a default issuer must not silently leave the admin plane open).
- **api/app.py** — wires the admin router and the per-app `JwksCache`.
- **pyproject.toml / uv.lock** — `pyjwt[crypto]>=2.9` added to the dev group (2.14.0 locked); runtime deps move to the API deployable's own manifest in Feature 17, per the existing comment.
- **tests/test_api_admin_auth.py (new, 22 tests)** — locally generated RSA key pair + JWKS built from its public key + counting stub fetcher: no token 401; malformed headers 401; garbage 401; expired 401; wrong issuer 401; wrong audience 401; future `nbf` 401; missing `exp` 401; unknown `kid` 401; bad signature (second local key pair) 401; valid token 200 with the subject; all rejections share one detail; JWKS fetched once across requests and refetched on TTL lapse; `from_env` requires each OIDC var (missing + empty).
- Existing tests updated mechanically only: the four direct `ApiConfig(...)` constructions and three `from_env` dicts gained the new required fields (no assertion weakened, none skipped).
## Test results (TZ=UTC)
```
TZ=UTC uv run pytest -q
1634 passed in 8.87s
TZ=UTC uv run pytest -q tests/test_api_admin_auth.py
22 passed in 0.38s
```
Baseline was 1612 passed; nothing skipped, weakened, or disabled (1612 + 22 new = 1634).
## Scope notes
- JWT validation only — group membership (`nestquest-admins`) is task 0f2afb29.
- Not merged; awaiting independent review.
The admin plane had no authentication: any caller could reach its
routes once they existed. This adds the plane's ONE reusable FastAPI
dependency (require_admin_jwt in api/auth.py) that verifies an
Authentik OIDC JWT — Bearer extraction, RS256-only signature check
against the provider's JWKS, issuer, audience, required exp, and
enforced nbf — and raises the SAME 401 for every failure so the
response never leaks which check failed.
The JWKS is fetched over httpx once per app (JwksCache, 5-minute TTL,
asyncio-lock-guarded, failed fetches never cached) and tests stub the
fetcher, so the suite proves signature/expiry/audience enforcement
with locally generated RSA key pairs and zero network access.
ApiConfig grows the three required OIDC settings
(NESTQUEST_OIDC_ISSUER/AUDIENCE/JWKS_URL) under the existing fail-fast
discipline: an unconfigured issuer must not silently leave the admin
plane open, so from_env raises rather than inventing a default.
This is authentication only — the verified claims are the dependency's
return value, so the nestquest-admins group check is the next task's
addition, not a rewrite. PyJWT[crypto] joins the dev dependency group
(the API deployable's runtime manifest lands in Feature 17).
Maestro task: e70e6bfb-085a-4781-a3f4-276355bbbd19
- P2: verify_admin_token parses and enforces the JWT header (alg ==
RS256, kid present) BEFORE consulting the JWKS cache, so a
structurally invalid token costs no fetch, no lock, no provider load.
- P2: JwksCache gains a short negative-cache backoff window (5s,
time.monotonic-recorded) after a failed fetch: within it, get()
re-raises the recorded fault without touching the fetcher — and
without holding or queueing on the lock — so an outage costs one
fetch per window instead of one per request. A successful fetch
clears the backoff; nothing is cached indefinitely (TTL unchanged).
- P3: _matching_key widens the from_jwk handling to
(PyJWTError, binascii.Error, ValueError, TypeError, KeyError) so a
JWKS entry matching the kid but carrying corrupt n/e maps to the
SAME uniform 401 instead of surfacing a 500.
- P3: an unmatched kid now forces ONE TTL-bypassing refetch and
retries the match once, so rotated keys verify without waiting out
the TTL; the forced refetch is itself bounded (inside the backoff
window or the 5s kid-refetch interval the cached document is served
as-is), so unknown-kid floods cannot force a fetch per request.
- Tests: no-fetch proofs for garbage/non-RS256 tokens, corrupt-entry
401, outage fail-fast (sequential + concurrent burst), backoff
lapse/recovery, outage-as-server-fault, and the unknown-kid
refetch: one forced, throttled, resume-after-interval, rotation
picked up, failed forced refetch propagates then fails fast.
Existing tests unchanged (uniform 401 contract intact).
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Maestro task
e70e6bfb-085a-4781-a3f4-276355bbbd19— Admin auth: Authentik JWT validation (Feature 16 — NestQuest API Service, admin plane D-012)Done-condition
Admin routes validate an Authentik OIDC JWT: issuer, audience, JWKS signature and expiry are all enforced. A request with no token returns 401; a malformed, expired, wrong-issuer, wrong-audience or bad-signature token returns 401; a valid token returns 200 on a probe admin route. The JWKS is fetched from the configured Authentik provider and cached. Config (issuer, audience, JWKS URL) comes from api config. Tests use a locally generated key pair plus a stubbed JWKS so signature/expiry/audience are proven without network access.
Changes
require_admin_jwtextracts the Bearer credential (missing/malformed header → 401) and returns the verified claims for the route to use; the nestquest-admins group check is the NEXT task (0f2afb29) and extends this dependency without a rewrite.verify_admin_tokenenforces RS256-onlyalg, requireskid, selects the RSA key named bykidfrom the JWKS, and decodes with PyJWT enforcing issuer, audience, requiredexp, andnbfwhen present. Every rejection carries the SAME uniform 401 detail (WWW-Authenticate: Bearer); a JWKS fetch failure is deliberately not mapped to 401 (server-side fault propagates, cache never poisoned).JwksCache— per-app cache onapp.state.jwks_cache(installed bycreate_app): fetched via httpx, 5-minute TTL, asyncio-lock-guarded double-checked fetch, failed fetches never cached. Tests stub the fetcher — zero network access./api/v1/adminwith the probe routeGET /api/v1/admin/ping→200 {"status": "ok", "sub": <verified subject>}; the JWT check is attached at router level and the route reuses the per-request cached dependency result.ApiConfiggrowsoidc_issuer/oidc_audience/oidc_jwks_url, read fromNESTQUEST_OIDC_ISSUER/NESTQUEST_OIDC_AUDIENCE/NESTQUEST_OIDC_JWKS_URLwith the same fail-fast discipline asNESTQUEST_DB_PATH/NESTQUEST_PANEL_TOKEN(unset/empty →ConfigError— a default issuer must not silently leave the admin plane open).JwksCache.pyjwt[crypto]>=2.9added to the dev group (2.14.0 locked); runtime deps move to the API deployable's own manifest in Feature 17, per the existing comment.nbf401; missingexp401; unknownkid401; bad signature (second local key pair) 401; valid token 200 with the subject; all rejections share one detail; JWKS fetched once across requests and refetched on TTL lapse;from_envrequires each OIDC var (missing + empty).ApiConfig(...)constructions and threefrom_envdicts gained the new required fields (no assertion weakened, none skipped).Test results (TZ=UTC)
Baseline was 1612 passed; nothing skipped, weakened, or disabled (1612 + 22 new = 1634).
Scope notes
nestquest-admins) is task 0f2afb29.Independent Claude Sonnet 5 review completed for this exact commit.