shomei-core-0.2.0.0: src/Shomei/Error.hs
-- | The error vocabulary of the authentication core.
--
-- 'AuthError' is the single error type returned by every workflow. 'TokenError' is the
-- narrower set of JWT-verification failures (interpreted by EP-4's verifier and wrapped
-- in 'TokenInvalid'). 'PasswordPolicyViolation' is the reason a password failed the
-- policy check.
module Shomei.Error
( AuthDependency (..),
AuthError (..),
TokenError (..),
PasswordPolicyViolation (..),
)
where
import Shomei.Authorization.Claims.Domain (Role)
import Shomei.Passkey.Ceremony.Port (WebAuthnError)
import Shomei.Prelude
data PasswordPolicyViolation
= -- | minimum length required
PasswordTooShort Int
| -- | maximum length allowed
PasswordTooLong Int
| -- | the password appears in the bundled common-password dictionary
PasswordTooCommon
| PasswordMissingRequiredClass Text
| -- | the password is essentially the user's own identity (email local-part,
-- full email, or display name)
PasswordResemblesIdentity
| -- | the password appears in a known public breach (HIBP). EP-3.
PasswordBreached
deriving stock (Generic, Eq, Show)
deriving anyclass (FromJSON, ToJSON)
data TokenError
= TokenMalformed
| TokenSignatureInvalid
| -- | The protected JWT header omitted @kid@ or named no published key.
TokenKeyNotFound !(Maybe Text)
| TokenExpired
| TokenIssuerInvalid
| TokenAudienceInvalid
| TokenOtherError Text
deriving stock (Generic, Eq, Show)
deriving anyclass (FromJSON, ToJSON)
-- | A required external dependency whose availability is part of an operation's
-- typed outcome. Extend this closed vocabulary only when another dependency has
-- an intentional operation-level availability contract.
data AuthDependency
= PostgreSQL
deriving stock (Generic, Eq, Show)
deriving anyclass (FromJSON, ToJSON)
data AuthError
= InvalidEmail
| -- | The supplied login identifier was empty or contained internal whitespace.
InvalidLoginId
| WeakPassword PasswordPolicyViolation
| EmailAlreadyRegistered
| -- | A user already exists with the requested login identifier (the principal
-- collision check; the generic counterpart to 'EmailAlreadyRegistered').
LoginIdAlreadyRegistered
| InvalidCredentials
| UserNotActive
| SessionNotFound
| SessionExpired
| SessionRevoked
| RefreshTokenInvalid
| RefreshTokenExpired
| RefreshTokenReuseDetected
| VerificationTokenInvalid
| PasswordResetTokenInvalid
| EmailAlreadyVerified
| -- | Token issuance was refused because runtime configuration requires a verified email
-- and the account's email is present but unverified. Maps to 403.
--
-- Deliberately distinct from 'InvalidCredentials': every path that can raise it has
-- already proven control of the account (a correct password, a valid refresh token, or
-- a verified passkey assertion), so naming the reason leaks no existence information —
-- while a generic 401 would strand a legitimate user with no idea they must click the
-- verification link.
EmailNotVerified
| -- | INTERNAL audit signal raised when a login hits a locked account; the HTTP layer
-- maps it to the SAME generic 401 as 'InvalidCredentials' so a locked account is
-- indistinguishable from a wrong password. (The 'Shomei.Session.Authentication.Workflow.login' workflow itself
-- returns 'InvalidCredentials' for the locked case so even a direct core caller cannot
-- distinguish; 'AccountLocked' exists for completeness and future internal use.)
AccountLocked
| -- | The per-IP failure throttle tripped; the HTTP layer maps it to 429.
TooManyRequests
| TokenInvalid TokenError
| -- | A WebAuthn registration verification failed (bad attestation, origin/challenge
-- mismatch, or malformed credential JSON). The HTTP layer maps this to 400.
WebAuthnCeremonyError WebAuthnError
| -- | No passkey with the given id is owned by the requesting user. Maps to 404.
PasskeyNotFound
| -- | The pending ceremony was missing, already consumed, or expired. Maps to 404.
PendingCeremonyNotFound
| -- | A WebAuthn login/step-up assertion failed verification (bad signature, clone
-- counter, user-not-present, or a credential not owned by the expected user). The
-- HTTP layer maps this to a generic 401 so nothing about the failure leaks.
MfaAssertionInvalid
| -- | EP-7: TOTP enrollment was attempted while @totpConfig.totpEnabled@ is off. Maps to 403.
TotpDisabled
| -- | EP-7: TOTP enrollment was attempted while a /confirmed/ credential already exists;
-- removal (a separate, proof-gated step) must come first. Maps to 409.
TotpAlreadyEnrolled
| -- | EP-7: no unconfirmed, unexpired TOTP enrollment exists to verify (or it has lapsed).
-- Maps to 404.
TotpEnrollmentNotFound
| -- | EP-7: a presented TOTP code did not verify (wrong code, outside the window, or a
-- replayed counter). Maps to a generic 401 so nothing about the failure leaks.
TotpCodeInvalid
| -- | EP-7: a presented recovery code was unknown or already spent. Maps to a generic 401.
RecoveryCodeInvalid
| -- | The caller may not start impersonation: they lack the @impersonate:user@ scope
-- or their own access token is older than the freshness window. Maps to 403.
ImpersonationForbidden
| -- | The impersonation target is missing, not active, or is the caller themselves.
-- Maps to 400.
ImpersonationTargetInvalid
| -- | A credential-changing action was attempted under a delegated (impersonation)
-- token. Maps to 403.
ImpersonationActionBlocked
| -- | EP-4: @client_credentials@ authentication failed at @POST \/oauth\/token@. Raised for an
-- unknown @client_id@, a wrong secret, a revoked account, and an inactive backing user
-- alike — a revoked credential must be indistinguishable from a wrong one, and account
-- existence must not leak to an unauthenticated caller.
--
-- The OAuth handler renders this as RFC 6749 §5.2 @invalid_client@ (HTTP 401), NOT through
-- the problem-details envelope; see "Shomei.Servant.OAuth".
OAuthClientInvalid
| -- | EP-4: the @scope@ parameter was present but empty, or requested scopes outside the
-- account's @allowed_scopes@. Rendered as RFC 6749 @invalid_scope@ (HTTP 400).
OAuthScopeInvalid
| -- | EP-6 (RFC 8693 token exchange): the presented @subject_token@ or @actor_token@ failed
-- verification, named an inactive\/absent user, or was itself a delegated token (chained
-- exchanges are refused). Rendered as RFC 6749 @invalid_grant@ (HTTP 400) at
-- @POST \/oauth\/token@; like the other OAuth errors it never reaches the problem envelope.
OAuthGrantInvalid
| -- | EP-6 (RFC 8693 token exchange): the request was structurally wrong for the exchange grant —
-- an unsupported @requested_token_type@, or a @subject_token_type@\/@actor_token_type@
-- combination that names neither exchange mode. Rendered as RFC 6749 @invalid_request@ (HTTP
-- 400); never reaches the problem envelope.
OAuthRequestMalformed
| -- | The named user does not exist. Raised by the role grant/revoke workflows, which
-- resolve the subject before touching the grant table. Maps to 404.
--
-- Deliberately NOT used by any authentication path: 'InvalidCredentials' stays the single
-- generic answer there, so account existence is never disclosed to an unauthenticated
-- caller. This constructor is only reachable from already-authorized admin surfaces.
UserNotFound
| -- | A grant named a role absent from the @shomei_roles@ registry. Maps to 422: the request
-- was well-formed but names a role the deployment never declared. Guards against
-- @roles grant --role adminn@ silently minting a role no gate will ever check.
RoleNotDefined Role
| -- | The target is not in a state that permits the requested lifecycle transition —
-- suspending an already-suspended user, reinstating one who was never suspended, deleting a
-- deleted one. Maps to 409.
--
-- Deliberately not silently idempotent: two administrators acting on one incident must be
-- able to tell which of them changed the state.
InvalidUserStatus
| -- | An admin asked Shōmei to email the target (a password reset) and the target has no
-- address. Maps to 409.
--
-- A real 409 leaks nothing here, unlike on the public reset endpoint: the caller is an
-- authorized admin who named a user id, not a stranger probing an email.
UserHasNoEmail
| -- | A required dependency could not execute an operation. Public adapters must
-- render this without exposing driver messages, SQL, or connection details.
DependencyUnavailable !AuthDependency
| InternalAuthError Text
deriving stock (Generic, Eq, Show)
deriving anyclass (FromJSON, ToJSON)