diff --git a/internal/auth/session.go b/internal/auth/session.go index 3a93a70..89d061e 100644 --- a/internal/auth/session.go +++ b/internal/auth/session.go @@ -93,10 +93,21 @@ func (m *SessionManager) StartSession(w http.ResponseWriter, r *http.Request, cl // The cookie outlives idle gaps so a returning device presents its ID; // the server decides whether the session is still live. Max-Age is whole // seconds, rounded up so the cookie never ends before the absolute limit. - http.SetCookie(w, sessionCookie(id, int((m.max+time.Second-1)/time.Second))) + http.SetCookie(w, sessionCookie(id, cookieMaxAge(m.max))) return nil } +// cookieMaxAge is lifetime in whole seconds, rounded up. It rounds the +// quotient rather than adding to lifetime, which would overflow near the +// largest time.Duration. +func cookieMaxAge(lifetime time.Duration) int { + seconds := lifetime / time.Second + if lifetime%time.Second != 0 { + seconds++ + } + return int(seconds) +} + // SessionFor returns the live session r carries and slides its last use to // now. It returns ErrNoSession when there is none; an expired session is // deleted. diff --git a/internal/auth/session_test.go b/internal/auth/session_test.go index 859cf3b..20e03fe 100644 --- a/internal/auth/session_test.go +++ b/internal/auth/session_test.go @@ -333,6 +333,22 @@ func TestSessionCookieOutlivesFractionalMax(t *testing.T) { } } +// A lifetime near the largest accepted duration still yields a positive, +// rounded-up cookie Max-Age rather than overflowing. +func TestSessionCookieMaxAgeNearLargestMax(t *testing.T) { + t.Parallel() + largest, err := time.ParseDuration("2562047h47m16.854s") + if err != nil { + t.Fatal(err) + } + h := newSessionHarness(t) + h.sessions.max = largest + _, cookie := h.signIn(t) + if want := 9223372037; cookie.MaxAge != want { + t.Errorf("session cookie MaxAge = %d, want %d", cookie.MaxAge, want) + } +} + // A lifetime shortened by configuration applies to existing sessions even // though their stored expires_at is later. func TestSessionLifetimeChecksUseCurrentConfig(t *testing.T) {