Commit Graph

4 Commits

Author SHA1 Message Date
H1K0 98de298e5b feat(backend): file-scoped content tokens for media URLs
Opening an original by URL (?access_token=) baked in the 15-minute access
token, so a long video opened in a new tab stopped streaming once that token
expired mid-playback: the access token can't be refreshed in an already-opened
tab, and its next Range request 401'd.

Add a content token: a signed, single-file capability (typ=content, fid claim)
with its own longer TTL (CONTENT_TOKEN_TTL, default 6h) and — crucially — no
session id, so it survives refresh rotation and outlives the short access TTL.
POST /files/:id/content-token mints one after the same view-ACL check content
serving does; GET /files/:id/content now runs under content-aware auth that
accepts either a normal access token or a content token scoped to that file.
View permission is still enforced against the token's user, so the token only
changes when a file may be read by URL, never which files. It's a bearer
capability for that one file until expiry, hence the bounded, configurable TTL.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 17:53:10 +03:00
H1K0 4645107ea1 fix(backend): make access tokens revocable via session validation
The auth middleware trusted any unexpired, well-signed access token, so
logout, session termination and admin blocks had no effect until the
15-minute token expired. The middleware now validates that the token's
session is still active on every request (SessionRepo.GetByID), and
blocking a user deactivates all of their sessions, immediately revoking
their outstanding access tokens.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 14:09:25 +03:00
H1K0 f069fccd96 fix(backend): harden JWT handling and login
Three related auth weaknesses:

- Access and refresh tokens were structurally identical, so a 30-day
  refresh token was accepted as a bearer access token. Tokens now carry a
  "typ" claim; the access path rejects refresh tokens and /refresh rejects
  access tokens.

- Login stored the hash of a throwaway refresh token (sid=0) but returned
  a re-issued one, so the stored hash never matched and /refresh always
  401'd. Tokens are no longer re-issued: the refresh token is located by
  hash and carries no session id, while the access token embeds the real
  session id. A random jti keeps tokens unique within the same second.

- Login skipped bcrypt for unknown users (a timing oracle) and returned
  403 for blocked accounts before checking the password (leaking account
  existence). It now always runs a bcrypt comparison and verifies the
  password before disclosing blocked state.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 14:04:33 +03:00
H1K0 277f42035c feat(backend): implement auth service with JWT and session management
Login: bcrypt credential validation, session creation, JWT pair issuance.
Logout/TerminateSession: soft-delete session (is_active = false).
Refresh: token rotation — deactivate old session, issue new pair.
ListSessions: marks IsCurrent by comparing session IDs.
ParseAccessToken: for use by auth middleware.

Claims carry uid (int16), adm (bool), sid (int). Refresh tokens are
stored as SHA-256 hashes; raw tokens never reach the database.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 00:38:21 +03:00