packages feed

shomei-core-0.2.0.0: src/Shomei/Session/Domain.hs

-- | The session entity: a server-side record of an authenticated login, against which
-- refresh tokens are issued and (optionally) access tokens are checked.
module Shomei.Session.Domain
  ( SessionStatus (..),
    SessionKind (..),
    Session (..),
    NewSession (..),
  )
where

import Data.Set (Set)
import Shomei.Authorization.Claims.Domain (Scope)
import Shomei.Id (SessionId, UserId)
import Shomei.Prelude

data SessionStatus = SessionActive | SessionRevoked | SessionExpired
  deriving stock (Generic, Eq, Show)
  deriving anyclass (FromJSON, ToJSON)

-- | How a session was established. This provenance is selected by the minting path rather than
-- by a caller-supplied option, so privilege-minting operations can distinguish a human login
-- from a machine credential or a delegation.
data SessionKind
  = -- | A human proved a credential, or exchanged a code authorized by such a session.
    InteractiveSession
  | -- | @client_credentials@: a service acting as itself, with no human involved.
    MachineSession
  | -- | Impersonation or RFC 8693 on-behalf-of: the token carries an @act@ claim.
    DelegatedSession
  deriving stock (Generic, Eq, Show)
  deriving anyclass (FromJSON, ToJSON)

data Session = Session
  { sessionId :: !SessionId,
    userId :: !UserId,
    status :: !SessionStatus,
    createdAt :: !UTCTime,
    expiresAt :: !UTCTime,
    revokedAt :: !(Maybe UTCTime),
    -- | for a delegated (impersonation) session, the operator acting on behalf
    -- of 'userId'; 'Nothing' for every ordinary login session.
    actor :: !(Maybe UserId),
    -- | the OAuth2 @client_id@ that minted this session through the authorization-code grant
    --     (EP-5); 'Nothing' for every other flow, including every session that predates the
    --     column. It exists to bind refresh: a token issued through client A must not be
    --     rotatable by client B at @\/oauth\/token@. The bespoke @\/v1\/auth\/refresh@ refuses it.
    oauthClientId :: !(Maybe Text),
    -- | how this session was established; see 'SessionKind'.
    kind :: !SessionKind,
    -- | the scopes the authorization-code grant granted, re-applied on every refresh so a
    --     rotated access token keeps @openid@ and friends. Empty for every other flow and for
    --     every row that predates the column.
    grantedScopes :: !(Set Scope),
    -- | when the last credential was proven; preserved across refresh and emitted as @auth_time@.
    authenticatedAt :: !UTCTime
  }
  deriving stock (Generic, Eq, Show)
  deriving anyclass (FromJSON, ToJSON)

data NewSession = NewSession
  { userId :: !UserId,
    createdAt :: !UTCTime,
    expiresAt :: !UTCTime,
    -- | set to @Just operator@ when minting a delegated session; 'Nothing' otherwise.
    actor :: !(Maybe UserId),
    -- | set to @Just client_id@ by the authorization-code grant; 'Nothing' otherwise.
    oauthClientId :: !(Maybe Text),
    -- | how this session was established; see 'SessionKind'.
    kind :: !SessionKind,
    -- | the authorization-code grant's persisted scope set; empty for every other flow.
    grantedScopes :: !(Set Scope),
    -- | when the credential establishing this session was proven.
    authenticatedAt :: !UTCTime
  }
  deriving stock (Generic, Eq, Show)
  deriving anyclass (FromJSON, ToJSON)