Authentication
Magic-link flow, token types, rotation
OpenSense uses magic links for human authentication and bearer tokens for machine authentication. There are no passwords, no TOTP, no SAML (yet).
Human auth — magic link
The web UI ("the dashboard") authenticates by emailing a short-lived code:
- User enters their email at
/login. - We email a six-character alphanumeric code (
A4F-9B2) plus a clickable link. - User pastes the code in the same browser session, or clicks the link.
- Session cookie issued,
httpOnly,Secure,SameSite=Lax, expires after 30 days of inactivity.
The code expires after 10 minutes and is single-use. A second request for the same email within the window invalidates the first.
For email providers that strip the code from inbox previews (some spam filters do), the link suffices: clicking it sets the cookie directly.
Machine auth — bearer tokens
Three token types, all carried as Authorization: Bearer <token>:
| Prefix | Scope | Issued by | Rotation |
|---|---|---|---|
ds_live_ | One device's ingest endpoints only | Dashboard, add-device flow | Any time, dashboard |
ua_live_ | Account management endpoints; user-scoped | Dashboard, account settings | Any time, dashboard |
whk_ | Inbound webhook (TTN, Shelly URL action) | Dashboard, add-device flow | Any time |
Tokens are bcrypt-hashed at rest; the plaintext is shown once at issuance time. Lose the plaintext, rotate.
Token rotation
- All token rotations are immediate. Old token returns 401 on the next request.
- The dashboard's
Tokenspage shows last-used timestamps so you can spot abandoned devices. - Tokens older than 365 days are flagged in the dashboard as "consider rotating". They continue to work; the flag is advisory.
Endpoints
POST /v1/auth/request
curl -X POST https://api.opensense.murzin.digital/v1/auth/request \
-H "Content-Type: application/json" \
-d '{"email": "you@example.com"}'
Always returns 200 OK, regardless of whether the email exists — we do not allow account enumeration.
POST /v1/auth/verify
curl -X POST https://api.opensense.murzin.digital/v1/auth/verify \
-H "Content-Type: application/json" \
-d '{"email": "you@example.com", "code": "A4F9B2"}'
Returns:
{
"session_id": "ses_4f3c…",
"expires_at": "2026-06-16T08:22:00Z",
"user_id": "usr_b1d2…"
}
Set the session cookie via the Set-Cookie response header.
POST /v1/tokens
User-scoped: requires a session cookie or another ua_live_ token.
curl -X POST https://api.opensense.murzin.digital/v1/tokens \
-H "Authorization: Bearer ua_live_4f3c…" \
-d '{ "label": "ci-pipeline", "scope": "read" }'
Returns the plaintext token once. Save it.
Why not OAuth / SAML / passwords?
- Passwords are the worst-of-both: bad for security, bad for UX, expensive to operate (forgot-password, leaked-password databases, rotation prompts).
- OAuth via Google/Microsoft: locks small EU operators into US identity providers; awkward when the bookkeeper resigns and the Gmail account is closed.
- SAML / SSO: solid for enterprise; not in scope for solo cafés. We will add it at the Team tier.
Magic-link plus session cookies is what most modern EU SaaS in this segment uses (Linear, Stripe Atlas, Notion login). It is enough.
Session cookie details
- Name:
os_session. - Path:
/. - Domain:
opensense.murzin.digital. HttpOnly,Secure,SameSite=Lax.- Rolling 30-day expiry — every request extends it.
The cookie is not valid against the API; the API is Authorization: Bearer … only. This means a malicious site cannot CSRF an API call
even if it can read the cookie name.
CSRF protection
For state-changing requests against the dashboard's own JSON endpoints
(/app/api/…), we require a X-Requested-With: XMLHttpRequest header
and the cookie. The pair makes simple form-CSRF impossible.
Logout
POST /v1/auth/logout invalidates the session server-side and clears
the cookie. All concurrent sessions for the same user can be
invalidated via dashboard Account → Sessions → Sign out everywhere.