packages feed

hstratus-auth-0.1.0.0: src-internal/Network/HStratus/Internal/LoginFSM.hs

{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE StrictData #-}
{-# LANGUAGE TypeFamilies #-}
{-# OPTIONS_HADDOCK prune #-}

{- |
Module      : Network.HStratus.Internal.LoginFSM
Copyright   : (c) 2025 Tim Emiola
Maintainer  : Tim Emiola <adetokunbo@emio.la>
SPDX-License-Identifier: BSD3

Provides datatypes that represent the Finite State Machine that specifies the
Login process
-}
module Network.HStratus.Internal.LoginFSM where

import Data.Functor ((<&>))
import Data.Kind (Type)
import Data.List.NonEmpty (NonEmpty)
import Data.Text (Text)
import Data.Word (Word8)
import Network.HStratus.Internal.Http (SrpContext (..))
import Network.HStratus.Internal.Session (AccountData, Credentials, SavedHeaders)
import Network.HStratus.Internal.Trust (Setup2SADevice, TrustData, TrustedPhone)


-- | Configuration for the 2FA challenge process.
data TwoFaConfig = TwoFaConfig
  { tfcPickPhone :: TrustData -> IO (Maybe TrustedPhone)
  -- ^ select a phone to receive an SMS code, or 'Nothing' to use a trusted device push
  , tfcReadCode :: Word8 -> IO Text
  -- ^ prompt the user for the verification code; receives the expected digit count
  }


-- | Configuration for the 2SA challenge process.
data TwoSaConfig = TwoSaConfig
  { tscPickDevice :: NonEmpty Setup2SADevice -> IO Setup2SADevice
  -- ^ select the device to receive a verification code
  , tscReadCode :: IO Text
  -- ^ prompt the user for the verification code
  }


{- | @LoginEvent@ represents the valid events of the Login FSM.

Each event is represent by a typeclass function that is constrained
to go between valid states
-}
class LoginEvent m where
  -- | Represents valid finite states at the endpoints of a 'LoginEvent'
  type State m :: Type -> Type


  initial :: m (State m RatifyCredentials)
  ratifyCreds :: State m RatifyCredentials -> m (AfterCredentials (State m))
  ratifyArtifactDir :: State m RatifyArtifactDir -> m (AfterArtifactDir (State m))
  mkArtifactDir :: State m MkArtifactDir -> m (AfterMkArtifactDir (State m))
  loadSession :: State m LoadLastSession -> m (AfterLoadLastSession (State m))
  validateSession :: State m HasSavedSession -> m (AfterValidateSession (State m))
  srpInit :: State m ReadyToAuth -> m (State m SrpInitDone)
  srpComplete :: State m SrpInitDone -> m (AfterSrpComplete (State m))
  acctLogin :: State m DoAccountLogin -> m (AfterAcctLogin (State m))
  listTwoSaDevices :: State m NeedsTwoSa -> m (State m TwoSaReady)
  beginTwoFa :: State m ReadyForTwoFa -> TwoFaConfig -> m (State m TwoFaVerifying)
  verifyTwoFa :: State m TwoFaVerifying -> TwoFaConfig -> m (AfterTwoFaVerify (State m))
  doTrust :: State m DoTrust -> m (State m DoAccountLogin)
  beginTwoSa :: State m ReadyForTwoSa -> TwoSaConfig -> m (State m TwoSaVerifying)
  verifyTwoSa :: State m TwoSaVerifying -> TwoSaConfig -> m (AfterTwoSaVerify (State m))


-- | The outcome of 'loginProcess'.
data LoginOutcome f
  = LoginAuthenticated (f AuthComplete)
  | LoginNeedsTwoFa (f NeedsTwoFa)
  | LoginNeedsTwoSa (f TwoSaReady)
  | LoginHaltCreds (f HaltMissingCredentials)
  | LoginHaltDir (f HaltCannotMkArtifactDir)
  | LoginHaltSrp (f HaltInvalidSrp)
  | LoginHaltTwoFaLocked (f HaltTwoFaLocked)


-- | The outcome of 'twoFaProcess' and 'twoSaProcess'.
data CompletionOutcome f
  = CompletionAuthenticated (f AuthComplete)
  | CompletionNeedsTwoFa (f NeedsTwoFa)
  | CompletionNeedsTwoSa (f TwoSaReady)
  | CompletionTwoFaLocked (f HaltTwoFaLocked)


-- | The canonical login process using events from 'LoginEvent'.
loginProcess
  :: ( LoginEvent m
     , Monad m
     )
  => m (LoginOutcome (State m))
loginProcess =
  initial >>= ratifyCreds >>= \case
    NoCreds e -> pure $ LoginHaltCreds e
    GotCreds x -> onCredsLoaded x


onCredsLoaded
  :: (Monad m, LoginEvent m)
  => State m RatifyArtifactDir
  -> m (LoginOutcome (State m))
onCredsLoaded s =
  ratifyArtifactDir s >>= \case
    DirPresent x -> onArtifactDirPresent x
    DirAbsent a ->
      mkArtifactDir a >>= \case
        NotMade e -> pure $ LoginHaltDir e
        DirMade x -> onArtifactDirPresent x


onArtifactDirPresent
  :: (Monad m, LoginEvent m)
  => State m LoadLastSession
  -> m (LoginOutcome (State m))
onArtifactDirPresent s =
  loadSession s >>= \case
    HasClientId x -> onReadyToAuth x
    HasPriorSession x ->
      validateSession x >>= \case
        SessionStillValid y -> pure $ LoginAuthenticated y
        SessionStale y -> onReadyToAuth y


onReadyToAuth
  :: (Monad m, LoginEvent m)
  => State m ReadyToAuth
  -> m (LoginOutcome (State m))
onReadyToAuth s =
  srpInit s >>= srpComplete >>= \case
    SrpCompleteOk x -> acctLogin x >>= fmap completionToLogin . onAcctLoginDone
    SrpCompleteInvalidKey x -> pure $ LoginHaltSrp x


onAcctLoginDone
  :: (Monad m, LoginEvent m)
  => AfterAcctLogin (State m)
  -> m (CompletionOutcome (State m))
onAcctLoginDone = \case
  AcctLoginOk y -> pure $ CompletionAuthenticated y
  AcctLogin2FA y -> pure $ CompletionNeedsTwoFa y
  AcctLogin2SA y -> listTwoSaDevices y <&> CompletionNeedsTwoSa


completionToLogin :: CompletionOutcome f -> LoginOutcome f
completionToLogin (CompletionAuthenticated x) = LoginAuthenticated x
completionToLogin (CompletionNeedsTwoFa x) = LoginNeedsTwoFa x
completionToLogin (CompletionNeedsTwoSa x) = LoginNeedsTwoSa x
completionToLogin (CompletionTwoFaLocked x) = LoginHaltTwoFaLocked x


-- | The 2FA completion process using events from 'LoginEvent'.
twoFaProcess
  :: (LoginEvent m, Monad m)
  => State m ReadyForTwoFa
  -> TwoFaConfig
  -> m (CompletionOutcome (State m))
twoFaProcess s cfg =
  beginTwoFa s cfg >>= flip verifyTwoFa cfg >>= \case
    TwoFaOk x -> doTrust x >>= acctLogin >>= onAcctLoginDone
    TwoFaRetry x -> twoFaProcess x cfg
    TwoFaLocked x -> pure $ CompletionTwoFaLocked x


-- | The 2SA completion process using events from 'LoginEvent'.
twoSaProcess
  :: (LoginEvent m, Monad m)
  => State m ReadyForTwoSa
  -> TwoSaConfig
  -> m (CompletionOutcome (State m))
twoSaProcess s cfg =
  beginTwoSa s cfg >>= flip verifyTwoSa cfg >>= \case
    TwoSaOk x -> acctLogin x >>= onAcctLoginDone
    TwoSaRetry x -> twoSaProcess x cfg


{- | The states of FSM defining the login process.

Each constructor specifies the concrete data required by the process in that
state, and is tagged with a distinct phantom type.
-}
data LoginFSM s where
  RatifyCredentials :: LoginFSM RatifyCredentials
  HaltMissingCredentials :: LoginFSM HaltMissingCredentials
  RatifyArtifactDir :: Credentials -> LoginFSM RatifyArtifactDir
  MkArtifactDir :: Credentials -> LoginFSM MkArtifactDir
  HaltCannotMkArtifactDir :: Credentials -> LoginFSM HaltCannotMkArtifactDir
  LoadLastSession :: Credentials -> LoginFSM LoadLastSession
  HasSavedSession :: Credentials -> SavedHeaders -> LoginFSM HasSavedSession
  ReadyToAuth :: Credentials -> SavedHeaders -> LoginFSM ReadyToAuth
  SrpInitDone :: Credentials -> SrpContext -> LoginFSM SrpInitDone
  DoAccountLogin :: Credentials -> LoginFSM DoAccountLogin
  AuthComplete :: Credentials -> AccountData -> LoginFSM AuthComplete
  NeedsTwoFa :: Credentials -> LoginFSM NeedsTwoFa
  ReadyForTwoFa :: Credentials -> TrustData -> LoginFSM ReadyForTwoFa
  TwoFaVerifying :: Credentials -> TrustData -> Maybe TrustedPhone -> LoginFSM TwoFaVerifying
  DoTrust :: Credentials -> LoginFSM DoTrust
  NeedsTwoSa :: Credentials -> LoginFSM NeedsTwoSa
  TwoSaReady :: Credentials -> NonEmpty Setup2SADevice -> LoginFSM TwoSaReady
  ReadyForTwoSa :: Credentials -> NonEmpty Setup2SADevice -> LoginFSM ReadyForTwoSa
  TwoSaVerifying :: Credentials -> Setup2SADevice -> NonEmpty Setup2SADevice -> LoginFSM TwoSaVerifying
  HaltInvalidSrp :: Credentials -> LoginFSM HaltInvalidSrp
  HaltTwoFaLocked :: Credentials -> LoginFSM HaltTwoFaLocked


-- | Phantom type linked to a unique state in 'LoginFSM'
data RatifyCredentials


-- | Phantom type linked to a unique state in 'LoginFSM'
data HaltMissingCredentials


-- | Phantom type linked to a unique state in 'LoginFSM'
data RatifyArtifactDir


-- | Phantom type linked to a unique state in 'LoginFSM'
data MkArtifactDir


-- | Phantom type linked to a unique state in 'LoginFSM'
data HaltCannotMkArtifactDir


-- | Phantom type linked to a unique state in 'LoginFSM'
data LoadLastSession


-- | Phantom type linked to a unique state in 'LoginFSM'
data HasSavedSession


-- | Phantom type linked to a unique state in 'LoginFSM'
data ReadyToAuth


-- | Phantom type linked to a unique state in 'LoginFSM'
data SrpInitDone


-- | Phantom type linked to a unique state in 'LoginFSM'
data DoAccountLogin


-- | Phantom type linked to a unique state in 'LoginFSM'
data AuthComplete


-- | Phantom type linked to a unique state in 'LoginFSM'
data NeedsTwoFa


-- | Phantom type linked to a unique state in 'LoginFSM'
data ReadyForTwoFa


-- | Phantom type linked to a unique state in 'LoginFSM'
data TwoFaVerifying


-- | Phantom type linked to a unique state in 'LoginFSM'
data DoTrust


-- | Phantom type linked to a unique state in 'LoginFSM'
data NeedsTwoSa


-- | Phantom type linked to a unique state in 'LoginFSM'
data TwoSaReady


-- | Phantom type linked to a unique state in 'LoginFSM'
data ReadyForTwoSa


-- | Phantom type linked to a unique state in 'LoginFSM'
data TwoSaVerifying


-- | Phantom type linked to a unique state in 'LoginFSM'
data HaltInvalidSrp


-- | Phantom type linked to a unique state in 'LoginFSM'
data HaltTwoFaLocked


-- | The valid states after 'loadSession'
data AfterLoadLastSession f
  = HasClientId (f ReadyToAuth)
  | HasPriorSession (f HasSavedSession)


-- | The valid states after 'validateSession'
data AfterValidateSession f
  = SessionStillValid (f AuthComplete)
  | SessionStale (f ReadyToAuth)


-- | The valid states after 'mkArtifactDir'
data AfterMkArtifactDir f
  = NotMade (f HaltCannotMkArtifactDir)
  | DirMade (f LoadLastSession)


-- | The valid states after 'ratifyArtifactDir'
data AfterArtifactDir f
  = DirPresent (f LoadLastSession)
  | DirAbsent (f MkArtifactDir)


-- | The valid states after 'ratifyCreds'
data AfterCredentials f
  = NoCreds (f HaltMissingCredentials)
  | GotCreds (f RatifyArtifactDir)


-- | The valid states after 'srpComplete'
data AfterSrpComplete f
  = SrpCompleteOk (f DoAccountLogin)
  | SrpCompleteInvalidKey (f HaltInvalidSrp)


-- | The valid states after 'acctLogin'
data AfterAcctLogin f
  = AcctLoginOk (f AuthComplete)
  | AcctLogin2FA (f NeedsTwoFa)
  | AcctLogin2SA (f NeedsTwoSa)


-- | The valid states after 'verifyTwoFa'
data AfterTwoFaVerify f
  = TwoFaOk (f DoTrust)
  | TwoFaRetry (f ReadyForTwoFa)
  | TwoFaLocked (f HaltTwoFaLocked)


-- | The valid states after 'verifyTwoSa'
data AfterTwoSaVerify f
  = TwoSaOk (f DoAccountLogin)
  | TwoSaRetry (f ReadyForTwoSa)