Authentication
Login flow, session cookies, the bearer-key alternative, the CSRF model, and rate limiting.
Mosaic accepts two forms of credential on every gated endpoint:
- Argon2id password with a server-issued session cookie.
- Bearer API key as
Authorization: Bearer <key>(or?key=<key>for WebSocket upgrades).
A request passes auth if either check succeeds.
Login flow (cookie sessions)
POST /api/login
Content-Type: application/json
{
"username": "admin",
"password": "<plaintext>"
}
Responses
| Status | Body | Notes |
|---|---|---|
| 200 | {"ok": true} |
Sets mosaic_session=<token> cookie. 12-hour TTL. |
| 400 | {"error": "<msg>"} |
Body could not be decoded as JSON. |
| 401 | {"error": "invalid credentials"} |
Username or password did not match. |
| 429 | {"error": "too many login attempts"} |
Per-IP rate limiter tripped. Retry-After: 12 set. |
The cookie is HttpOnly, SameSite=Strict, Secure (when HTTPS), Path=/. Subsequent requests over the same browser context attach it automatically; scripts can extract it from the Set-Cookie response header.
Logout
POST /api/logout
Deletes the session server-side and clears the cookie. Idempotent — calling it without a session is still 200.
Bearer API key
The API key is a single rotatable token stored in the SQLite settings table. View it in Settings → Web Interface in the desktop UI, or rotate via:
POST /api/settings/web/api_key/rotate
→ 200 {"api_key": "<new-token>"}
The token is a 32-byte cryptographic random encoded with base64.RawURLEncoding (URL-safe alphabet, no padding) — characters come from [A-Za-z0-9-_].
Once rotated, the previous key is immediately invalid. Every subsequent gated request must use the new key (or a still-valid cookie session).
Using the key
curl -k https://localhost:8080/api/torrents \
-H "Authorization: Bearer $MOSAIC_API_KEY"
For WebSocket upgrades that can’t set headers (browser WebSocket() constructor):
const ws = new WebSocket(`wss://localhost:8080/api/ws?key=${encodeURIComponent(key)}`);
Setting the password
The password must be set before you can log in.
PUT /api/settings/web/password
Content-Type: application/json
{ "password": "<new-plaintext>" }
Side effect: every active session is revoked, so any browser still holding a pre-change cookie is forced to re-log in.
This route is itself gated — first-time provisioning is done through the desktop UI, which calls the same Service method without going through HTTPS.
CSRF model
Cookie-authed state-changing requests (POST/PUT/DELETE/PATCH) go through OriginGuard. The middleware:
- Skips bearer-keyed requests entirely. A
Authorization: Bearer ...header (or?key=...) means the caller is a script, not a browser auto-attaching a cookie — there’s no CSRF vector to defend against. - For everyone else, requires the
Originheader’s host to match theHostheader. Falls back toRefererifOriginis absent. If both are absent, allows the request —SameSite=Stricton the cookie means a real browser-mediated CSRF wouldn’t have the cookie attached anyway, and the absence ofOriginis what most non-browser clients send.
Practically:
- Browser SPA running same-origin: works. The browser auto-attaches
Origin: https://host:portmatchingHost. - curl/scripts with bearer auth: works. Bypasses the guard.
- curl/scripts with cookie auth: works as long as Origin/Referer are absent or matching. Don’t forge a mismatched Origin.
- A malicious cross-origin page: blocked. The browser’s auto-attached
Originis the attacker’s host, which won’t match Mosaic’s Host. SameSite=Strict prevents the cookie from being attached in the first place; this is defense in depth.
A 403 response means you fell into the cookie-authed cross-origin bucket: {"error": "origin mismatch"}.
Rate limiting
POST /api/login is rate-limited per source IP:
| Parameter | Value |
|---|---|
| Refill interval | 12 seconds |
| Burst | 5 attempts |
| Idle bucket eviction | 10 minutes |
| Janitor period | 5 minutes |
When tripped, the response is 429 Too Many Requests with Retry-After: 12. No other route is currently rate-limited — the API key is your throughput control.
Session lifetime
| Property | Value |
|---|---|
| TTL | 12 hours from issue |
| Storage | In-memory (resets on process restart) |
| Cap | 100 sessions; oldest evicted on overflow |
| Revocation | Set new password, change username, or RevokeAll() |
| Token shape | 32-byte cryptographic random, base64.RawURLEncoding |
There is no refresh endpoint. Re-login when the cookie expires.
Putting it together
# 1) Login (one-shot, gets a cookie)
curl -k -c cookies.txt -X POST https://localhost:8080/api/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"hunter2"}'
# 2) Subsequent calls reuse the cookie
curl -k -b cookies.txt https://localhost:8080/api/torrents
# 3) Or skip login entirely with a bearer key
curl -k https://localhost:8080/api/torrents \
-H "Authorization: Bearer $MOSAIC_API_KEY"