seq-auth: - startAutoRefresh() -> startSSE(), stopAutoRefresh() -> stopSSE() - Cookie secure flag: 'secure (prod)' -> 'secure (if TRUST_PROXY)' component: - Fix typo creatApp -> createApp - Add GET /csrf, POST /logout to browser->auth arrow - Add GET /stream (SSE) to browser->dashboard arrow class-server: - Add subscribers Set, onPollComplete(), offPollComplete() to Poller class class-data: - Add SSE Event /stream shape alongside API Response /user-downloads - Add sser *-- dl relationship state-ui: - Fix invalid multi-line transition labels with raw Unicode arrows (broke PlantUML parser); replace with valid \n escapes on single line seq-dashboard, seq-polling, state-poller, activity-matching: - Whitespace touch to trigger render-diagrams CI workflow
105 lines
3.7 KiB
Plaintext
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 (if TRUST_PROXY), 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 (if TRUST_PROXY)
|
|
auth --> browser : { success: true, user, csrfToken }
|
|
browser -> browser : store csrfToken in memory
|
|
browser -> browser : fadeOutLogin()
|
|
browser -> browser : showDashboard()
|
|
browser -> browser : startSSE()
|
|
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 : stopSSE()
|
|
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
|