key-cape/src/internal/server/oidc/login_session.go
tegwick b6af6c5268
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 34s
Finish KEY-WP-0008: registration handoff and client MFA isolation
Add signed registration/enrollment handoffs, per-request assurance
policy with login-session isolation, and /logout. coulomb-social
stays AAL1 unless acr_values or another client raises the bar.
2026-08-16 01:05:27 +02:00

130 lines
2.7 KiB
Go

package oidc
import (
"net/http"
"sync"
"time"
"keycape/internal/domain"
)
const (
loginCookieName = "kc_login"
loginSessionTTL = 8 * time.Hour
)
// LoginSession is a KeyCape browser session that records the assurance
// already proven for a user. It is not client-specific: a later high-
// assurance client must still step up if the stored level is too low.
type LoginSession struct {
ID string
Username string
Level domain.AssuranceLevel
IssuedAt time.Time
ExpiresAt time.Time
}
// LoginSessionStore is an in-memory login-session map keyed by cookie value.
type LoginSessionStore struct {
mu sync.Mutex
sessions map[string]*LoginSession
}
// NewLoginSessionStore returns an empty login-session store.
func NewLoginSessionStore() *LoginSessionStore {
return &LoginSessionStore{sessions: make(map[string]*LoginSession)}
}
// Create stores a session and returns it.
func (s *LoginSessionStore) Create(username string, level domain.AssuranceLevel) *LoginSession {
if s == nil {
return nil
}
id, err := randomID()
if err != nil {
panic("oidc: failed to generate login session id: " + err.Error())
}
now := time.Now()
sess := &LoginSession{
ID: id,
Username: username,
Level: level,
IssuedAt: now,
ExpiresAt: now.Add(loginSessionTTL),
}
s.mu.Lock()
s.sessions[id] = sess
s.mu.Unlock()
return sess
}
// Get returns a live session by id.
func (s *LoginSessionStore) Get(id string) (*LoginSession, bool) {
if s == nil || id == "" {
return nil, false
}
s.mu.Lock()
sess, ok := s.sessions[id]
s.mu.Unlock()
if !ok {
return nil, false
}
if time.Now().After(sess.ExpiresAt) {
s.Delete(id)
return nil, false
}
return sess, true
}
// Delete removes a session.
func (s *LoginSessionStore) Delete(id string) {
if s == nil {
return
}
s.mu.Lock()
delete(s.sessions, id)
s.mu.Unlock()
}
func (s *LoginSessionStore) fromRequest(r *http.Request) *LoginSession {
if s == nil || r == nil {
return nil
}
c, err := r.Cookie(loginCookieName)
if err != nil || c.Value == "" {
return nil
}
sess, ok := s.Get(c.Value)
if !ok {
return nil
}
return sess
}
func writeLoginCookie(w http.ResponseWriter, sess *LoginSession, secure bool) {
if sess == nil {
return
}
http.SetCookie(w, &http.Cookie{
Name: loginCookieName,
Value: sess.ID,
Path: "/",
Expires: sess.ExpiresAt,
MaxAge: int(time.Until(sess.ExpiresAt).Seconds()),
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
Secure: secure,
})
}
func clearLoginCookie(w http.ResponseWriter, secure bool) {
http.SetCookie(w, &http.Cookie{
Name: loginCookieName,
Value: "",
Path: "/",
MaxAge: -1,
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
Secure: secure,
})
}