feat: make http session cookie path & duration configurable (fixes #10522) (#10632)

Signed-off-by: Vikram Vaswani <2571660+vvaswani@users.noreply.github.com>
Signed-off-by: Jakob Borg <jakob@kastelo.net>
Co-authored-by: Jakob Borg <jakob@kastelo.net>
This commit is contained in:
vvaswani
2026-04-26 07:59:24 +00:00
committed by GitHub
co-authored by Jakob Borg
parent 1f57187461
commit 987e631176
5 changed files with 124 additions and 17 deletions
+49 -16
View File
@@ -7,6 +7,7 @@
package api
import (
"math"
"net/http"
"slices"
"strings"
@@ -35,6 +36,8 @@ type tokenManager struct {
saveTimer *time.Timer
}
const defaultSessionCookieDurationS = 7 * 24 * 60 * 60
func newTokenManager(key string, miscDB *db.Typed, lifetime time.Duration, maxItems int) *tokenManager {
var tokens apiproto.TokenSet
if bs, ok, _ := miscDB.Bytes(key); ok {
@@ -60,18 +63,19 @@ func (m *tokenManager) Check(token string) bool {
defer m.mut.Unlock()
expires, ok := m.tokens.Tokens[token]
if ok {
if expires < m.timeNow().UnixNano() {
// The token is expired.
m.saveLocked() // removes expired tokens
return false
}
// Give the token further life.
m.tokens.Tokens[token] = m.timeNow().Add(m.lifetime).UnixNano()
m.saveLocked()
if !ok {
return false
}
return ok
if expires != 0 && expires < m.timeNow().UnixNano() {
// The token is expired.
m.saveLocked() // removes expired tokens
return false
}
// Give the token further life.
m.tokens.Tokens[token] = m.newExpiryNanos()
m.saveLocked()
return true
}
// New creates a new token and returns it.
@@ -81,12 +85,19 @@ func (m *tokenManager) New() string {
m.mut.Lock()
defer m.mut.Unlock()
m.tokens.Tokens[token] = m.timeNow().Add(m.lifetime).UnixNano()
m.tokens.Tokens[token] = m.newExpiryNanos()
m.saveLocked()
return token
}
func (m *tokenManager) newExpiryNanos() int64 {
if m.lifetime <= 0 {
return 0
}
return m.timeNow().Add(m.lifetime).UnixNano()
}
// Delete removes a token.
func (m *tokenManager) Delete(token string) {
m.mut.Lock()
@@ -100,7 +111,7 @@ func (m *tokenManager) saveLocked() {
// Remove expired tokens.
now := m.timeNow().UnixNano()
for token, expiry := range m.tokens.Tokens {
if expiry < now {
if expiry != 0 && expiry < now {
delete(m.tokens.Tokens, token)
}
}
@@ -152,12 +163,16 @@ type tokenCookieManager struct {
}
func newTokenCookieManager(shortID string, guiCfg config.GUIConfiguration, evLogger events.Logger, miscDB *db.Typed) *tokenCookieManager {
sessionLifetimeS := guiCfg.SessionCookieDurationS
if sessionLifetimeS == 0 {
sessionLifetimeS = defaultSessionCookieDurationS
}
return &tokenCookieManager{
cookieName: "sessionid-" + shortID,
shortID: shortID,
guiCfg: guiCfg,
evLogger: evLogger,
tokens: newTokenManager("sessions", miscDB, maxSessionLifetime, maxActiveSessions),
tokens: newTokenManager("sessions", miscDB, time.Duration(sessionLifetimeS)*time.Second, maxActiveSessions),
}
}
@@ -176,7 +191,11 @@ func (m *tokenCookieManager) createSession(username string, persistent bool, w h
maxAge := 0
if persistent {
maxAge = int(maxSessionLifetime.Seconds())
maxAge = m.sessionCookieMaxAge()
}
path := m.guiCfg.SessionCookiePath
if path == "" {
path = "/"
}
http.SetCookie(w, &http.Cookie{
Name: m.cookieName,
@@ -185,12 +204,26 @@ func (m *tokenCookieManager) createSession(username string, persistent bool, w h
// but in http.Cookie MaxAge = 0 means unspecified (session) and MaxAge < 0 means delete immediately
MaxAge: maxAge,
Secure: useSecureCookie,
Path: "/",
Path: path,
})
emitLoginAttempt(true, username, r, m.evLogger)
}
func (m *tokenCookieManager) sessionCookieMaxAge() int {
switch {
case m.guiCfg.SessionCookieDurationS < 0:
// A negative value means "never expire the cookie". Use a very
// large Max-Age to make the browser keep the cookie for a long
// time.
return math.MaxInt32
case m.guiCfg.SessionCookieDurationS == 0:
return defaultSessionCookieDurationS
default:
return m.guiCfg.SessionCookieDurationS
}
}
func (m *tokenCookieManager) hasValidSession(r *http.Request) bool {
for _, cookie := range r.Cookies() {
// We iterate here since there may, historically, be multiple