packages feed

shomei-servant-0.2.0.0: src/Shomei/Mfa/Handler.hs

-- | MFA, TOTP, and recovery-code HTTP adapters.
module Shomei.Mfa.Handler (mfaServer) where

import Data.Time (addUTCTime)
import Network.Socket (SockAddr)
import Servant (Handler)
import Servant.Server.Generic (AsServerT)
import Shomei.Account.Handler (loadUser)
import Shomei.Authorization.Claims.Domain (AuthClaims (..))
import Shomei.Config (ImpersonationConfig (..), ShomeiConfig (..))
import Shomei.Delegation.Handler (denyUnderDelegation)
import Shomei.Id (parseId)
import Shomei.Mfa.Api (MfaApi (..))
import Shomei.Mfa.Dto
import Shomei.Mfa.RecoveryCode.Store (countUnusedRecoveryCodes)
import Shomei.Mfa.Result
import Shomei.Mfa.Totp.Workflow qualified as Totp
import Shomei.Mfa.Workflow qualified as Mfa
import Shomei.Prelude
import Shomei.Servant.Application (ApplicationHandler, port, rejectProblem, runApplicationHandler, workflow)
import Shomei.Servant.Auth (AuthUser (..))
import Shomei.Servant.ClientIp (clientIpText)
import Shomei.Servant.Cookie (tokenCookies)
import Shomei.Servant.Error (detailOccurrence, noProblemOccurrence, pcBadRequest, pcReauthenticationRequired)
import Shomei.Servant.Result (cookieResponse)
import Shomei.Servant.Seam (Env (..))
import Shomei.Session.Command (ProofContext (..))
import Shomei.Session.Dto (tokenPairToResponse)
import Shomei.Session.LoginAttempt.Domain (ClientIp (..))
import Shomei.Time.Store (now)

mfaServer :: Env -> MfaApi (AsServerT Handler)
mfaServer env =
  MfaApi
    { complete = completeH env,
      totpEnroll = totpEnrollH env,
      totpVerify = totpVerifyH env,
      totpDelete = totpDeleteH env,
      recoveryCodesGenerate = recoveryCodesGenerateH env,
      recoveryCodesCount = recoveryCodesCountH env
    }

completeH :: Env -> SockAddr -> MfaCompleteRequest -> Handler MfaCompleteResult
completeH env peer request = runApplicationHandler do
  ceremonyId <-
    either
      (const (rejectProblem pcBadRequest (detailOccurrence "invalid ceremonyId")))
      pure
      (parseId request.ceremonyId)
  (_, tokens) <- workflow env (Mfa.completeMfa env.config (proofContext env peer) ceremonyId (mfaCompletionOf request))
  pure (cookieResponse env.config (tokenCookies env.config tokens) (tokenPairToResponse env.config tokens))

totpEnrollH :: Env -> AuthUser -> Handler TotpEnrollResult
totpEnrollH env authUser = runApplicationHandler do
  denyUnderDelegation env "totp_enroll" authUser
  user <- loadUser env authUser
  Totp.TotpEnrollment {secretBase32, otpauthUri} <- workflow env (Totp.enrollTotp env.config user)
  pure TotpEnrollResponse {secret = secretBase32, otpauthUri}

totpVerifyH :: Env -> AuthUser -> TotpVerifyRequest -> Handler TotpVerifyResult
totpVerifyH env authUser request = runApplicationHandler do
  user <- loadUser env authUser
  workflow env (Totp.verifyTotpEnrollment env.config user request.code)

totpDeleteH :: Env -> AuthUser -> SockAddr -> TotpRemoveRequest -> Handler TotpDeleteResult
totpDeleteH env authUser peer request = runApplicationHandler do
  denyUnderDelegation env "totp_remove" authUser
  requireFreshAuth env authUser
  user <- loadUser env authUser
  workflow env (Totp.removeTotp env.config (proofContext env peer) user (totpRemovalProofOf request))

recoveryCodesGenerateH :: Env -> AuthUser -> Handler RecoveryCodesGenerateResult
recoveryCodesGenerateH env authUser = runApplicationHandler do
  denyUnderDelegation env "recovery_codes_generate" authUser
  requireFreshAuth env authUser
  user <- loadUser env authUser
  codes <- workflow env (Totp.regenerateRecoveryCodes env.config user)
  pure RecoveryCodesResponse {codes}

recoveryCodesCountH :: Env -> AuthUser -> Handler RecoveryCodesCountResult
recoveryCodesCountH env authUser = runApplicationHandler do
  remaining <- port env (countUnusedRecoveryCodes authUser.authUserId)
  pure RecoveryCodesCountResponse {remaining}

requireFreshAuth :: Env -> AuthUser -> ApplicationHandler ()
requireFreshAuth env user = do
  timestamp <- port env now
  let window = env.config.impersonationConfig.actorFreshnessWindow
  when (timestamp > addUTCTime window user.authClaims.authTime) $
    rejectProblem pcReauthenticationRequired noProblemOccurrence

proofContext :: Env -> SockAddr -> ProofContext
proofContext env peer =
  ProofContext
    { clientIp = ClientIp (clientIpText peer),
      accountKeyOf = env.accountKeyOf
    }