Admin plane: validate Authentik OIDC JWTs on /api/v1/admin/*
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
This commit is contained in:
+15
-2
@@ -12,6 +12,11 @@ exposes the two endpoints every later task depends on:
|
||||
(service-token protected; see :mod:`api.routes_panel`, whose router
|
||||
carries the shared :func:`api.dependencies.require_panel_token`
|
||||
check).
|
||||
- ``GET /api/v1/admin/ping`` — the admin plane's auth probe, protected
|
||||
by the Authentik OIDC JWT check (see :mod:`api.routes_admin`, whose
|
||||
router carries the shared :func:`api.auth.require_admin_jwt`
|
||||
dependency; the JWKS document it verifies against is cached on
|
||||
``app.state.jwks_cache``).
|
||||
- ``GET /docs`` — FastAPI's auto-served OpenAPI document (Swagger UI);
|
||||
``/openapi.json`` is the raw schema. No extra wiring is needed;
|
||||
FastAPI serves both by default.
|
||||
@@ -37,9 +42,11 @@ from collections.abc import AsyncIterator
|
||||
|
||||
from fastapi import FastAPI
|
||||
|
||||
from api.auth import JwksCache
|
||||
from api.config import ApiConfig
|
||||
from api.database import DatabaseState, make_database
|
||||
from api.events import TransitionPublisher
|
||||
from api.routes_admin import router as admin_router
|
||||
from api.routes_panel import router as panel_router
|
||||
|
||||
|
||||
@@ -90,10 +97,16 @@ def create_app(config: ApiConfig | None = None) -> FastAPI:
|
||||
)
|
||||
|
||||
# The resolved config rides on app.state so the reusable route
|
||||
# dependencies (the panel service-token check) read the configured
|
||||
# values from the app rather than module state.
|
||||
# dependencies (the panel service-token check, the admin JWT check)
|
||||
# read the configured values from the app rather than module state.
|
||||
app.state.config = cfg
|
||||
# The admin plane's ONE per-app JWKS cache: fetched from the
|
||||
# configured provider on first use, then reused until its TTL
|
||||
# lapses (see api.auth.JwksCache). Tests replace it with a
|
||||
# stub-backed cache so no request ever leaves the process.
|
||||
app.state.jwks_cache = JwksCache(cfg.oidc_jwks_url)
|
||||
app.include_router(panel_router)
|
||||
app.include_router(admin_router)
|
||||
|
||||
@app.get("/health", summary="Service health")
|
||||
async def health() -> dict[str, str]:
|
||||
|
||||
Reference in New Issue
Block a user