feat: replace gorilla/sessions with alexedwards/scs/v2

Browser sessions are now server-side, stored in the SQLite database via
scs/sqlite3store (new `sessions` table, DB migration to version 12) instead
of gorilla/sessions client-side cookie storage. Only an opaque random token
is kept in the cookie; session data lives server-side and survives restarts.

Session middleware is wired as a hybrid to avoid buffering large responses:
scs.LoadAndSave on the login/logout write paths, and a non-buffering
read-only LoadSession middleware on the secured/config/frontend read paths
so the large GraphQL /query responses stream unbuffered. JWT-only APIs
(/api, /userapi, /api/metricstore) and static files are left unwrapped.

The session cookie Secure flag is now derived from the server config (set
when cc-backend terminates TLS itself); previously it was effectively never
set. The SESSION_KEY env var is removed as server-side tokens need no
signing secret. The dormant Bearer-JWT branch in the frontend urql client
is removed; the web UI authenticates GraphQL via the session cookie.

Closes #558

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: b51075f43cc7
This commit is contained in:
2026-06-17 07:54:26 +02:00
parent 3bfd3d06ca
commit 2b01b57495
15 changed files with 183 additions and 118 deletions

View File

@@ -23,9 +23,6 @@ const envString = `
# You can generate your own keypair using the gen-keypair tool
JWT_PUBLIC_KEY="kzfYrYy+TzpanWZHJ5qSdMj5uKUWgq74BWhQG6copP0="
JWT_PRIVATE_KEY="dtPC/6dWJFKZK7KZ78CvWuynylOmjBFyMsUWArwmodOTN9itjL5POlqdZkcnmpJ0yPm4pRaCrvgFaFAbpyik/Q=="
# Some random bytes used as secret for cookie-based sessions (DO NOT USE THIS ONE IN PRODUCTION)
SESSION_KEY="67d829bf61dc5f87a73fd814e2c9f629"
`
const configString = `

View File

@@ -121,6 +121,7 @@ func (s *Server) init() error {
}
authHandle := auth.GetAuthInstance()
sessionManager := authHandle.SessionManager()
// Middleware must be defined before routes in chi
s.router.Use(func(next http.Handler) http.Handler {
@@ -220,10 +221,12 @@ func (s *Server) init() error {
})
}
s.router.Post("/login", authHandle.Login(loginFailureHandler).ServeHTTP)
s.router.HandleFunc("/jwt-login", authHandle.Login(loginFailureHandler).ServeHTTP)
// Login/logout mutate the session, so they are wrapped with
// scs.LoadAndSave, which commits the session and writes the cookie.
s.router.Post("/login", sessionManager.LoadAndSave(authHandle.Login(loginFailureHandler)).ServeHTTP)
s.router.Handle("/jwt-login", sessionManager.LoadAndSave(authHandle.Login(loginFailureHandler)))
s.router.Post("/logout", authHandle.Logout(
s.router.Post("/logout", sessionManager.LoadAndSave(authHandle.Logout(
http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
rw.Header().Add("Content-Type", "text/html; charset=utf-8")
rw.WriteHeader(http.StatusOK)
@@ -234,7 +237,7 @@ func (s *Server) init() error {
Build: buildInfo,
Infos: info,
})
})).ServeHTTP)
}))).ServeHTTP)
}
if flagDev {
@@ -246,6 +249,10 @@ func (s *Server) init() error {
// Secured routes (require authentication)
s.router.Group(func(secured chi.Router) {
if !config.Keys.DisableAuthentication {
// Non-buffering session load: makes the session available to
// AuthViaSession without wrapping/buffering the (potentially large,
// e.g. GraphQL /query) response.
secured.Use(authHandle.LoadSession)
secured.Use(func(next http.Handler) http.Handler {
return authHandle.Auth(
next,
@@ -309,6 +316,7 @@ func (s *Server) init() error {
// the /config page route that is registered in the secured group)
s.router.Group(func(configapi chi.Router) {
if !config.Keys.DisableAuthentication {
configapi.Use(authHandle.LoadSession)
configapi.Use(func(next http.Handler) http.Handler {
return authHandle.AuthConfigAPI(next, onFailureResponse)
})
@@ -319,6 +327,7 @@ func (s *Server) init() error {
// Frontend API routes
s.router.Route("/frontend", func(frontendapi chi.Router) {
if !config.Keys.DisableAuthentication {
frontendapi.Use(authHandle.LoadSession)
frontendapi.Use(func(next http.Handler) http.Handler {
return authHandle.AuthFrontendAPI(next, onFailureResponse)
})