packages feed

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

-- | Passkey registration, listing, removal, and passwordless-login HTTP adapters.
module Shomei.Passkey.Handler (passkeyServer) where

import Data.Text (Text)
import Network.Socket (SockAddr)
import Servant (Handler)
import Servant.Server.Generic (AsServerT)
import Shomei.Delegation.Handler (denyUnderDelegation)
import Shomei.Id (CeremonyId, PasskeyId, idText, parseId)
import Shomei.Mfa.Workflow qualified as Mfa
import Shomei.Passkey.Api (PasskeyApi (..))
import Shomei.Passkey.Dto
import Shomei.Passkey.Result
import Shomei.Passkey.Workflow qualified as Passkey
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, pcBadRequest)
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 (..))

passkeyServer :: Env -> PasskeyApi (AsServerT Handler)
passkeyServer env =
  PasskeyApi
    { registerBegin = registerBeginH env,
      registerComplete = registerCompleteH env,
      list = listH env,
      remove = removeH env,
      loginBegin = loginBeginH env,
      loginComplete = loginCompleteH env
    }

registerBeginH :: Env -> AuthUser -> Handler RegisterBeginResult
registerBeginH env user = runApplicationHandler do
  denyUnderDelegation env "passkey_register" user
  (ceremonyId, options) <- workflow env (Passkey.beginPasskeyRegistration env.config user.authUserId)
  pure PasskeyRegisterBeginResponse {ceremonyId = idText ceremonyId, options}

registerCompleteH :: Env -> AuthUser -> PasskeyRegisterCompleteRequest -> Handler RegisterCompleteResult
registerCompleteH env user request = runApplicationHandler do
  denyUnderDelegation env "passkey_register" user
  ceremonyId <- parseCeremonyId request.ceremonyId
  passkey <- workflow env (Passkey.completePasskeyRegistration env.config user.authUserId ceremonyId request.credential request.label)
  pure (passkeyToResponse passkey)

listH :: Env -> AuthUser -> Handler ListPasskeysResult
listH env user = runApplicationHandler (map passkeyToResponse <$> port env (Passkey.listPasskeys user.authUserId))

removeH :: Env -> AuthUser -> PasskeyId -> Handler RemovePasskeyResult
removeH env user passkeyId = runApplicationHandler do
  denyUnderDelegation env "passkey_remove" user
  workflow env (Passkey.removePasskey user.authUserId passkeyId)

loginBeginH :: Env -> Handler PasskeyLoginBeginResult
loginBeginH env = runApplicationHandler do
  (ceremonyId, options) <- workflow env (Mfa.beginPasswordlessLogin env.config)
  pure PasskeyLoginBeginResponse {ceremonyId = idText ceremonyId, options}

loginCompleteH :: Env -> SockAddr -> PasskeyLoginCompleteRequest -> Handler PasskeyLoginCompleteResult
loginCompleteH env peer request = runApplicationHandler do
  ceremonyId <- parseCeremonyId request.ceremonyId
  let pctx = ProofContext {clientIp = ClientIp (clientIpText peer), accountKeyOf = env.accountKeyOf}
  (_, tokens) <- workflow env (Mfa.completePasswordlessLogin env.config pctx ceremonyId request.assertion)
  pure (cookieResponse env.config (tokenCookies env.config tokens) (tokenPairToResponse env.config tokens))

parseCeremonyId :: Text -> ApplicationHandler CeremonyId
parseCeremonyId requestId =
  either (const (rejectProblem pcBadRequest (detailOccurrence "invalid ceremonyId"))) pure (parseId requestId)