feat(auth): JWKS bearer validation, require_role, and GET /me #56
No reviewers
Labels
No labels
in-progress
in-review
ready-for-agent
seam-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
patrick/Polaris!56
Loading…
Reference in a new issue
No description provided.
Delete branch "feature/auth-keycloak"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
PH0.4 — Keycloak auth: JWKS bearer validation,
require_role,GET /me, LDAP smoke test.What this adds
backend/src/polaris/api/auth.py: verifies a Keycloak bearer token's RS256 signature against the realm's cached JWKS (jwt.PyJWKClient), checksaud/iss/exp, exposesget_current_user(->CurrentUser:sub,preferred_username,roles,locale) andrequire_role(*roles)(403ForbiddenErrordependency factory). Every validation failure — missing header, unknown key, wrong audience, wrong issuer, expired, bad signature — comes back as the same 401UnauthorizedErrorproblem-details body.backend/src/polaris/api/routers/me/profile.py:GET /api/v1/me.get_current_userpopulatespolaris.core.context'sactor_sub/actor_rolecontextvars for the request's lifetime (the audit hook's actor fields). It's anasync defgenerator dependency deliberately — a sync one intermittently failscontextvars.Token.reset()because FastAPI runs sync generator dependencies in a worker thread.Tests
backend/tests/unit/api/auth_test.py— locally-signed-token unit tests (right audience passes; wrong audience/expired/bad signature/wrong issuer/missing header all 401;require_role403s a caller lacking the role). No network; a fakeSigningKeyResolverstands in forPyJWKClient.backend/tests/integration/api/auth_smoke_test.py(newsmokepytest marker) — against the real compose stack (db,openldap,ldap-init,keycloak), logs in as each of the five LDAP users viapolaris-web's Authorization Code + PKCE flow (it's deliberately direct-access-grants-disabled, so there's no password-grant client — this drives the actual browser login form headlessly withhttpx) and assertsGET /mereturns exactly that user's realm role. Ran this against a livedocker compose up db openldap ldap-init keycloakin this session — all 5 users (admin,planner,technician,viewer,ingest) pass. Skips (not fails) when Keycloak isn't reachable, so a plainpytestrun doesn't need the stack up.Set-Cookierather than rely on httpx's jar: Keycloak marksAUTH_SESSION_ID/KC_RESTARTSecure, and httpx (unlike a real browser, which treatslocalhostas a trustworthy origin even over plain HTTP) refuses to send aSecurecookie back over this un-TLS'd dev stack — which otherwise makes Keycloak reject the login POST with "Cookie not found."Two deviations from the owned-paths list, both explained in the commit body and flagged on the issue as I started
mainwas missing PH0.2 (PR #51 mergedfeature/api-skeletonintodevelop, notmain, sobackend/didn't exist onmain). This branch mergesorigin/developin first (disjoint paths, clean merge) to getbackend/to build against. Flagged on the issue in casedevelop→mainneeds a separate sync PR.backend/pyproject.toml/uv.lock: addedpyjwt[crypto]— no JWT/JWKS library was declared by PH0.2 (onlyhttpx, for outbound calls generally), since JWKS validation was this ticket's job. Also added asmokepytest marker next to the existingintegrationone. Both are small, additive, low-conflict-risk changes with the reasoning in the commit body perCLAUDE.md's "don't add dependencies without saying why."Verified locally
ruff check,ruff format --check,mypy --strictall clean; fullpytestsuite (46 tests: existing PH0.2 tests + new unit + smoke) green, smoke test run against a realdocker composestack.Closes #7
🤖 Generated with Claude Code
https://claude.ai/code/session_01LoNrSy7Reyp7evkfcdHeLX