security: 15-day session lifetime with 7-day auto-renewal

- Reduce SESSION_MAX_DURATION from 365 days to 15 days
- Add SESSION_RENEWAL_THRESHOLD (7 days): sessions are only extended
  when fewer than 7 days remain, instead of refreshing on every request
- Align cookie MaxAge with SESSION_MAX_DURATION (derived from the constant)
- Enforce expiry in load(): expired sessions are deleted on first use
  and the caller receives an error, preventing Bearer-token replay of
  stale sessions that the securecookie age check would not catch

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
nemunaire 2026-03-07 23:46:07 +07:00
commit 41fac845eb
2 changed files with 15 additions and 5 deletions

View file

@ -50,7 +50,7 @@ func NewSessionStore(opts *happydns.Options, storage sessionUC.SessionStorage, k
Codecs: securecookie.CodecsFromPairs(keyPairs...), Codecs: securecookie.CodecsFromPairs(keyPairs...),
options: &sessions.Options{ options: &sessions.Options{
Path: opts.BasePath + "/", Path: opts.BasePath + "/",
MaxAge: 86400 * 30, MaxAge: int(sessionUC.SESSION_MAX_DURATION.Seconds()),
Secure: opts.DevProxy == "" && opts.ExternalURL.Scheme != "http", Secure: opts.DevProxy == "" && opts.ExternalURL.Scheme != "http",
HttpOnly: true, HttpOnly: true,
SameSite: http.SameSiteLaxMode, SameSite: http.SameSiteLaxMode,
@ -171,6 +171,11 @@ func (s *SessionStore) load(session *sessions.Session) error {
session.Values["created_on"] = mysession.IssuedAt session.Values["created_on"] = mysession.IssuedAt
} }
if !mysession.ExpiresOn.IsZero() { if !mysession.ExpiresOn.IsZero() {
if mysession.ExpiresOn.Before(time.Now()) {
// Session has expired; delete it and treat this as a new session.
_ = s.storage.DeleteSession(session.ID)
return fmt.Errorf("session has expired")
}
session.Values["expires_on"] = mysession.ExpiresOn session.Values["expires_on"] = mysession.ExpiresOn
} }
@ -210,11 +215,12 @@ func (s *SessionStore) save(session *sessions.Session, ua string) error {
} }
if exOn == nil { if exOn == nil {
expiresOn = time.Now().Add(time.Second * time.Duration(session.Options.MaxAge)) expiresOn = time.Now().Add(sessionUC.SESSION_MAX_DURATION)
} else { } else {
expiresOn = exOn.(time.Time) expiresOn = exOn.(time.Time)
if expiresOn.Sub(time.Now().Add(time.Second*time.Duration(session.Options.MaxAge))) < 0 { // Auto-renew if the session expires within the renewal window.
expiresOn = time.Now().Add(time.Second * time.Duration(session.Options.MaxAge)) if time.Until(expiresOn) < sessionUC.SESSION_RENEWAL_THRESHOLD {
expiresOn = time.Now().Add(sessionUC.SESSION_MAX_DURATION)
} }
} }

View file

@ -33,7 +33,11 @@ import (
"git.happydns.org/happyDomain/model" "git.happydns.org/happyDomain/model"
) )
const SESSION_MAX_DURATION = 24 * 365 * time.Hour const SESSION_MAX_DURATION = 15 * 24 * time.Hour
// SESSION_RENEWAL_THRESHOLD is the remaining lifetime below which a session
// is automatically renewed to SESSION_MAX_DURATION on the next request.
const SESSION_RENEWAL_THRESHOLD = 7 * 24 * time.Hour
// Service handles all session-related operations. // Service handles all session-related operations.
// This consolidates what were previously separate usecase structs into a single service. // This consolidates what were previously separate usecase structs into a single service.