feat: dual auth middleware — accept Keycloak JWT alongside API key #267

Closed
opened 2026-04-12 18:04:05 +00:00 by forgejo_admin · 0 comments

Type

Feature

Lineage

Standalone — discovered during pal-e-app "app definition" compliance audit (2026-04-12). Blocks all downstream identity features.

Repo

forgejo_admin/pal-e-api

User Story

As a pal-e-app user, I want the API to validate my Keycloak JWT token so that the backend knows WHO I am, not just WHETHER I'm authenticated.
As an MCP agent, I want the existing API key auth to keep working unchanged.

Context

pal-e-app has Keycloak OIDC (realm pal-e, client pal-e-docs-app, PKCE). The frontend passes Authorization: Bearer {jwt} but the backend (auth.py, 24 lines) only checks a static API key via X-PALEDocs-Token header. The backend has no concept of WHO is making a request. This ticket adds JWT validation alongside the existing API key, enabling all downstream identity-aware features.

File Targets

Files the agent should modify or create:

  • src/pal_e_docs/auth.py (24 lines) — add JWT validation, get_current_user() dependency, UserContext dataclass
  • src/pal_e_docs/config.py — add keycloak_url, keycloak_realm settings
  • src/pal_e_docs/routes/users.py (new) — GET /me endpoint returning user claims from JWT
  • pyproject.toml — add PyJWT[crypto] or python-jose[cryptography] dependency

Files the agent should NOT touch:

  • src/pal_e_docs/routes/notes.py — wiring UserContext into endpoints is the next ticket
  • src/pal_e_docs/models.py — adding columns is the next ticket

Acceptance Criteria

  • curl -H "Authorization: Bearer {valid_jwt}" /me returns { sub, email, name, roles }
  • curl -H "X-PALEDocs-Token: {api_key}" /notes still works (no regression)
  • curl -H "Authorization: Bearer {valid_jwt}" /notes returns authenticated data
  • Invalid/expired JWT doesn't crash — falls through to unauthenticated
  • GET /me without JWT returns 401

Test Expectations

  • Unit test: valid JWT → UserContext populated with sub, email, name, roles
  • Unit test: expired JWT → returns None (no crash)
  • Unit test: API key still works independently
  • Integration test: GET /me with mocked JWT returns claims
  • Integration test: GET /me without auth returns 401
  • Run command: pytest tests/ -v

Constraints

  • get_is_authenticated() must stay exactly as-is — 20 endpoints depend on it
  • New dependency should add get_current_user() that returns UserContext | None
  • JWKS keys should be cached (fetch once, refresh on rotation)
  • Match existing FastAPI dependency injection pattern used throughout routes/
  • Keycloak JWKS endpoint: https://keycloak.tail5b443a.ts.net/realms/pal-e/protocol/openid-connect/certs

Checklist

  • PR opened
  • Tests pass (144 existing + new)
  • No unrelated changes
  • pal-e-docs — project this affects
  • definition-app — the app definition driving this work
### Type Feature ### Lineage Standalone — discovered during pal-e-app "app definition" compliance audit (2026-04-12). Blocks all downstream identity features. ### Repo `forgejo_admin/pal-e-api` ### User Story As a **pal-e-app user**, I want the API to validate my Keycloak JWT token so that the backend knows WHO I am, not just WHETHER I'm authenticated. As an **MCP agent**, I want the existing API key auth to keep working unchanged. ### Context pal-e-app has Keycloak OIDC (realm `pal-e`, client `pal-e-docs-app`, PKCE). The frontend passes `Authorization: Bearer {jwt}` but the backend (`auth.py`, 24 lines) only checks a static API key via `X-PALEDocs-Token` header. The backend has no concept of WHO is making a request. This ticket adds JWT validation alongside the existing API key, enabling all downstream identity-aware features. ### File Targets Files the agent should modify or create: - `src/pal_e_docs/auth.py` (24 lines) — add JWT validation, `get_current_user()` dependency, `UserContext` dataclass - `src/pal_e_docs/config.py` — add `keycloak_url`, `keycloak_realm` settings - `src/pal_e_docs/routes/users.py` (new) — `GET /me` endpoint returning user claims from JWT - `pyproject.toml` — add `PyJWT[crypto]` or `python-jose[cryptography]` dependency Files the agent should NOT touch: - `src/pal_e_docs/routes/notes.py` — wiring UserContext into endpoints is the next ticket - `src/pal_e_docs/models.py` — adding columns is the next ticket ### Acceptance Criteria - [ ] `curl -H "Authorization: Bearer {valid_jwt}" /me` returns `{ sub, email, name, roles }` - [ ] `curl -H "X-PALEDocs-Token: {api_key}" /notes` still works (no regression) - [ ] `curl -H "Authorization: Bearer {valid_jwt}" /notes` returns authenticated data - [ ] Invalid/expired JWT doesn't crash — falls through to unauthenticated - [ ] `GET /me` without JWT returns 401 ### Test Expectations - [ ] Unit test: valid JWT → UserContext populated with sub, email, name, roles - [ ] Unit test: expired JWT → returns None (no crash) - [ ] Unit test: API key still works independently - [ ] Integration test: `GET /me` with mocked JWT returns claims - [ ] Integration test: `GET /me` without auth returns 401 - Run command: `pytest tests/ -v` ### Constraints - `get_is_authenticated()` must stay exactly as-is — 20 endpoints depend on it - New dependency should add `get_current_user()` that returns `UserContext | None` - JWKS keys should be cached (fetch once, refresh on rotation) - Match existing FastAPI dependency injection pattern used throughout routes/ - Keycloak JWKS endpoint: `https://keycloak.tail5b443a.ts.net/realms/pal-e/protocol/openid-connect/certs` ### Checklist - [ ] PR opened - [ ] Tests pass (144 existing + new) - [ ] No unrelated changes ### Related - `pal-e-docs` — project this affects - `definition-app` — the app definition driving this work
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
forgejo_admin/pal-e-api#267
No description provided.