Tokens, signed URLs, and session URLs¶
The HTTP access token lifecycle and the two temporary-sharing URL schemes. The security semantics are in 10-security; storage is in 13-data-storage.
Tokens and revocation¶
HTTP access tokens are Branca tokens signed with the Site master token; their meaning at the proxy is in 10-security. The API contract:
POST /token/requiresduration(seconds, 1..2147483647) andidentifier([A-Za-z0-9._@-]+);permissionisrorrw, compared after lowercasing. The code's defaultronly applies to a body that fails to parse, and such a request then fails thedurationcheck: every successful creation carries an explicitpermission. The response is{token}with the expiry embedded in the Branca payload.POST /token/decode/checks revocation first (403 for a revoked token), then returns{expiration, identifier, permission, valid}and records a last-use entry asynchronously.DELETE /token/accepts{token}or a 64-character{hash}; an unknown hash answers 404.GET /token/history/returns a per-token summary (not an event log), soonest expiry first, withstatusinactive | expiring | expired | revoked;identifier,permission, andinclude_expiredfilter the query, whileexpiring_withinonly feeds theexpiringcomputation ofstatusand filters nothing.DELETE /token/history/removes one entry by hash.POST /token/history/backup/writes a gzipped JSON snapshot beside the database and reports row count and checksum;POST /token/history/restore/reads that file, replaces all rows, and rebuilds the filter (the read is capped at 64 MiB after decompression). The storage and recovery contract is in 13-data-storage.
Revocation is checked on every token decode: an in-memory Bloom filter
(10 000 capacity, 0.1% false-positive rate) seeded from the SQLite history
database at startup filters most lookups, and the database gives the
authoritative answer (api/token_revocation_bloom.go,
api/token_storage_sqlite.go). Only the decode path's last-use write is
asynchronous: it goes through a bounded queue of 256 entries (four workers)
and overflows are dropped with a warning. Token creation records nothing in
the history; revocation writes its history row synchronously inside the
request. A background job purges records older than a 7-day grace
after expiry; it runs once at startup and then daily.
Deliberate trade-off: when the history database is unavailable, revocation
fails closed — a loaded Bloom filter that does not contain the token hash
still accepts the token without a database read, but a Bloom hit or a missing
filter needs the database and the decode answers 500 — while the history,
backup, and revoke endpoints answer 503. The cost is that a token which cannot
be confirmed is refused during the outage; a revoked token cannot be used. The
refusal window does not persist: the API retries the database in the background
while it is unavailable (StartTokenDatabaseRecovery, every 30 s) and rebuilds
the revocation filter and the history endpoints when it opens, so a transient
start-up failure recovers on its own. A snapshot of the history is written to
token-history-backup.json.gz beside the database at startup and once per day
(StartTokenHistoryBackup), so POST /token/history/restore/ has a source
when the database must be rebuilt. The restore path reinitializes a database
that does not open and reloads the snapshot.
Signed and session URLs¶
POST /signed-urls/takesduration,url, andpermission(r/rw), and returns the same URL with thes/signed/r/ors/signed/rw/prefix inserted and a base58 expiry signature (query changes do not invalidate the signature).POST /signed-urls/verify/answers 204 for a valid URL and 403 for a tampered or expired one. The proxy's Lua layer calls verify during the access phase. The proxy reads the permission from the signed path (ther/rwsegment) and not from the request URI, because the signature does not cover the query; a read signed URL stays read-only even when the query string containss/signed/rw/.POST /session-urls/takesduration,permission, a requiredidentifier, and an optionalpathscope (default/,..rejected), and returns a URL carrying the Branca token unders/session/. The scheme followsX-Forwarded-Protowith anhttpsdefault. Verify answers 403 for a bad token, an expired token, or a request path outside the scope (sibling prefixes are rejected, not just..escapes); on success it returns the token payload. The proxy calls verify for every session URL request. Verify checks the scope on the path that the proxy acts on, and not on the text of the request URI. It removes the query, decodes the request path once, and resolves the dot segments and repeated slashes (the steps of the nginx$uri). A write request goes to the file API, which decodes the path once more (path handling), so the path must also be inside the scope after that second decode. Thus/A/%2e%2e/B,/A/x%2f..%2fB, and/A/%252e%252e/Bare outside the scope/A/. A path with an invalid encoding or a NUL byte answers 400. The bare session URL (.../s/session/{token}, without a path) is a request for the site root, because the proxy forwards it there. It is inside the scope only for a session without a scope. The Hub always adds the scope to the URL it creates, so a scoped session URL is not a bare URL. The scope applies to the request path only: the file API refuses amatchpattern with a..segment, so that a pattern cannot match outside of the path. Verify also checks the resolved physical path (the path with its symlinks resolved), so a symlink inside the scope that points to a file outside it is refused, for a read and for a write.