From 40113bc6347b7f922b8a59a8dd7af3736fa9265d Mon Sep 17 00:00:00 2001 From: "Adrian A. Baumann" Date: Sun, 1 Mar 2026 00:37:49 +0100 Subject: [PATCH] docs: add Keycloak SSO integration design --- docs/plans/2026-03-01-keycloak-sso-design.md | 75 ++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 docs/plans/2026-03-01-keycloak-sso-design.md diff --git a/docs/plans/2026-03-01-keycloak-sso-design.md b/docs/plans/2026-03-01-keycloak-sso-design.md new file mode 100644 index 0000000..89532a3 --- /dev/null +++ b/docs/plans/2026-03-01-keycloak-sso-design.md @@ -0,0 +1,75 @@ +# Keycloak SSO Integration Design + +**Goal:** Replace local username/password auth with Keycloak OIDC using a backend callback flow. + +**Architecture:** Backend acts as OIDC confidential client. Browser is redirected to Keycloak, which redirects back to a backend callback endpoint. Backend validates the token, checks group membership, provisions the user, and issues its own httpOnly JWT cookie. Frontend is unchanged except the login page. + +**Tech Stack:** authlib (OIDC), FastAPI, Alembic, React + +--- + +## Auth Flow + +``` +Browser → GET /api/auth/oidc/login + → backend generates state, stores in short-lived cookie, redirects to Keycloak + +Keycloak → user authenticates → redirects to: + GET /api/auth/oidc/callback?code=...&state=... + +Backend: + 1. Validates state cookie (CSRF protection) + 2. Exchanges code for tokens via Keycloak token endpoint + 3. Validates ID token signature via Keycloak JWKS (authlib handles this) + 4. Checks groups claim for "firewall admins" → 403 if absent + 5. Looks up user by keycloak_sub → auto-provisions row if first login + 6. Issues httpOnly JWT cookie (same mechanism as before) + 7. Redirects browser to / +``` + +Removed endpoints: `POST /auth/login`, `POST /auth/register` +Kept endpoints: `GET /auth/me`, `POST /auth/logout` + +## Data Model + +New Alembic migration: +- Add `keycloak_sub VARCHAR(255) UNIQUE` to `users` table +- Make `hashed_password` nullable (always NULL for SSO users; kept for schema stability) + +## Configuration + +**ConfigMap** (non-secret): +- `KEYCLOAK_URL`: `https://sso.baumann.gr` +- `KEYCLOAK_REALM`: `homelab` +- `KEYCLOAK_CLIENT_ID`: `shorefront` + +**Secret** (added to `scripts/create-secrets.sh`): +- `KEYCLOAK_CLIENT_SECRET` + +**Redirect URI** (backend callback, registered in Keycloak): +- `https://shorefront.baumann.gr/api/auth/oidc/callback` + +## Backend Changes + +- Add `authlib` + `httpx` to `requirements.txt` +- Add `keycloak_url`, `keycloak_realm`, `keycloak_client_id`, `keycloak_client_secret` to `Settings` +- Add `keycloak_sub` column to `User` model +- New migration: add `keycloak_sub`, make `hashed_password` nullable +- Replace `backend/app/api/auth.py` with OIDC endpoints: + - `GET /auth/oidc/login` — generate state, redirect to Keycloak + - `GET /auth/oidc/callback` — exchange code, validate token, check group, provision user, set cookie, redirect + - Keep `POST /auth/logout`, `GET /auth/me` +- Remove `hash_password`, `verify_password` from `auth.py` + +## Frontend Changes + +- `Login.tsx`: replace username/password form with a single "Sign in with SSO" button (`window.location.href = '/api/auth/oidc/login'`) +- All other components unchanged + +## Keycloak Manual Setup (pre-deploy) + +1. Create client `shorefront`, access type: confidential +2. Set Valid Redirect URIs: `https://shorefront.baumann.gr/api/auth/oidc/callback` +3. Set Web Origins: `https://shorefront.baumann.gr` +4. Add Group Membership mapper on client: include groups in ID token, claim name `groups` +5. Create group `firewall admins`, add users to it