Add server-side sessions, session cookie and logout
CI / ci (push) Successful in 1m25s
CI / ci (pull_request) Successful in 1m24s
Independent review Codex gpt-6-sol high: CHANGES_REQUIRED (1 medium)

A successful OIDC callback now creates a sessions row through the new
auth.SessionManager: a random 256-bit ID whose SHA-256 hash is the only
stored form, the subject, email, name and groups claims, a separate random
CSRF token and created/last_seen/expires timestamps. The ID travels only in
the __Host-mealquest_session cookie (HttpOnly, Secure, SameSite=Lax, Path=/,
no Domain).

SessionFor rejects a session unused for MEALQUEST_SESSION_IDLE or older
than MEALQUEST_SESSION_MAX, deletes it, and otherwise slides last_seen_at.
POST /auth/logout deletes the row, clears the cookie and redirects to the
provider's end_session_endpoint, which discovery now requires.

The session lifetimes are optional configuration (defaults 720h and 4320h,
per D-004); set values must be positive with MAX at least IDLE.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 10:02:17 -04:00
co-authored by Claude Opus 5.5
parent 99c8f1625f
commit 1dfba7b9db
8 changed files with 851 additions and 27 deletions
+15 -2
View File
@@ -86,6 +86,7 @@ func (p *fakeProvider) discovery(w http.ResponseWriter, _ *http.Request) {
"authorization_endpoint": p.issuer() + "/authorize",
"token_endpoint": p.issuer() + "/token",
"jwks_uri": p.issuer() + "/jwks",
"end_session_endpoint": p.issuer() + "/end-session/",
"response_types_supported": []string{"code"},
"subject_types_supported": []string{"public"},
"id_token_signing_alg_values_supported": []string{"RS256", "HS256"},
@@ -223,11 +224,12 @@ func writeJSON(w http.ResponseWriter, v any) {
json.NewEncoder(w).Encode(v)
}
// memorySessions is an in-memory SessionStarter that records the claims it
// was handed.
// memorySessions is an in-memory Sessions that records the claims it was
// handed and how many sessions were ended.
type memorySessions struct {
mu sync.Mutex
claims []Claims
ended int
err error
}
@@ -242,6 +244,16 @@ func (m *memorySessions) StartSession(w http.ResponseWriter, _ *http.Request, cl
return nil
}
func (m *memorySessions) EndSession(http.ResponseWriter, *http.Request) error {
m.mu.Lock()
defer m.mu.Unlock()
if m.err != nil {
return m.err
}
m.ended++
return nil
}
func (m *memorySessions) started() []Claims {
m.mu.Lock()
defer m.mu.Unlock()
@@ -780,6 +792,7 @@ func TestNewRejectsRemoteHTTPEndpoint(t *testing.T) {
{"authorization_endpoint", "authorization endpoint"},
{"token_endpoint", "token endpoint"},
{"jwks_uri", "JWKS endpoint"},
{"end_session_endpoint", "end session endpoint"},
}
for _, tc := range tests {
t.Run(tc.field, func(t *testing.T) {