131 lines
2.7 KiB
Go
131 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,
|
||
|
|
})
|
||
|
|
}
|