165 lines
4.7 KiB
Go
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[:])
|
|
}
|