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

4.5 KiB

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
  • secrethash_secret("session", secret) → DB lookup
  • OIDC sidbase64url.encode(hash_secret("oidc", session.key))

OIDClientuuid, 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:

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

  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/util/crypto.py, paskia/fastapi/oid.py

Modified: paskia/db/structs.py, paskia/db/operations.py, paskia/fastapi/ws.py, paskia/fastapi/api.py, paskia/domains.py, paskia/fastapi/mainapp.py