Skip to content

Commit 2b01b57

Browse files
moebiusband73claude
andcommitted
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
1 parent 3bfd3d0 commit 2b01b57

15 files changed

Lines changed: 184 additions & 119 deletions

File tree

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
TARGET = ./cc-backend
22
FRONTEND = ./web/frontend
3-
VERSION = 1.5.4
3+
VERSION = 1.6.0
44
GIT_HASH := $(shell git rev-parse --short HEAD || echo 'development')
55
CURRENT_TIME = $(shell date +"%Y-%m-%d:T%H:%M:%S")
66
LD_FLAGS = '-s -X main.date=${CURRENT_TIME} -X main.version=${VERSION} -X main.commit=${GIT_HASH}'

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,21 @@ ln -s <your-existing-job-archive> ./var/job-archive
152152
./cc-backend -help
153153
```
154154

155+
### Authentication and sessions
156+
157+
Browser sessions are stored server-side in the SQLite database (the `sessions`
158+
table) using [`alexedwards/scs`](https://github.com/alexedwards/scs); only an
159+
opaque random token is kept in the session cookie. No cookie-signing secret is
160+
required, so the former `SESSION_KEY` environment variable is no longer used and
161+
can be removed from your `.env`.
162+
163+
The session cookie's `Secure` flag is set automatically when cc-backend serves
164+
HTTPS itself (i.e. `https-cert-file` and `https-key-file` are configured in
165+
`config.json`); otherwise it is left unset so that plain-HTTP development works.
166+
For production deployments, serve cc-backend over HTTPS so the session cookie is
167+
marked `Secure`. If you terminate TLS at a reverse proxy, prefer letting
168+
cc-backend serve HTTPS directly for now so the flag is applied.
169+
155170
## Database Configuration
156171

157172
cc-backend uses SQLite as its database. For large installations, SQLite memory

ReleaseNotes.md

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,48 @@
1-
# `cc-backend` version 1.5.4
1+
# `cc-backend` version 1.6.0
2+
3+
Supports job archive version 3 and database version 12.
4+
5+
This release replaces the browser session implementation and requires a database
6+
migration to version 12. Run `./cc-backend -migrate-db` after upgrading; the new
7+
`sessions` table is created automatically (a fresh `-init-db` also creates it).
8+
Existing login sessions are invalidated by the upgrade, so users have to log in
9+
again once. See the behavior changes below regarding the session cookie `Secure`
10+
flag and the removal of the `SESSION_KEY` environment variable.
11+
For release specific notes visit the [ClusterCockpit Documentation](https://clusterockpit.org/docs/release/).
12+
13+
## Changes in 1.6.0
14+
15+
### Behavior changes
16+
17+
- **Session backend replaced (`gorilla/sessions``alexedwards/scs`)**: Browser
18+
sessions are now managed by `github.com/alexedwards/scs/v2` with server-side
19+
storage in the SQLite database (new `sessions` table, database version 12)
20+
instead of `gorilla/sessions` client-side cookie storage. Only an opaque random
21+
token is stored in the cookie; the session payload lives server-side. Sessions
22+
still survive backend restarts. This requires the database migration noted above
23+
and invalidates existing sessions.
24+
- **Session cookie `Secure` flag**: The `Secure` attribute on the session cookie
25+
is now derived from the server configuration — it is set when cc-backend
26+
terminates TLS itself (`https-cert-file` and `https-key-file` configured) and
27+
unset otherwise. Previously the flag was effectively never set. Deployments that
28+
terminate TLS at a reverse proxy and want the `Secure` flag should serve
29+
cc-backend over HTTPS directly for now.
30+
- **`SESSION_KEY` removed**: The `SESSION_KEY` environment variable is no longer
31+
used and should be removed from your `.env`. Server-side sessions use random
32+
tokens, so no cookie-signing secret is required. It is ignored if left in place.
33+
34+
### Dependencies
35+
36+
- **Added** `github.com/alexedwards/scs/v2` and
37+
`github.com/alexedwards/scs/sqlite3store`.
38+
- **Removed** `github.com/gorilla/sessions` (and `github.com/gorilla/securecookie`).
39+
40+
## Changes in 1.5.4
241

342
Supports job archive version 3 and database version 11.
443

5-
This is a security and bugfix release of `cc-backend`, the API backend and
44+
This was a security and bugfix release of `cc-backend`, the API backend and
645
frontend implementation of ClusterCockpit.
7-
For release specific notes visit the [ClusterCockpit Documentation](https://clusterockpit.org/docs/release/).
846
If you are upgrading from v1.5.1 no database migration is required.
947
If you are upgrading from v1.5.0 you need to do another DB migration. This
1048
should not take long. For optimal database performance after the migration it is
@@ -15,8 +53,6 @@ While we are confident that the memory issue with the metricstore cleanup move
1553
policy is fixed, it is still recommended to use delete policy for cleanup.
1654
This is also the default.
1755

18-
## Changes in 1.5.4
19-
2056
### Security fixes
2157

2258
- **JWT HMAC empty-key bypass (critical)**: `jwtSession.go` now refuses to

cmd/cc-backend/init.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,6 @@ const envString = `
2323
# You can generate your own keypair using the gen-keypair tool
2424
JWT_PUBLIC_KEY="kzfYrYy+TzpanWZHJ5qSdMj5uKUWgq74BWhQG6copP0="
2525
JWT_PRIVATE_KEY="dtPC/6dWJFKZK7KZ78CvWuynylOmjBFyMsUWArwmodOTN9itjL5POlqdZkcnmpJ0yPm4pRaCrvgFaFAbpyik/Q=="
26-
27-
# Some random bytes used as secret for cookie-based sessions (DO NOT USE THIS ONE IN PRODUCTION)
28-
SESSION_KEY="67d829bf61dc5f87a73fd814e2c9f629"
2926
`
3027

3128
const configString = `

