4.8 KiB
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 Nonepreferred_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 documentGET /.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
kidin 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 tokenprofile— includesname,preferred_usernameemail— includesemail(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:
- Client redirects to
/auth/restricted/?client_id=...&redirect_uri=...&response_type=code&scope=openid&state=... - Frontend detects OIDC params from
window.location.search - User authenticates via passkey
- Frontend passes raw query string to
/auth/ws/authenticate?{query_string} - WebSocket validates client/redirect_uri, authenticates user, creates auth code
- WebSocket returns
{"redirect_url": "redirect_uri?code=...&state=..."} - Frontend redirects to the URL
- Client exchanges code at
/auth/oid/token→ receivesid_token+access_token - Optionally calls
/auth/oid/userinfowith 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 UUIDredirect_uri— exact registered redirect URIscope— must includeopenidstate,nonce— passed throughcode_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— AddedOIDClient,OIDAuthCodemodels; User fieldsemail,preferred_usernamepaskia/db/operations.py— CRUD for OIDC entitiespaskia/fastapi/oid.py— Token and userinfo endpointspaskia/util/oidjwt.py— RSA key management, JWT creation, JWKS
Modified
paskia/fastapi/mainapp.py— Mount OIDC app, well-known endpointspaskia/fastapi/ws.py— OIDC params support in/authenticatefrontend/src/utils/passkey.js— Pass query string to authenticatefrontend/auth/restricted/RestrictedApi.vue— Detect OIDC from URL, handle redirectfrontend/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)