packages feed

shomei-core-0.2.0.0: src/Shomei/Passkey/Ceremony/Port.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE TypeFamilies #-}

-- | The WebAuthn ceremony port (MasterPlan 3, IP-1).
--
-- A passkey is a public-key credential a browser creates with
-- @navigator.credentials.create()@ and proves possession of with
-- @navigator.credentials.get()@. Each exchange is a /ceremony/ with a /begin/ step
-- (the server emits options carrying a random challenge) and a /complete/ step (the
-- server verifies the browser's signed response).
--
-- This port lets 'Shomei.Session.Authentication.Workflow' code orchestrate those ceremonies without
-- @shomei-core@ ever importing the heavy @webauthn@ library: its operations cross the
-- package boundary using only aeson 'Value' (the browser-facing @webauthn-json@
-- payloads, already a core dependency) plus the 'Shomei.Passkey.Domain' domain types.
-- The real interpreter (@runWebAuthnCeremonyLibrary@ in @shomei-webauthn@) does the
-- encode/decode/verify against the library; a deterministic fake
-- ('Shomei.Test.InMemory.runWebAuthnCeremonyFake') drives tests without cryptography.
module Shomei.Passkey.Ceremony.Port
  ( WebAuthnCeremony (..),
    WebAuthnError (..),
    CredentialUserInfo (..),
    BeginCeremony (..),
    StoredCredentialForVerify (..),
    VerifiedRegistration (..),
    VerifiedAuthentication (..),
    beginRegistrationCeremony,
    completeRegistrationCeremony,
    beginAuthenticationCeremony,
    completeAuthenticationCeremony,
  )
where

import Data.Aeson (Value)
import Data.ByteString (ByteString)
import Effectful (Dispatch (Dynamic), DispatchOf, Eff, Effect, (:>))
import Effectful.Dispatch.Dynamic (send)
import Shomei.Passkey.Domain
  ( PublicKeyBytes,
    SignatureCounter,
    UserHandle,
    UserVerificationPolicy,
    WebAuthnCredentialId,
  )
import Shomei.Prelude

-- | The verification failure modes, mapped from the library's
-- RegistrationError/AuthenticationError families to a small stable closed set.
data WebAuthnError
  = WebAuthnDecodeError Text
  | WebAuthnChallengeMismatch
  | WebAuthnOriginMismatch
  | WebAuthnRpIdMismatch
  | WebAuthnUserNotPresent
  | WebAuthnUserNotVerified
  | WebAuthnSignatureInvalid
  | WebAuthnCounterCloned
  | WebAuthnOtherError Text
  deriving stock (Generic, Eq, Show)
  deriving anyclass (FromJSON, ToJSON)

-- | The user identity baked into a registration's options.
data CredentialUserInfo = CredentialUserInfo
  { userHandle :: !UserHandle,
    accountName :: !Text,
    displayName :: !Text
  }
  deriving stock (Generic, Eq, Show)

-- | The two outputs of a begin step: the JSON for the browser and the opaque
-- blob for PendingCeremonyStore (EP-2).
data BeginCeremony = BeginCeremony
  { optionsJson :: !Value,
    optionsBlob :: !ByteString
  }
  deriving stock (Generic, Eq, Show)

-- | The stored fields the authentication verify step needs (EP-4 reads them
-- from PasskeyStore and hands them here).
data StoredCredentialForVerify = StoredCredentialForVerify
  { credentialId :: !WebAuthnCredentialId,
    userHandle :: !UserHandle,
    publicKey :: !PublicKeyBytes,
    signCounter :: !SignatureCounter,
    transports :: ![Text]
  }
  deriving stock (Generic, Eq, Show)

data VerifiedRegistration = VerifiedRegistration
  { credentialId :: !WebAuthnCredentialId,
    userHandle :: !UserHandle,
    publicKey :: !PublicKeyBytes,
    signCounter :: !SignatureCounter,
    transports :: ![Text]
  }
  deriving stock (Generic, Eq, Show)

data VerifiedAuthentication = VerifiedAuthentication
  { credentialId :: !WebAuthnCredentialId,
    newSignCounter :: !SignatureCounter,
    cloneWarning :: !Bool
  }
  deriving stock (Generic, Eq, Show)

data WebAuthnCeremony :: Effect where
  -- 2nd arg = excludeCredentials (ids already enrolled for this user).
  BeginRegistrationCeremony ::
    CredentialUserInfo -> [WebAuthnCredentialId] -> WebAuthnCeremony m BeginCeremony
  -- optionsBlob, then the browser's credential JSON.
  CompleteRegistrationCeremony ::
    ByteString -> Value -> WebAuthnCeremony m (Either WebAuthnError VerifiedRegistration)
  -- User-verification policy, then allowCredentials ([] = passwordless discovery).
  BeginAuthenticationCeremony ::
    UserVerificationPolicy -> [WebAuthnCredentialId] -> WebAuthnCeremony m BeginCeremony
  CompleteAuthenticationCeremony ::
    ByteString ->
    StoredCredentialForVerify ->
    Value ->
    WebAuthnCeremony m (Either WebAuthnError VerifiedAuthentication)

type instance DispatchOf WebAuthnCeremony = Dynamic

beginRegistrationCeremony ::
  (WebAuthnCeremony :> es) => CredentialUserInfo -> [WebAuthnCredentialId] -> Eff es BeginCeremony
beginRegistrationCeremony u xs = send (BeginRegistrationCeremony u xs)

completeRegistrationCeremony ::
  (WebAuthnCeremony :> es) => ByteString -> Value -> Eff es (Either WebAuthnError VerifiedRegistration)
completeRegistrationCeremony b v = send (CompleteRegistrationCeremony b v)

beginAuthenticationCeremony ::
  (WebAuthnCeremony :> es) => UserVerificationPolicy -> [WebAuthnCredentialId] -> Eff es BeginCeremony
beginAuthenticationCeremony uv ids = send (BeginAuthenticationCeremony uv ids)

completeAuthenticationCeremony ::
  (WebAuthnCeremony :> es) =>
  ByteString -> StoredCredentialForVerify -> Value -> Eff es (Either WebAuthnError VerifiedAuthentication)
completeAuthenticationCeremony b c v = send (CompleteAuthenticationCeremony b c v)