Files
shorefront/docs/plans/2026-03-01-keycloak-sso-design.md

2.9 KiB

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