EqualifyEverything / equalify-reflow

feat(auth): generic OIDC provider + Microsoft Entra preset (PR2)
Implements the OIDC half of the optional auth layer scaffolded in PR1. ``AUTH_MODE=oidc`` becomes a real, working choice — Entra is just a config preset (its discovery URL is the only thing distinguishing it from Google / Okta / Auth0 / Keycloak / generic OIDC). Backend - src/auth/providers/oidc_provider.py — generic OIDCAuthProvider parametrised by an OpenID Connect discovery URL. Authorisation-code flow with PKCE (we use it even with a confidential client; defence in depth). ID-token validation: signature against the IdP's JWKS (with one force-refresh retry on rotation), iss matches discovery's issuer, aud includes our client_id, exp not in the past (60s leeway), nonce matches what we minted on kickoff. Discovery + JWKS cached for an hour; rotation is handled silently. - src/auth/factory.py — swap NotImplementedError stub for real OIDCAuthProvider. Add get_auth_providers() returning a {id: provider} map so routes look up by id under multi-provider OIDC; PR3 surfaces this in the UI. - src/auth/cookies.py — set/clear helpers for the short-lived (10 min) reflow_oauth_tx cookie that carries state, nonce, PKCE verifier, and the original next_path through the IdP redirect-back. - src/auth/session.py — make_tx_serializer() with a different itsdangerous salt from the session cookie so the two can never be confused (session value can't be replayed as a tx value or vice versa). - src/auth/routes.py — new GET /api/v1/auth/login/{provider_id} kickoff and GET /api/v1/auth/callback/{provider_id} callback. Open-redirect sanitiser on ?next= rejects schemes and protocol-relative URLs. /auth/config now iterates all configured providers (Phase 3 will surface a chooser UI; backend already returns the list). Dependencies - Add joserfc>=1.0.0 (JWT validation; authlib.jose was deprecated by the authlib maintainers in favour of joserfc). - Add respx>=0.21.1 to dev deps (httpx-native IdP mock for tests). Tests - tests/unit/auth/test_oidc_provider.py (13 tests) — PKCE round-trip, state tampering, provider-id binding, nonce/aud/iss mismatch, expired ID token, aud-as-list accepted, token-endpoint error surfaces, JWKS rotation handled, login_url + Protocol-method shape. - tests/integration/auth/test_oidc_roundtrip.py (8 tests) — full kickoff→callback→/me round-trip against a respx-mocked IdP. Plus IdP-error redirect, missing-tx 400, tampered-state 400, unknown- provider 404, open-redirect sanitisation. - tests/conftest_fixtures/auth.py — auth_oidc_app/_client fixtures. Docs - New docs/how-to/configure-sso.md — end-to-end Entra walkthrough (register app, add ID-token claims, generate secret, env block, smoke test) plus discovery-URL reference table for Google/Okta/ Auth0/Keycloak. Calls out just-in-time provisioning explicitly so operators don't hunt for a "users" admin page. - docs/explanation/authentication-design.md — appended sequence diagram and design rationale: why two cookies, why PKCE with a confidential client, why we validate every claim ourselves, why JWKS rotation gets a force-refresh retry, why the open-redirect sanitiser, why no group/role gating yet. - docs/reference/authentication.md — extended cookie list with reflow_oauth_tx, endpoint table now lists OIDC routes as live, env matrix updated. - AGENTS.md — Common workflows row. Test totals: 579 unit + 36 auth integration, all green. No regressions against PR1's tests. Live verification against a real IdP requires a registered app at the target IdP — that's part of the UIC rollout (the user side), not PR2 sign-off. The respx-mocked integration test exercises the same code path end-to-end against the live route handlers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Blake Bertuccelli-Booth Blake Bertuccelli-Booth committed on May 7, 2026, 08:04 PM
Showing 14 changed files +1552 additions -24 deletions
Browse files at this commit →