cmd/cc-backend/server.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ func (s *Server) init() error {
121121
}
122122

123123
authHandle := auth.GetAuthInstance()
124+
sessionManager := authHandle.SessionManager()
124125

125126
// Middleware must be defined before routes in chi
126127
s.router.Use(func(next http.Handler) http.Handler {
@@ -220,10 +221,12 @@ func (s *Server) init() error {
220221
})
221222
}
222223

223-
s.router.Post("/login", authHandle.Login(loginFailureHandler).ServeHTTP)
224-
s.router.HandleFunc("/jwt-login", authHandle.Login(loginFailureHandler).ServeHTTP)
224+
// Login/logout mutate the session, so they are wrapped with
225+
// scs.LoadAndSave, which commits the session and writes the cookie.
226+
s.router.Post("/login", sessionManager.LoadAndSave(authHandle.Login(loginFailureHandler)).ServeHTTP)
227+
s.router.Handle("/jwt-login", sessionManager.LoadAndSave(authHandle.Login(loginFailureHandler)))
225228

226-
s.router.Post("/logout", authHandle.Logout(
229+
s.router.Post("/logout", sessionManager.LoadAndSave(authHandle.Logout(
227230
http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
228231
rw.Header().Add("Content-Type", "text/html; charset=utf-8")
229232
rw.WriteHeader(http.StatusOK)
@@ -234,7 +237,7 @@ func (s *Server) init() error {
234237
Build: buildInfo,
235238
Infos: info,
236239
})
237-
})).ServeHTTP)
240+
}))).ServeHTTP)
238241
}
239242

