Files
browser/platform/service/auth_sessions.go
T

165 lines
4.7 KiB
Go

package service
import (
"crypto/sha256"
"crypto/subtle"
"encoding/hex"
"errors"
"strings"
"time"
"browser.local/platform/domain"
"browser.local/platform/repo"
"browser.local/platform/validator"
)
const defaultAuthSessionTTL = 8 * time.Hour
func (svc *CoreService) issueAuthSession(user domain.User, message string) (domain.AuthSession, error) {
token, err := randomToken()
if err != nil {
return domain.AuthSession{}, err
}
hash := tokenHash(token)
stamp := svc.now()
generation := 1
existing, err := svc.store.AuthSessions().List(domain.AuthSessionFilter{UserID: user.ID})
if err != nil {
return domain.AuthSession{}, err
}
for _, session := range existing {
if session.Generation >= generation {
generation = session.Generation + 1
}
}
record := domain.AuthSessionRecord{
ID: "auth-session-" + hash[:24],
UserID: user.ID,
TokenHash: hash,
Status: domain.AuthSessionStatusActive,
Generation: generation,
IssuedAt: stamp,
ExpiresAt: stamp.Add(defaultAuthSessionTTL),
LastSeenAt: stamp,
}
if err := validator.ValidateAuthSessionRecord(record); err != nil {
return domain.AuthSession{}, err
}
if err := svc.store.AuthSessions().Create(record); err != nil {
return domain.AuthSession{}, err
}
svc.authMu.Lock()
svc.authSessions[token] = user.ID
svc.authMu.Unlock()
return domain.AuthSession{
SessionID: token,
User: domain.CopyUser(user),
Status: "authenticated",
Message: message,
ExpiresAt: record.ExpiresAt,
}, nil
}
func (svc *CoreService) authenticatedSession(token string) (domain.AuthSessionRecord, error) {
token = strings.TrimSpace(token)
if token == "" {
return domain.AuthSessionRecord{}, ErrUnauthorized
}
hash := tokenHash(token)
sessions, err := svc.store.AuthSessions().List(domain.AuthSessionFilter{TokenHash: hash})
if err != nil {
return domain.AuthSessionRecord{}, err
}
if len(sessions) != 1 || subtle.ConstantTimeCompare([]byte(sessions[0].TokenHash), []byte(hash)) != 1 {
return domain.AuthSessionRecord{}, ErrUnauthorized
}
session := sessions[0]
stamp := svc.now()
if session.Status != domain.AuthSessionStatusActive || !session.RevokedAt.IsZero() || !stamp.Before(session.ExpiresAt) {
if session.Status == domain.AuthSessionStatusActive && !stamp.Before(session.ExpiresAt) {
session.Status = domain.AuthSessionStatusRevoked
session.RevokedAt = stamp
_ = svc.store.AuthSessions().Update(session)
}
return domain.AuthSessionRecord{}, ErrUnauthorized
}
user, err := svc.store.Users().Get(session.UserID)
if err != nil {
if errors.Is(err, repo.ErrNotFound) {
return domain.AuthSessionRecord{}, ErrUnauthorized
}
return domain.AuthSessionRecord{}, err
}
if user.Status != domain.UserStatusActive {
return domain.AuthSessionRecord{}, ErrUnauthorized
}
if session.LastSeenAt.IsZero() || stamp.Sub(session.LastSeenAt) >= time.Minute {
session.LastSeenAt = stamp
if err := svc.store.AuthSessions().Update(session); err != nil {
return domain.AuthSessionRecord{}, err
}
}
return domain.CopyAuthSessionRecord(session), nil
}
func (svc *CoreService) revokeAuthSession(token string) error {
session, err := svc.authenticatedSession(token)
if err != nil {
return err
}
stamp := svc.now()
session.Status = domain.AuthSessionStatusRevoked
session.RevokedAt = stamp
session.LastSeenAt = stamp
if err := validator.ValidateAuthSessionRecord(session); err != nil {
return err
}
if err := svc.store.AuthSessions().Update(session); err != nil {
return err
}
svc.authMu.Lock()
delete(svc.authSessions, token)
svc.authMu.Unlock()
return nil
}
func (svc *CoreService) RotateUserSession(token string) (domain.AuthSession, error) {
session, err := svc.authenticatedSession(token)
if err != nil {
return domain.AuthSession{}, err
}
user, err := svc.store.Users().Get(session.UserID)
if err != nil {
return domain.AuthSession{}, err
}
if err := svc.revokeAuthSession(token); err != nil {
return domain.AuthSession{}, err
}
return svc.issueAuthSession(user, "会话已安全轮换")
}
func (svc *CoreService) revokeUserSessions(userID string) error {
sessions, err := svc.store.AuthSessions().List(domain.AuthSessionFilter{UserID: userID, Status: domain.AuthSessionStatusActive})
if err != nil {
return err
}
stamp := svc.now()
for _, session := range sessions {
session.Status = domain.AuthSessionStatusRevoked
session.RevokedAt = stamp
session.LastSeenAt = stamp
if err := validator.ValidateAuthSessionRecord(session); err != nil {
return err
}
if err := svc.store.AuthSessions().Update(session); err != nil {
return err
}
}
return nil
}
func tokenHash(token string) string {
sum := sha256.Sum256([]byte(token))
return hex.EncodeToString(sum[:])
}