Files
paskia/oidc.md
T
LeoVasanko 49e37f9afe Docs: rewrite MultiSite.md from the webadmin perspective; refresh oidc.md
MultiSite.md now describes the shipped feature for administrators —
managing domains in the admin panel, sign-in sites, the auth host,
related origins, cross-domain sign-in, OIDC discovery, CLI and 1.x
upgrade notes — instead of the internal design narrative. oidc.md:
realms -> domains, instance-global provider, current auth-code shapes.
2026-09-07 13:33:48 +00:00

100 lines
4.5 KiB
Markdown

# OIDC Provider Implementation
OpenID Connect 1.0 provider enabling third-party apps to authenticate users via passkey. Also supports native cookie-based authentication.
## Domains (multi rp-id)
The OIDC provider is instance-global: one signing key (`oidc.key` in the transaction log) and one client set for the whole instance, usable through every configured domain. Discovery, keys, token and userinfo endpoints resolve the issuer from the request host (domain dispatch), so every configured host is an issuer alias sharing the one key. `Session.issuer` records the issuing origin (scheme included, stamped from the WS Origin) so refresh and back-channel logout produce the right `iss`; `Session.rp_id` records the owning domain for display. `CookieCode` is stamped with the session's rp-id and verified at redemption; `OIDCCode` is not, since the provider is instance-global.
## Data Models
**User** — Added: `email`, `preferred_username`
**Session** — Added: `client_uuid` (None = native, set = OIDC), `issuer` (origin that issued the session), `rp_id` (owning domain, display only)
- `key: bytes` — hashed DB key, never stored raw
- `secret``hash_secret("session", secret)` → DB lookup
- OIDC `sid``base64url.encode(hash_secret("oidc", session.key))`
**OIDClient**`uuid, client_secret_hash, name, redirect_uris`
## Auth Codes (In-Memory Only)
60-second lifetime, auto-cleaned. Two separate stores keep the OIDC and cookie flows isolated:
```python
from paskia.authcode import CookieCode, OIDCCode, store_cookie, store_oidc
class OIDCCode(msgspec.Struct):
session_key: str # Session DB key
created: datetime
redirect_uri, scope: str
nonce, code_challenge: str | None # PKCE S256 when provided
class CookieCode(msgspec.Struct):
session_key: str
created: datetime
rp_id: str # domain the code was issued in; checked at redemption
```
Usage: `code = store_oidc(OIDCCode(...))` → later popped from `oidc_codes` / `cookie_codes`.
## Authorization Flows
### OIDC (Authorization Code)
1. `GET /auth/restricted/oidc?client_id=UUID&redirect_uri=...&scope=openid&nonce=...&code_challenge=...`
2. Frontend → WebSocket: `/auth/ws/authenticate?client_id=...&redirect_uri=...&...`
3. Validate client/redirect_uri, authenticate via passkey
4. `db.oidc_login()``(secret, session_key)`
5. Create `AuthCode(session_key, oidc=OIDC(...))` → code
6. Return: `{"redirect_url": "{redirect_uri}?code={code}&state={state}"}`
7. Client exchanges code at `/auth/oidc/token` with `code_verifier` (PKCE S256)
**Token:** `access_token, id_token, refresh_token={secret}, expires_in=3600`
**ID token:** `sub, sid (base64url), name, preferred_username, email, groups`
### Native (Cookie)
1. WebSocket: `/auth/ws/authenticate` (no OIDC params)
2. Authenticate via passkey
3. `db.login()` → secret
4. Create `AuthCode(session_key=secret, oidc=None)` → exchange_code
5. Return: `{"user": "UUID", "exchange_code": "..."}`
6. `POST /auth/api/exchange` with code → sets cookie
## Refresh & Logout
**Refresh:** `POST /auth/oidc/token` with `grant_type=refresh_token&refresh_token={secret}&client_id=...&client_secret=...`
- Looks up session, validates client match
- Extends expiry +24h (sliding window)
- Returns new tokens with same `sid`
**Back-channel logout:** `POST /auth/oidc/backchannel-logout` with `logout_token={jwt}`
- Verify signature, extract `sid` or `sub`
- Delete matched sessions
- Return 200 OK
Discovery: `backchannel_logout_supported: true`
## Endpoints
- `GET /.well-known/openid-configuration` — Discovery
- `GET /auth/oidc/keys` — Keys (EdDSA)
- `POST /auth/oidc/token` — Exchange/refresh
- `GET /auth/oidc/userinfo` — User (bearer token, includes `picture` when `profile` scope is granted and avatar exists)
- `POST /auth/oidc/backchannel-logout` — Logout
- `POST /auth/api/exchange` — Native auth code → cookie
## Claims
- `profile` scope may include `name`, `preferred_username`, and `picture`
- `email` scope may include `email`
- `groups` is emitted from client-scoped permissions
## Files
**Created:** [paskia/authcode.py](paskia/authcode.py), [paskia/util/crypto.py](paskia/util/crypto.py), [paskia/fastapi/oid.py](paskia/fastapi/oid.py)
**Modified:** [paskia/db/structs.py](paskia/db/structs.py), [paskia/db/operations.py](paskia/db/operations.py), [paskia/fastapi/ws.py](paskia/fastapi/ws.py), [paskia/fastapi/api.py](paskia/fastapi/api.py), [paskia/domains.py](paskia/domains.py), [paskia/fastapi/mainapp.py](paskia/fastapi/mainapp.py)