Files
paskia/oidc.md
T

4.8 KiB

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

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

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:

{
  "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)