OAuth2 OpenID Connect provider support, API and DB refactoring (#3)
Allows Paskia to authenticate the user to a client site. - User friendly client registration flow on the admin app - Redirect-based authentication flow (per spec) - Backchannel logout both ways to keep sessions synchronized - Groups integrated with Paskia's permission system - Adds email, preferred username and telephone fields on user profile - All new user basic info layout to show the new information, better looks - API and DB structures redesigned - Various unrelated fixes to theming and layout
This commit was merged in pull request #3.
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
# OIDC Provider Implementation
|
||||
|
||||
OpenID Connect 1.0 provider enabling third-party apps to authenticate users via passkey. Also supports native cookie-based authentication.
|
||||
|
||||
## Data Models
|
||||
|
||||
**User** — Added: `email`, `preferred_username`
|
||||
|
||||
**Session** — Added: `client_uuid` (None = native, set = OIDC)
|
||||
- `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:
|
||||
|
||||
```python
|
||||
from paskia.authcode import AuthCode, OIDC, codes
|
||||
|
||||
class AuthCode(msgspec.Struct):
|
||||
session_key: str # Session DB key
|
||||
created: datetime
|
||||
oidc: OIDC | None # Only for OIDC mode
|
||||
|
||||
class OIDC(msgspec.Struct):
|
||||
redirect_uri, scope, nonce, code_challenge, code_challenge_method: str
|
||||
```
|
||||
|
||||
Usage: `code = authcode.store(AuthCode(...))` → later `codes.pop(code, None)`
|
||||
|
||||
## 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)
|
||||
- `POST /auth/oidc/backchannel-logout` — Logout
|
||||
- `POST /auth/api/exchange` — Native auth code → cookie
|
||||
|
||||
## 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/globals.py](paskia/globals.py), [paskia/fastapi/mainapp.py](paskia/fastapi/mainapp.py)
|
||||
Reference in New Issue
Block a user