Skip to content
Local Drive

Authentication

Tokens, refresh, device approval, and two factor.

Tokens

A successful sign in returns:

{
  "access_token": "...",
  "refresh_token": "...",
  "expires_at": 1730000000000,
  "session_id": "...",
  "user": { "id": "...", "username": "...", "role": "member" }
}

Send the access token as Authorization: Bearer <token>. It lasts 15 minutes by default.

The four routes that also read the token from the query string

A websocket handshake and a media element have the same problem: the browser builds the request itself and gives you nowhere to put a header. An <img> tag pointed at a private thumbnail cannot send one, so the thumbnail comes back 401 and the grid fills with placeholder icons.

Four routes therefore accept ?access_token=<token> as well as the header:

Route Loaded by
GET /api/v1/ws the websocket handshake
GET /api/v1/nodes/{id}/thumbnail an image element
GET /api/v1/nodes/{id}/preview an image element
GET /api/v1/nodes/{id}/download an image, video or audio element

Everything else takes the header only. The narrowness is the point: a token in a URL is written to browser history and to any proxy log on the way, so it is worth doing only where there is no alternative.

The same rules still apply on those four. The token must be full scope, so a device waiting for approval cannot read a byte, and the account behind it still has to have access to the node. The server sends Referrer-Policy: no-referrer so a URL carrying one is never handed to a third party.

A public share link is the exception that proves it. Those URLs are meant to be copied to people who have no account, so they carry a share token that grants one node and nothing else, and the client never signs them with a personal token.

Refreshing

POST /api/v1/auth/refresh with {"refresh_token": "..."}.

Refresh tokens rotate and are single use. A replayed one finds nothing to rotate and is rejected, which makes a stolen token useful only until the real client next refreshes.

The client refreshes once on a 401 and replays the original request. Only one refresh runs at a time; every request that hit a 401 together waits on the same result.

A device waiting for approval

When device approval is on, sign in returns 202 instead of 200:

{ "status": "pending", "session_id": "...", "access_token": "..." }

That token has a pending scope. It can call GET /api/v1/auth/session/{id}/status and nothing else; every other endpoint returns 403. Poll no faster than every four to five seconds. The moment it is approved, the same endpoint returns the real token pair.

Two factor

If the account has TOTP enabled, sign in returns 401 with code totp_required. Send the same request again with totp_code.

A single use recovery code is accepted in place of a code from the authenticator, and is consumed when used.

A temporary password

An account with must_change_password set can call PATCH /api/v1/me/password and read only endpoints. Everything else returns 403 with code must_change_password.

Errors

One envelope, every endpoint:

{ "error": { "code": "forbidden", "message": "you do not have permission to do that" } }

The message is already a plain sentence written for a person to read. The code is what to branch on.