Show a login notice when a session ends due to idle timeout.
Return a distinct session_idle error from the API and reload to /login with a clear signed-out message.
This commit is contained in:
@@ -20,7 +20,10 @@ const (
|
||||
sessionRotateAfter = time.Hour
|
||||
)
|
||||
|
||||
var ErrSessionNotFound = errors.New("session not found")
|
||||
var (
|
||||
ErrSessionNotFound = errors.New("session not found")
|
||||
ErrSessionIdle = errors.New("session idle")
|
||||
)
|
||||
|
||||
// SessionManager persists sessions in sessions.enc and sets hardened cookies.
|
||||
type SessionManager struct {
|
||||
@@ -108,6 +111,7 @@ func (manager *SessionManager) LookupValidSession(
|
||||
lifetime := time.Duration(security.SessionLifetimeHours) * time.Hour
|
||||
|
||||
var found *settings.SessionRecord
|
||||
idleMatched := false
|
||||
remaining := make([]settings.SessionRecord, 0, len(store.Sessions))
|
||||
for index := range store.Sessions {
|
||||
session := store.Sessions[index]
|
||||
@@ -116,6 +120,7 @@ func (manager *SessionManager) LookupValidSession(
|
||||
}
|
||||
if session.ID == cookie.Value {
|
||||
if now.Sub(session.LastSeenAt) > idleLimit {
|
||||
idleMatched = true
|
||||
continue
|
||||
}
|
||||
copySession := session
|
||||
@@ -129,6 +134,9 @@ func (manager *SessionManager) LookupValidSession(
|
||||
store.Sessions = remaining
|
||||
_ = settings.SaveSessions(manager.configDir, store, manager.key)
|
||||
clearSessionCookie(writer)
|
||||
if idleMatched {
|
||||
return settings.SessionRecord{}, ErrSessionIdle
|
||||
}
|
||||
return settings.SessionRecord{}, ErrSessionNotFound
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"codeberg.org/SquidSE/ClusterCanvas/service/internal/settings"
|
||||
)
|
||||
|
||||
func TestLookupValidSessionReturnsIdle(t *testing.T) {
|
||||
configDir := t.TempDir()
|
||||
key := testSessionKey()
|
||||
manager := NewSessionManager(configDir, key)
|
||||
security := settings.DefaultSecuritySettings()
|
||||
security.IdleTimeoutMinutes = 5
|
||||
|
||||
createRec := httptest.NewRecorder()
|
||||
record, err := manager.CreateSession(createRec, "user-1", security)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateSession: %v", err)
|
||||
}
|
||||
|
||||
store, err := settings.LoadSessions(configDir, key)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadSessions: %v", err)
|
||||
}
|
||||
if len(store.Sessions) != 1 {
|
||||
t.Fatalf("expected 1 session, got %d", len(store.Sessions))
|
||||
}
|
||||
store.Sessions[0].LastSeenAt = time.Now().UTC().Add(-6 * time.Minute)
|
||||
if err := settings.SaveSessions(configDir, store, key); err != nil {
|
||||
t.Fatalf("SaveSessions: %v", err)
|
||||
}
|
||||
|
||||
request := httptest.NewRequest(http.MethodGet, "/api/v1/status", nil)
|
||||
request.AddCookie(&http.Cookie{Name: SessionCookieName, Value: record.ID})
|
||||
lookupRec := httptest.NewRecorder()
|
||||
|
||||
_, err = manager.LookupValidSession(lookupRec, request, security)
|
||||
if !errors.Is(err, ErrSessionIdle) {
|
||||
t.Fatalf("expected ErrSessionIdle, got %v", err)
|
||||
}
|
||||
|
||||
store, err = settings.LoadSessionsOrEmpty(configDir, key)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadSessionsOrEmpty: %v", err)
|
||||
}
|
||||
if len(store.Sessions) != 0 {
|
||||
t.Fatalf("expected idle session removed, got %#v", store.Sessions)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLookupValidSessionMissingCookie(t *testing.T) {
|
||||
configDir := t.TempDir()
|
||||
manager := NewSessionManager(configDir, testSessionKey())
|
||||
security := settings.DefaultSecuritySettings()
|
||||
|
||||
request := httptest.NewRequest(http.MethodGet, "/api/v1/status", nil)
|
||||
lookupRec := httptest.NewRecorder()
|
||||
|
||||
_, err := manager.LookupValidSession(lookupRec, request, security)
|
||||
if !errors.Is(err, ErrSessionNotFound) {
|
||||
t.Fatalf("expected ErrSessionNotFound, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLookupValidSessionUnknownCookie(t *testing.T) {
|
||||
configDir := t.TempDir()
|
||||
manager := NewSessionManager(configDir, testSessionKey())
|
||||
security := settings.DefaultSecuritySettings()
|
||||
|
||||
request := httptest.NewRequest(http.MethodGet, "/api/v1/status", nil)
|
||||
request.AddCookie(&http.Cookie{Name: SessionCookieName, Value: "unknown-session"})
|
||||
lookupRec := httptest.NewRecorder()
|
||||
|
||||
_, err := manager.LookupValidSession(lookupRec, request, security)
|
||||
if !errors.Is(err, ErrSessionNotFound) {
|
||||
t.Fatalf("expected ErrSessionNotFound, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func testSessionKey() []byte {
|
||||
key := make([]byte, 32)
|
||||
for index := range key {
|
||||
key[index] = byte(index + 3)
|
||||
}
|
||||
return key
|
||||
}
|
||||
Reference in New Issue
Block a user