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).

The web UI ("the dashboard") authenticates by emailing a short-lived code:

  1. User enters their email at /login.
  2. We email a six-character alphanumeric code (A4F-9B2) plus a clickable link.
  3. User pastes the code in the same browser session, or clicks the link.
  4. 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>:

PrefixScopeIssued byRotation
ds_live_One device's ingest endpoints onlyDashboard, add-device flowAny time, dashboard
ua_live_Account management endpoints; user-scopedDashboard, account settingsAny time, dashboard
whk_Inbound webhook (TTN, Shelly URL action)Dashboard, add-device flowAny 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 Tokens page 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.

  • 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.