Authentication and sessions
Login flow
Clumsies uses the organization's OpenID Connect provider. Desktop starts the flow in the system browser and listens on an ephemeral 127.0.0.1 port for the final authorization code. Client and provider exchanges both use PKCE with S256; state and OIDC nonce are validated before Server returns a short-lived client authorization code.
Before starting setup or login, the native App validates and persists one normalized Server origin. Remote origins must use HTTPS; HTTP is accepted only for loopback development. Credentials, paths, queries, and fragments are rejected so authentication and API requests cannot drift to another authority.
Desktop native
-> Server /oauth2/authorization/oidc
-> organization OIDC provider
-> Server /login/oauth2/code/oidc
-> Desktop loopback callback
-> Server /api/v1/auth/token
-> daemon credential storeSwiftUI views never receive the access token or refresh token. All authenticated product requests are sent through daemon, which injects the bearer token outside the App's presentation state.
First installation
When GET /api/v1/setup reports setup_required, the App presents native setup instead of opening a Server-hosted UI. It exchanges the deployment Setup Code for an HttpOnly setup cookie, keeps the returned CSRF token in native memory, and submits the organization name, default Project, and optional email domain allowlist.
The App then starts its normal loopback authorization flow and sends the same redirect_uri, state, and S256 challenge to POST /api/v1/setup/oidc-authorizations. After provider verification, Server creates the organization, first Owner, default Project, identity binding, and initial Refs in one transaction. It returns a client authorization code to the loopback callback; the App exchanges that code with its verifier and installs the token pair in daemon. Setup never creates a browser admin session, and a completed installation cannot be claimed again.
Provider verification
Server discovers provider metadata and JWKS from the configured issuer and requires the discovery document to return that exact issuer. Authorization code exchange failures use oidc_code_exchange_failed; issuer, audience, nonce, expiry, and signature failures use oidc_id_token_invalid.
When an ID Token references an unknown signing key, Server refreshes discovery and JWKS once, then verifies the already received token again. It does not redeem the one-time provider authorization code a second time. A signature failure against a matching key is rejected without a refresh retry.
Member admission
An organization owner or admin creates a member admission record through the admin API. On first successful OIDC login, Server matches the verified email and binds the stable (issuer, subject) identity to that record. Later logins resolve the external identity directly, so an email claim change cannot move the identity to another user. Unknown, disabled, unverified, or disallowed-domain identities are rejected.
Only native first-installation setup creates the initial Owner. Every later member must be admitted by an existing organization owner or administrator.
Token lifecycle
Server issues opaque access and refresh tokens. Only hashes are stored in PostgreSQL. Refresh is rotating: the presented refresh token is revoked and a new access/refresh pair is returned.
Daemon handles a 401 Unauthorized by attempting one refresh and one request retry. It does not expose secrets through health, project config, IPC response, or renderer state.
The in-App Administration surface uses the same bearer credentials through daemon. If daemon cannot start, Administrator Recovery performs the native OIDC flow directly against the trusted Server origin and keeps its short-lived credentials in App memory only; it can inspect health, repair member access, and revoke tokens without creating a cookie session.
Sign-out calls DELETE /api/v1/auth/session, revokes the active session, and clears local credentials.
Required Server configuration
| Variable | Meaning |
|---|---|
CLUMSIES_PUBLIC_ORIGIN | canonical HTTPS Server origin; loopback HTTP is allowed for local development |
CLUMSIES_OIDC_ISSUER | exact organization OIDC issuer |
CLUMSIES_OIDC_CLIENT_ID | OIDC confidential client ID |
CLUMSIES_OIDC_CLIENT_SECRET | OIDC confidential client secret |
CLUMSIES_CLIENT_REDIRECT_URIS | comma-separated additional trusted client callbacks |
Server derives the provider callback at /login/oauth2/code/oidc from CLUMSIES_PUBLIC_ORIGIN. Native client callbacks remain independently allowlisted so Server never redirects an authorization code to an untrusted origin.
For Desktop dynamic loopback ports, configure CLUMSIES_CLIENT_REDIRECT_URIS=http://127.0.0.1/callback. The missing port is a deliberate template: Server accepts any ephemeral port only for that exact loopback host, scheme, path, and query.
Remote Server URLs used by Desktop must be HTTPS. Plain HTTP is accepted only for loopback development addresses.
Local credential storage
Daemon stores one generic-password item in macOS Keychain. The service is ai.clumsies, the account is server-session, and the encrypted value contains the Server URL plus the access/refresh token pair. The Server URL binds the credentials to one endpoint; daemon refuses to load a Keychain session when it does not match the configured Server.
SQLite stores only non-secret Server and project configuration. Login and token refresh replace the Keychain value as one record, while clearing the daemon session or an invalid refresh session deletes it. There is no SQLite or plaintext-file credential fallback. Tests inject an isolated credential store; a separately gated smoke test exercises the real macOS Keychain.