Authentication & sessions
Login is passkey-style: there is no password sent to the server. A device proves it holds its private signing key by signing a fresh server challenge.
Challenge / response
Challenge
The client sends its auth public key. The server returns a fresh random challenge (single-use, short-lived, a couple of minutes) bound to that key.
Sign
The device signs the challenge with its auth private key (hybrid: ML-DSA-65 ‖ Ed25519).
Verify → token
The server verifies the signature and, if the device is active, issues a session token.
The challenge is single-use and expires quickly, so a captured challenge can't be replayed.
Session tokens
Tokens are PASETO v4.local, symmetric, encrypted (with authentication) under a server-only key kept in instance config:
- Stateless. The server doesn't store sessions; it validates the token cryptographically on each request.
- Short-lived. ~15-minute lifetime; the client transport re-authenticates transparently when one expires.
- Audience-bound. The token carries this deployment's server URL as its audience, so a token leaked from one instance is rejected by another.
- Carries the user id, the device id, issue/expiry times, and a claim version that must match (so a token shape change cleanly invalidates old tokens).
SSO (IDP mode)
A deployment in IDP mode replaces the password onboarding above with OpenID Connect:
Auth-code + PKCE
The client runs a standard OIDC auth-code flow with PKCE (a public client, no client secret) against your IdP and obtains an ID token. Attribute providers also receive an IdP-API access token, used only to reach the IdP's own attribute store, never sent to open-secret.
Exchange for a session
The client calls IdpService.ExchangeSSOToken with the ID token. The server validates it (issuer / audience / expiry / signature against the IdP's JWKS), JIT-provisions the user + a single identity device on first login, and mints the same PASETO session token the password flow issues.
Derive the vault key
The client joins its non-server factor (factor1) with the server-held K2 to unlock the escrowed identity keypair. The server never sees factor1. See Key custody.
Challenge/Verify still exist in IDP mode (the identity device has a real auth keypair, used for token refresh); only password Signup is gated off.
Public vs. authenticated endpoints
Only a handful of operations are reachable without a valid token - everything else requires one:
| Public (no token) | Why |
|---|---|
AuthService.Signup | Create the first credential (standalone mode). |
AuthService.Challenge | Begin a login. |
AuthService.Verify | Complete a login, get a token. |
InstanceService.GetConfig | Read non-secret instance config (signups open, backup enabled, auth mode, and the public OIDC client config in IDP mode). |
IdpService.ExchangeSSOToken | Exchange a validated OIDC ID token for a session (IDP mode). |
GetConfig surfaces the deployment's auth_mode and, in IDP mode, the public OIDC client config (issuer, client id, provider kind, scopes) so the frontend can start PKCE. It never surfaces a client secret (there is none) or the K2 unlock half. IdpService.GetWrappingFactorK2 and PutEscrowedVault are session-gated, not public.
Every User, Device, and Entry RPC is authenticated, and identity comes from the token, not from the request body, so you can only ever read or change rows you own or that another user has explicitly shared with you.
Device revocation
Revoking a device (see Devices & sharing) takes effect immediately, on three fronts:
- New logins from the revoked device are refused at
Challenge/Verify, it can never obtain a fresh token. - Signed commands (delete, rename, share, …) re-verify device trust, so a revoked device can't issue them even while holding a still-valid token.
- Plain reads (
Get/List), even though tokens are stateless, the auth interceptor runs a per-request device-active check, so a revoked device's existing token is rejected on its very next request rather than lingering until the token expires.
Although session tokens are stateless (no server-side session table), the interceptor consults the device's revoked_at on every authenticated request. A compromised-then-revoked device therefore loses all access - reads included, on its next request, not after the ~15-minute token lifetime. The token's short TTL only bounds how long an unrevoked, idle session stays valid before re-authenticating.