Authentication
The API exposes two login transports: bearer tokens for native apps, scripts, and API clients, and httpOnly session cookies for browsers. Redis backs auth token storage, MFA challenge state, and rate limiting.
Authentication builds on FastAPI-Users with Relab-specific refresh-token, session-cookie, OAuth, and validation logic.
Supported flows
Section titled “Supported flows”- Email/password login with optional TOTP MFA completion for accounts that enable it
- Refresh-token rotation and logout revocation
- Password reset and email verification
- Google and GitHub OAuth login with the same optional TOTP MFA completion step
- OAuth account linking for existing users
- Disposable email checks during registration
Security controls
Section titled “Security controls”Password validation follows the OWASP Authentication Cheat Sheet. Passwords must be 12 to 128 characters and must not contain the username or email address. They are checked against a committed common-password list (seeded into Redis when available) and against Have I Been Pwned through the k-anonymity range API. The breach check fails open if Have I Been Pwned is unavailable. Stored passwords use Argon2id; Relab stores no reversible passwords.
Password inputs do not block paste, so password managers work.
Changing email or password through PATCH /v1/users/me requires current_password. Username, profile, and preference updates do not. Email changes mark the account unverified, send a verification email to the new address, and send a security notification to the previous address. Password changes send a security notification to the account email address.
TOTP MFA is optional account hardening for contributors and required for administrators:
- Accounts without TOTP receive bearer tokens or session cookies after a successful password or OAuth login.
- Accounts with TOTP receive a short-lived challenge token and must submit a current one-time code before the API issues credentials.
- Every superuser power (the
/adminroutes, moderation of other users’ products, reading private profiles) needs MFA enrolled on the superuser account. - TOTP seeds and pending challenge tokens use cryptographically secure randomness.
- Accepted TOTP counters are recorded briefly to prevent replay within the clock window.
- Wrong TOTP and recovery codes at the login challenge, and wrong passwords or codes re-entered by a signed-in user, count against one per-account budget shared by all of these routes, not only against the client IP or the challenge token.
- Administrators can reset a user’s MFA enrollment after identity-proofed recovery, but cannot choose a replacement factor.
Forgot-password returns the same accepted response for known, unknown, and inactive accounts, padded to a small minimum duration against timing-based enumeration. Login failures use the same bad-credentials response for unknown, invalid, and inactive accounts.
Rate limits use the Redis-backed limits integration:
- login by transient client-IP bucket (60 a minute, so a workshop room behind one IP can sign in) and by failed attempts against a keyed digest of the submitted identifier (3 a minute); successful logins do not count against the account, so a shared demo account is not locked by use
- forgot-password requests by transient client-IP bucket and by a keyed digest of the submitted email address
- reset-password submissions, registration, and email verification requests by transient client-IP bucket
Password reset uses email links. Reset tokens are valid for one hour and include a fingerprint of the previous password hash, so a successful change invalidates them. A successful reset revokes existing refresh tokens and sends a confirmation email without credentials, tokens, or links. The web app is served with Referrer-Policy: no-referrer and removes the token from browser history after loading the reset page.
Auth logs record successful logins and rate-limit events. They do not include plaintext passwords, reset tokens, verification tokens, refresh tokens, full verification/reset URLs, or full email addresses.
Token handling
Section titled “Token handling”sequenceDiagram accTitle: Login and token issuance flow accDescr: A user submits bearer or session login credentials to the API, which validates them against the database, updates last-login metadata, creates access and refresh tokens in Redis, and returns bearer JSON tokens or browser cookies. participant User participant API participant TokenStore as Redis participant DB as Database
User->>API: POST /v1/auth/bearer/login or /v1/auth/session/login API->>DB: Validate credentials API->>DB: Update last login metadata API->>TokenStore: Create access and refresh tokens API-->>User: Bearer JSON tokens or browser cookiesAccess tokens live 15 minutes. Auth flows require Redis and fail closed without it.
Login updates last_login_at; client IP addresses are processed only transiently for rate limiting and never stored on the account. Browser sessions receive refresh and access cookies; bearer clients receive tokens in the response body.
Refresh-token rotation uses dedicated endpoints:
POST /v1/auth/bearer/refreshfor bearer clientsPOST /v1/auth/session/refreshfor browser sessions
Refresh tokens have a 30-day inactivity lifetime and a 30-day absolute session lifetime. Each refresh rotates the token but keeps the original absolute expiry, so activity cannot extend a session beyond 30 days. An account may hold several concurrent sessions. “Sign out everywhere” on the profile screen ends all of them; there is no per-device session inventory.
Token revocation events:
- Bearer logout: revokes the submitted refresh token.
- Session logout: clears auth cookies, asks the browser to clear local session data, and blacklists the refresh token when one is present.
- Revoke all (
POST /v1/auth/sessions/revoke-all): revokes every refresh and access token for the account and clears browser session state. - Password reset, password change, email change, account deactivation, and account deletion: all revoke every refresh and access token for the affected account.
Access tokens carry their issue time, and each revocation records a per-user epoch in Redis; a token issued before that epoch is refused on its next request. Revocation is immediate, not bounded by the 15-minute lifetime. Refresh state is purged before the epoch is stamped, so a refresh racing the revocation cannot mint a token that outlives it.
Refresh tokens are random bearer secrets stored in Redis under SHA-256 fingerprints, so the keys are not usable credentials if exposed. OAuth provider tokens that must stay reversible (Google/YouTube access and refresh tokens) are encrypted with AES-256-GCM under DATA_ENCRYPTION_KEY before database storage.
Authorization is enforced in the backend route layer, not by client UI state. FastAPI dependencies separate public reads, authenticated self-service, verified-user writes, owner-scoped resources, device assertions, and superuser-only admin routes. Request bodies cannot set account-control or ownership fields, and product owner identity is redacted from viewers who cannot see the owner’s profile.
Account deletion
Section titled “Account deletion”Signed-in users delete their own account from the account screen (DELETE /v1/users/me). Like an email or password change, this requires the current password; accounts that sign in only through Google or GitHub have no password to enter. Accounts with two-step verification also enter a current authenticator code or a recovery code, whether or not they have a password. An account with neither a password nor two-step verification must have signed in within the last 10 minutes; otherwise the route returns 403 and asks the user to sign in again. Failed password and code attempts count against a per-account budget, on top of the per-IP login limit. A wrong current password returns 403 rather than 401 here and in every other password step-up, as does a wrong authenticator code when confirming or turning off two-step verification, regenerating recovery codes, or deleting the account, because the session itself is still valid and a client must not refresh and resend the guess. Deletion erases the account details (email address, username, linked sign-ins, and cameras), revokes every session, and sends a notification email to the former address. Products and their media move to an inactive anonymous system account, so they stay public without the owner’s name. That includes any text the owner wrote in product records and the original file names of their photos. Removing the uploads as well is a request to the maintainers.
Admins delete accounts through DELETE /v1/admin/users/{user_id} and can choose to delete the products instead of anonymizing them. Neither route deletes the last active superuser.
Google and GitHub support session-cookie callbacks, bearer-token callbacks, and account linking. Google accounts may be linked by verified email; GitHub linking requires explicit association. OAuth login creates Relab-owned sessions; Relab does not perform IdP-wide logout.
Backend-mediated flow
Section titled “Backend-mediated flow”Google and GitHub use the backend-mediated flow on all platforms.
Callbacks use a CSRF cookie and signed state token. Frontend redirect targets must exactly match the backend allowlist derived from APP_PUBLIC_URL (/login and /account) plus the fixed native app redirects (relab-app://login and relab-app://account). Entries are normalized as scheme://host/path and must not contain credentials, query strings, or fragments. Provider callback URLs are versioned under /v1/oauth and must be registered with each provider:
https://api.cml-relab.org/v1/oauth/google/session/callbackhttps://api.cml-relab.org/v1/oauth/google/associate/callbackhttps://api.cml-relab.org/v1/oauth/google-youtube/associate/callbackhttps://api.cml-relab.org/v1/oauth/github/session/callbackhttps://api.cml-relab.org/v1/oauth/github/associate/callback
The public backend origin comes from API_PUBLIC_URL; production uses https://api.cml-relab.org. Bearer callback routes use the same pattern, such as /v1/oauth/google/token/callback, when enabled for a client.
Callback results travel only in the URL fragment: #status=success, #status=error&error=<code>, or #status=mfa_required&mfa_handoff=<one-time-handoff>. The app reads the fragment and clears it from browser history; access tokens never travel through callback URLs. When OAuth login needs MFA, the app claims the one-time handoff through the API, keeps the pending challenge in runtime or session storage, and routes to the MFA screen. The MFA challenge token itself never appears in a query string or fragment.
sequenceDiagram accTitle: OAuth sign-in flow accDescr: The frontend requests an authorization URL from the API, redirects the user to the OAuth provider, and after the user approves, the provider calls back to the API, which exchanges the code for tokens, creates or links the user account, and redirects the frontend with a session or token response. participant User participant Frontend participant API participant Provider as OAuth Provider
User->>Frontend: Start OAuth sign-in Frontend->>API: Request authorization URL API-->>Frontend: Redirect URL + CSRF cookie Frontend->>Provider: Redirect user User->>Provider: Approve login Provider-->>API: Callback with authorization code API->>Provider: Exchange code for tokens API->>API: Create or link user account API-->>Frontend: Redirect with session/token responsePublic auth endpoints
Section titled “Public auth endpoints”Expand endpoint list
POST /v1/auth/bearer/loginPOST /v1/auth/session/loginPOST /v1/auth/mfa/totp/setupPOST /v1/auth/mfa/totp/confirmPOST /v1/auth/mfa/oauth/claimPOST /v1/auth/mfa/challengePOST /v1/auth/bearer/refreshPOST /v1/auth/session/refreshPOST /v1/auth/bearer/logoutPOST /v1/auth/session/logoutPOST /v1/auth/sessions/revoke-allPOST /v1/auth/registerPOST /v1/auth/verifyPOST /v1/auth/forgot-passwordPOST /v1/auth/reset-passwordPOST /v1/auth/validate-emailGET /v1/oauth/*
For the full live surface, see the API reference overview or open the public API reference.