# OIDC Provider Implementation ## Overview Minimal OpenID Connect 1.0 provider implementation for Paskia, enabling third-party apps to authenticate users via passkeys. ## Database Changes ### User model additions (in `db/structs.py`) - `email: str | None` — omit if None - `preferred_username: str` — initially derived from full name, require unique and non-empty ### New OIDC-specific models ```python class OIDClient(msgspec.Struct): uuid: UUID client_secret_hash: bytes name: str redirect_uris: list[str] created_at: datetime def verify_secret(self, secret: str) -> bool: ... class OIDAuthCode(msgspec.Struct): code: str # dict key, secure random client: UUID user: UUID redirect_uri: str scope: str nonce: str | None code_challenge: str | None # PKCE code_challenge_method: str | None created_at: datetime expires_at: datetime # 10 minutes @classmethod def create(cls, ...) -> OIDAuthCode: ... ``` ### DB additions ```python oid_clients: dict[UUID, OIDClient] = {} oid_auth_codes: dict[str, OIDAuthCode] = {} ``` ## Endpoints ### Well-known (root level) - `GET /.well-known/openid-configuration` — Discovery document - `GET /.well-known/jwks.json` — Public keys for token verification ### OIDC routes (`/auth/oid/`) - `POST /auth/oid/token` — Token endpoint (code exchange) - `GET /auth/oid/userinfo` — UserInfo endpoint (bearer token) ### Authorization (via existing restricted app) - `GET /auth/restricted/?client_id=...&redirect_uri=...&response_type=code&scope=openid...` The restricted app detects OIDC params from URL and handles authentication via WebSocket. ## JWT & Signing - RSA keypair generated on first boot (stored in data directory) - ID tokens signed with RS256 - `kid` in JWKS for key rotation support - Access tokens are signed JWTs (not opaque) ## Scopes & Claims **Standard OIDC scopes** — implicit for authenticated users: - `openid` — required, enables ID token - `profile` — includes `name`, `preferred_username` - `email` — includes `email` (if set) **Paskia permissions as claims** — user's effective permission scopes included automatically: ```json { "sub": "user-uuid", "name": "Alice", "preferred_username": "alice", "email": "alice@example.com", "permissions": ["admin", "reports:view"] } ``` ## Authorization Flow The `/auth/restricted/` page handles OIDC authorization alongside normal iframe auth: 1. Client redirects to `/auth/restricted/?client_id=...&redirect_uri=...&response_type=code&scope=openid&state=...` 2. Frontend detects OIDC params from `window.location.search` 3. User authenticates via passkey 4. Frontend passes raw query string to `/auth/ws/authenticate?{query_string}` 5. WebSocket validates client/redirect_uri, authenticates user, creates auth code 6. WebSocket returns `{"redirect_url": "redirect_uri?code=...&state=..."}` 7. Frontend redirects to the URL 8. Client exchanges code at `/auth/oid/token` → receives `id_token` + `access_token` 9. Optionally calls `/auth/oid/userinfo` with bearer token **Key design points:** - No session created during OIDC auth (stateless for the OIDC client) - Raw query string preserved throughout (no parsing/reconstruction of redirect_uri) - Redirect URI validated against client's registered URIs via exact string match - PKCE supported (S256 and plain methods) ## WebSocket OIDC Mode The `/auth/ws/authenticate` WebSocket accepts optional OIDC query params: - `client_id` — OIDC client UUID - `redirect_uri` — exact registered redirect URI - `scope` — must include `openid` - `state`, `nonce` — passed through - `code_challenge`, `code_challenge_method` — for PKCE When OIDC params present: - Validates client and redirect_uri before authentication - Creates auth code after successful passkey auth - Returns `{"redirect_url": "..."}` instead of session token When OIDC params absent: - Normal authentication flow with session creation ## Files ### Created - `paskia/db/structs.py` — Added `OIDClient`, `OIDAuthCode` models; User fields `email`, `preferred_username` - `paskia/db/operations.py` — CRUD for OIDC entities - `paskia/fastapi/oid.py` — Token and userinfo endpoints - `paskia/util/oidjwt.py` — RSA key management, JWT creation, JWKS ### Modified - `paskia/fastapi/mainapp.py` — Mount OIDC app, well-known endpoints - `paskia/fastapi/ws.py` — OIDC params support in `/authenticate` - `frontend/src/utils/passkey.js` — Pass query string to authenticate - `frontend/auth/restricted/RestrictedApi.vue` — Detect OIDC from URL, handle redirect - `frontend/src/components/RestrictedAuth.vue` — Pass OIDC query string prop ## TODO - Master admin UI for client management - User UI for editing preferred_username, email, profile picture - Token revocation endpoint (optional) - Logout/session management spec (optional)