240243
if flagDev {
@@ -246,6 +249,10 @@ func (s *Server) init() error {
246249
// Secured routes (require authentication)
247250
s.router.Group(func(secured chi.Router) {
248251
if !config.Keys.DisableAuthentication {
252+
// Non-buffering session load: makes the session available to
253+
// AuthViaSession without wrapping/buffering the (potentially large,
254+
// e.g. GraphQL /query) response.
255+
secured.Use(authHandle.LoadSession)
249256
secured.Use(func(next http.Handler) http.Handler {
250257
return authHandle.Auth(
251258
next,
@@ -309,6 +316,7 @@ func (s *Server) init() error {
309316
// the /config page route that is registered in the secured group)
310317
s.router.Group(func(configapi chi.Router) {
311318
if !config.Keys.DisableAuthentication {
319+
configapi.Use(authHandle.LoadSession)
312320
configapi.Use(func(next http.Handler) http.Handler {
313321
return authHandle.AuthConfigAPI(next, onFailureResponse)
314322
})
@@ -319,6 +327,7 @@ func (s *Server) init() error {
319327
// Frontend API routes
320328
s.router.Route("/frontend", func(frontendapi chi.Router) {
321329
if !config.Keys.DisableAuthentication {
330+
frontendapi.Use(authHandle.LoadSession)
322331
frontendapi.Use(func(next http.Handler) http.Handler {
323332
return authHandle.AuthFrontendAPI(next, onFailureResponse)
324333
})

configs/env-template.txt

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,5 @@ JWT_PRIVATE_KEY="dtPC/6dWJFKZK7KZ78CvWuynylOmjBFyMsUWArwmodOTN9itjL5POlqdZkcnmpJ
77
# Keys in PEM format can be converted, see `tools/convert-pem-pubkey/Readme.md`
88
CROSS_LOGIN_JWT_PUBLIC_KEY=""
99

10-
# Some random bytes used as secret for cookie-based sessions (DO NOT USE THIS ONE IN PRODUCTION)
11-
SESSION_KEY="67d829bf61dc5f87a73fd814e2c9f629"
12-
1310
# Password for the ldap server (optional)
1411
LDAP_ADMIN_PASSWORD="mashup"

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ require (
1212
github.com/ClusterCockpit/cc-lib/v2 v2.12.0
1313
github.com/ClusterCockpit/cc-line-protocol/v2 v2.4.0
1414
github.com/Masterminds/squirrel v1.5.4
15+
github.com/alexedwards/scs/sqlite3store v0.0.0-20251002162104-209de6e426de
16+
github.com/alexedwards/scs/v2 v2.9.0
1517
github.com/aws/aws-sdk-go-v2 v1.41.7
1618
github.com/aws/aws-sdk-go-v2/config v1.32.18
1719
github.com/aws/aws-sdk-go-v2/credentials v1.19.17
@@ -25,7 +27,6 @@ require (
2527
github.com/golang-jwt/jwt/v5 v5.3.1
2628
github.com/golang-migrate/migrate/v4 v4.19.1
2729
github.com/google/gops v0.3.29
28-
github.com/gorilla/sessions v1.4.0
2930
github.com/jmoiron/sqlx v1.4.0
3031
github.com/joho/godotenv v1.5.1
3132
github.com/mattn/go-sqlite3 v1.14.44
@@ -80,7 +81,6 @@ require (
8081
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
8182
github.com/goccy/go-yaml v1.19.2 // indirect
8283
github.com/google/uuid v1.6.0 // indirect
83-
github.com/gorilla/securecookie v1.1.2 // indirect
8484
github.com/gorilla/websocket v1.5.3 // indirect
8585
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
8686
github.com/influxdata/influxdb-client-go/v2 v2.14.0 // indirect

go.sum

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc
2727
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
2828
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
2929
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
30+
github.com/alexedwards/scs/sqlite3store v0.0.0-20251002162104-209de6e426de h1:c72K9HLu6K442et0j3BUL/9HEYaUJouLkkVANdmqTOo=
31+
github.com/alexedwards/scs/sqlite3store v0.0.0-20251002162104-209de6e426de/go.mod h1:Iyk7S76cxGaiEX/mSYmTZzYehp4KfyylcLaV3OnToss=
32+
github.com/alexedwards/scs/v2 v2.9.0 h1:xa05mVpwTBm1iLeTMNFfAWpKUm4fXAW7CeAViqBVS90=
33+
github.com/alexedwards/scs/v2 v2.9.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8=
3034
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
3135
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
3236
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
@@ -153,18 +157,12 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
153157
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
154158
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
155159
github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
156-
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
157-
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
158160
github.com/google/gops v0.3.29 h1:n98J2qSOK1NJvRjdLDcjgDryjpIBGhbaqph1mXKL0rY=
159161
github.com/google/gops v0.3.29/go.mod h1:8N3jZftuPazvUwtYY/ncG4iPrjp15ysNKLfq+QQPiwc=
160162
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
161163
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
162164
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
163165
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
164-
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
165-
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
166-
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
167-
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
168166
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
169167
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
170168
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
@@ -212,6 +210,7 @@ github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
212210
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
213211
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
214212
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
213+
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
215214
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
216215
github.com/mattn/go-sqlite3 v1.14.44 h1:3VSe+xafpbzsLbdr2AWlAZk9yRHiBhTBakioXaCKTF8=
217216
github.com/mattn/go-sqlite3 v1.14.44/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=

internal/api/api_test.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,6 @@ func setup(t *testing.T) *api.RestAPI {
170170

171171
archiver.Start(repository.GetJobRepository(), context.Background())
172172

173-
t.Setenv("SESSION_KEY", "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
174173
if cfg := ccconf.GetPackageConfig("auth"); cfg != nil {
175174
auth.Init(&cfg)
176175
} else {

internal/api/nats_test.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,6 @@ func setupNatsTest(t *testing.T) *NatsAPI {
156156

157157
archiver.Start(repository.GetJobRepository(), context.Background())
158158

159-
t.Setenv("SESSION_KEY", "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
160159
if cfg := ccconf.GetPackageConfig("auth"); cfg != nil {
161160
auth.Init(&cfg)
162161
} else {

0 commit comments

Comments
 (0)