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, }) }