Authentication¶
Auth is ON by default (owner decision, Aug 2026): a fresh install
lands on the register/login screen, and every /api/v1 route except
health + auth sits behind a session.
First run: the admin account¶
The first account registered (username/password or OAuth) becomes
the instance admin and adopts any pre-existing unowned scans (data
scanned while auth was disabled, or via the CLI without --user); every
later account is a regular user who only sees their own scans. There
are no seeded accounts: you register them yourself.
For a local evaluation, the quickstart
shows an example: register a first account with any username and a
password you choose yourself (the password123 in the docs is only
an example, never a real credential).
Sign-in methods¶
| Method | When it appears |
|---|---|
| Username / password | Always (local auth). Passwords hashed with stdlib hashlib.scrypt: no new deps. |
| GitHub OAuth | Only when both MOBARK_GITHUB_CLIENT_ID and MOBARK_GITHUB_CLIENT_SECRET are set |
| Google OAuth | Only when both MOBARK_GOOGLE_CLIENT_ID and MOBARK_GOOGLE_CLIENT_SECRET are set |
A provider's button renders only when configured (no config → no
button → never a broken flow). Redirect URIs are derived from
MOBARK_PUBLIC_BASE_URL: never from the request:
{MOBARK_PUBLIC_BASE_URL}/api/v1/auth/oauth/{github|google}/callback
Register the app at https://github.com/settings/developers and https://console.cloud.google.com/apis/credentials with that URI.
OAuth identities resolve to accounts by (provider, oauth_id) then by verified email (which links an existing local account to the OAuth sign-in), creating a fresh account when neither matches.
Sessions¶
- The session is an HttpOnly + SameSite=Lax cookie; set
Secureautomatically whenMOBARK_COOKIE_SECURE=1(serve over TLS). - Sessions are sliding: refreshed on use, so an active session never
expires mid-work; a dormant one dies after
MOBARK_SESSION_DAYS(7). - Logout revokes the exact session row behind the cookie (idempotent).
- An
OriginCheckMiddlewareguards cross-origin requests.
Per-user isolation¶
Every scan, chat session, edit, and build is owned by the user who created it:
GET /scanslists your scans only.- A foreign scan reads as 404: byte-identical to a nonexistent scan (no existence leak). This is structural: every scan-keyed route resolves the owner from the request context.
- The first user's registration claims any legacy unowned scans; the
admin can also claim unowned rows via
POST /api/v1/auth/claim(e.g. CLI-created scans withpython -m app.cli scan --user ...).
The vault (per-user key encryption)¶
BYOK model/search API keys are stored per user under
data/users/<uid>/ and encrypted at rest (envelope encryption:
scrypt-derived KEK + AES-GCM):
- Local users: the password is the key-encryption key; the vault unlocks automatically at login.
- OAuth users: set a dedicated vault passphrase once (Settings → vault), entered once per session.
- Forgot the passphrase?
POST /api/v1/auth/vault/resetdestroys the vault and clears the stored keys (there is no way to recover keys wrapped under a lost passphrase): re-enter them after. - Host operator password reset (
python -m app.cli auth reset-password <user>) revokes every session and destroys the vault for the same reason.
Disabling auth (dev/CI)¶
MOBARK_AUTH_ENABLED=0 restores the fully-open single-user behavior
byte-for-byte: the dev/CI parity mode. The register/login
surface becomes inert (400), /auth/me returns null, and all routes
are reachable without a session.
Security notes¶
- Login failures are one message:
invalid username or password, for unknown user, wrong password, and disabled account alike (no user enumeration). - Register failures are specific (the input is the caller's own).
- Only one admin can ever exist (partial unique index on
is_admin).