Files
sofarr/docs/diagrams/seq-auth.puml
Gronod 8c829f9651
Some checks failed
Build and Push Docker Image / build (push) Successful in 35s
CI / Security audit (push) Successful in 58s
CI / Tests & coverage (push) Failing after 1m5s
docs: audit and update all documentation to reflect current codebase
ARCHITECTURE.md:
- Node version: 18+ → 22 (Alpine)
- Tech stack: add helmet, express-rate-limit, cookie-parser, testing tools
- Directory structure: add server/app.js, verifyCsrf.js, tokenStore.js,
  sanitizeError.js, tests/, docs/, .gitea/workflows/, vitest.config.js
- §4.1: document app.js factory (createApp) vs index.js entry point;
  CSP nonce, rate limiters, CSRF middleware, trust proxy
- §4.2: add CSRF Required column; document verifyCsrf; fix auth note
- §4.3: add tokenStore.js and sanitizeError.js descriptions
- §6 Auth flow: add rememberMe, rate limiter, stable DeviceId, server-side
  token store, CSRF token issuance, correct cookie TTL (session/30d not 24h)
- §9 API: add csrfToken to login response, rememberMe field, 400/429 codes;
  add GET /api/auth/csrf endpoint; fix /me response; fix /logout CSRF note
- §11 Config: add DATA_DIR, COOKIE_SECRET, TRUST_PROXY, NODE_ENV; split
  into Core / Emby / Service Instances / Tuning sections
- §12 Deployment: update Dockerfile description to multi-stage node:22-alpine;
  add COOKIE_SECRET, TRUST_PROXY, named volume to compose example;
  add security hardening checklist; add CI/CD table

diagrams/seq-auth.puml:
- Add TokenStore participant
- Add rememberMe, CSRF token issuance, stable DeviceId note
- Add login rate limiter note
- Add GET /csrf refresh flow
- Add server-side token revocation on logout

diagrams/class-server.puml:
- Add app.js createApp() factory class
- Add verifyCsrf middleware class
- Add TokenStore and SanitizeError utility classes
- Update auth.js routes (add GET /csrf)
- Fix relationships: entry → appfn → routes

diagrams/component.puml:
- Add app.js factory component
- Add helmet, express-rate-limit components
- Add verifyCsrf middleware component
- Add tokenStore.js and sanitizeError.js utility components
- Fix wiring: entry → createApp() → mounts routes

Dockerfile:
- Fix stale comments referencing better-sqlite3 and SQLite

server/routes/auth.js:
- Fix stale comment: SQLite-backed → JSON file-backed
2026-05-17 08:05:08 +01:00

105 lines
3.7 KiB
Plaintext

@startuml seq-auth
!theme plain
title sofarr — Authentication Sequence
actor User as user
participant "Browser\n(app.js)" as browser
participant "Express\n/api/auth" as auth
participant "TokenStore\n(tokens.json)" as tokens
participant "Emby\nServer" as emby
== Page Load ==
user -> browser : Navigate to sofarr
activate browser
browser -> auth : GET /api/auth/me
activate auth
auth -> auth : Read emby_user cookie\n(signed if COOKIE_SECRET set)
alt Cookie exists and valid
auth --> browser : { authenticated: true, user: { name, isAdmin } }
browser -> auth : GET /api/auth/csrf
activate auth
auth -> auth : Generate 32-byte hex csrfToken
auth --> browser : { csrfToken } + Set csrf_token cookie
deactivate auth
browser -> browser : store csrfToken in memory
browser -> browser : showDashboard()
browser -> browser : startAutoRefresh()
browser -> browser : dismissSplash()
else No cookie / tampered
auth --> browser : { authenticated: false }
browser -> browser : dismissSplash()
browser -> browser : showLogin()
end
deactivate auth
== Login ==
user -> browser : Enter username + password\n(+ optional rememberMe checkbox)
browser -> auth : POST /api/auth/login\n{ username, password, rememberMe }
activate auth
note right of auth
Rate limiter: max 10 failed
attempts per IP / 15 min
(successful requests excluded)
end note
auth -> emby : POST /Users/authenticatebyname\n{ Username, Pw }\nX-Emby-Authorization: MediaBrowser\nDeviceId = sha256(username)[0:16]
activate emby
alt Valid credentials
emby --> auth : { User: { Id }, AccessToken }
auth -> emby : GET /Users/{userId}\nX-MediaBrowser-Token: AccessToken
emby --> auth : { Name, Policy: { IsAdministrator } }
deactivate emby
auth -> tokens : storeToken(userId, AccessToken)
note right of tokens
Stored server-side only.
Never sent to the client.
31-day TTL, atomic JSON write.
end note
auth -> auth : Set emby_user cookie\n{ id, name, isAdmin }\nhttpOnly, sameSite=strict\nsecure (prod), signed (COOKIE_SECRET)\nrememberMe=true → Max-Age 30d\nrememberMe=false → session cookie
auth -> auth : Generate csrfToken\n(32-byte random hex)
auth -> auth : Set csrf_token cookie\nhttpOnly=false (JS-readable)\nsameSite=strict, secure (prod)
auth --> browser : { success: true, user, csrfToken }
browser -> browser : store csrfToken in memory
browser -> browser : fadeOutLogin()
browser -> browser : showDashboard()
browser -> browser : startAutoRefresh()
browser -> browser : dismissSplash()
else Invalid credentials
emby --> auth : 401 Error
deactivate emby
auth --> browser : { success: false, error: "Invalid username or password" }
browser -> browser : showLoginError()
end
deactivate auth
== CSRF Token Refresh (after page reload) ==
note over browser : csrfToken lost from memory\non hard page reload
browser -> auth : GET /api/auth/csrf
activate auth
auth -> auth : Generate new csrfToken
auth --> browser : { csrfToken } + new csrf_token cookie
deactivate auth
browser -> browser : store new csrfToken in memory
== Logout ==
user -> browser : Click Logout
browser -> browser : stopAutoRefresh()
browser -> auth : POST /api/auth/logout\n(no CSRF required — auth routes\nexempt; sameSite:strict protects)
activate auth
auth -> auth : Parse emby_user cookie → user
auth -> tokens : getToken(user.id)
activate tokens
tokens --> auth : { accessToken }
deactivate tokens
auth -> emby : POST /Sessions/Logout\nX-MediaBrowser-Token: accessToken
activate emby
emby --> auth : 204 / error (ignored)
deactivate emby
auth -> tokens : clearToken(user.id)
auth -> auth : clearCookie(emby_user)\nclearCookie(csrf_token)
auth --> browser : { success: true }
deactivate auth
browser -> browser : showLogin()
deactivate browser
@enduml