163 lines
4.6 KiB
Go
163 lines
4.6 KiB
Go
// Package devicesession defines the source-of-truth domain model for one
|
|
// authenticated device session.
|
|
package devicesession
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"galaxy/authsession/internal/domain/common"
|
|
)
|
|
|
|
// Status identifies the coarse lifecycle state of one device session.
|
|
type Status string
|
|
|
|
const (
|
|
// StatusActive reports that the session may be used for authenticated
|
|
// request verification.
|
|
StatusActive Status = "active"
|
|
|
|
// StatusRevoked reports that the session has been revoked and must no
|
|
// longer authenticate requests.
|
|
StatusRevoked Status = "revoked"
|
|
)
|
|
|
|
// RevokeReasonDeviceLogout reports that one device logged itself out.
|
|
const RevokeReasonDeviceLogout common.RevokeReasonCode = "device_logout"
|
|
|
|
// RevokeReasonLogoutAll reports that the session was revoked by a
|
|
// user-scoped logout-all action.
|
|
const RevokeReasonLogoutAll common.RevokeReasonCode = "logout_all"
|
|
|
|
// RevokeReasonAdminRevoke reports that the session was revoked
|
|
// administratively.
|
|
const RevokeReasonAdminRevoke common.RevokeReasonCode = "admin_revoke"
|
|
|
|
// RevokeReasonUserBlocked reports that the session was revoked because future
|
|
// auth flow for the user or e-mail was blocked.
|
|
const RevokeReasonUserBlocked common.RevokeReasonCode = "user_blocked"
|
|
|
|
// IsKnown reports whether Status is one of the device-session states
|
|
// supported by the current domain model.
|
|
func (s Status) IsKnown() bool {
|
|
switch s {
|
|
case StatusActive, StatusRevoked:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// CanTransitionTo reports whether the current device-session Status may move
|
|
// to next under the Stage-2 lifecycle rules.
|
|
func (s Status) CanTransitionTo(next Status) bool {
|
|
return s == StatusActive && next == StatusRevoked
|
|
}
|
|
|
|
// IsKnownRevokeReasonCode reports whether code is one of the built-in revoke
|
|
// reasons fixed by the Stage-2 domain model.
|
|
func IsKnownRevokeReasonCode(code common.RevokeReasonCode) bool {
|
|
switch code {
|
|
case RevokeReasonDeviceLogout,
|
|
RevokeReasonLogoutAll,
|
|
RevokeReasonAdminRevoke,
|
|
RevokeReasonUserBlocked:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// Revocation stores the audit metadata recorded when a session is revoked.
|
|
type Revocation struct {
|
|
// At reports when the revoke took effect.
|
|
At time.Time
|
|
|
|
// ReasonCode stores one machine-readable revoke reason code.
|
|
ReasonCode common.RevokeReasonCode
|
|
|
|
// ActorType stores one machine-readable initiator type.
|
|
ActorType common.RevokeActorType
|
|
|
|
// ActorID optionally stores a stable initiator identifier.
|
|
ActorID string
|
|
}
|
|
|
|
// Validate reports whether Revocation contains all metadata required for a
|
|
// revoked session.
|
|
func (r Revocation) Validate() error {
|
|
if r.At.IsZero() {
|
|
return errors.New("session revocation time must not be zero")
|
|
}
|
|
if err := r.ReasonCode.Validate(); err != nil {
|
|
return fmt.Errorf("session revocation reason code: %w", err)
|
|
}
|
|
if err := r.ActorType.Validate(); err != nil {
|
|
return fmt.Errorf("session revocation actor type: %w", err)
|
|
}
|
|
if strings.TrimSpace(r.ActorID) != r.ActorID {
|
|
return errors.New("session revocation actor id must not contain surrounding whitespace")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Session is the minimal source-of-truth aggregate shape fixed by Stage 2.
|
|
type Session struct {
|
|
// ID identifies the device session.
|
|
ID common.DeviceSessionID
|
|
|
|
// UserID identifies the durable user linkage for the session.
|
|
UserID common.UserID
|
|
|
|
// ClientPublicKey stores the validated device public key in parsed form.
|
|
ClientPublicKey common.ClientPublicKey
|
|
|
|
// Status reports the coarse lifecycle state of the session.
|
|
Status Status
|
|
|
|
// CreatedAt reports when the session was created.
|
|
CreatedAt time.Time
|
|
|
|
// Revocation is present only when Status is StatusRevoked.
|
|
Revocation *Revocation
|
|
}
|
|
|
|
// Validate reports whether Session satisfies the Stage-2 structural and
|
|
// lifecycle invariants.
|
|
func (s Session) Validate() error {
|
|
if err := s.ID.Validate(); err != nil {
|
|
return fmt.Errorf("session id: %w", err)
|
|
}
|
|
if err := s.UserID.Validate(); err != nil {
|
|
return fmt.Errorf("session user id: %w", err)
|
|
}
|
|
if err := s.ClientPublicKey.Validate(); err != nil {
|
|
return fmt.Errorf("session client public key: %w", err)
|
|
}
|
|
if !s.Status.IsKnown() {
|
|
return fmt.Errorf("session status %q is unsupported", s.Status)
|
|
}
|
|
if s.CreatedAt.IsZero() {
|
|
return errors.New("session creation time must not be zero")
|
|
}
|
|
|
|
switch s.Status {
|
|
case StatusActive:
|
|
if s.Revocation != nil {
|
|
return errors.New("active session must not contain revocation metadata")
|
|
}
|
|
case StatusRevoked:
|
|
if s.Revocation == nil {
|
|
return errors.New("revoked session must contain revocation metadata")
|
|
}
|
|
if err := s.Revocation.Validate(); err != nil {
|
|
return fmt.Errorf("session revocation: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|