NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages

Every failure extends RoundlyConsulting\Passkeys\Exceptions\PasskeyException (a RuntimeException). Catch the base for a uniform response, or a child for finer control:

use RoundlyConsulting\Passkeys\Exceptions\PasskeyException;

try {
    $passkey = Passkeys::authenticate($response);
} catch (PasskeyException $e) {
    // Return a uniform failure to the client; log $e server-side.
    return response()->json(['message' => 'Sign-in failed.'], 422);
}

Ceremony failures

ExceptionThrown when
InvalidClientDataclientData.type is wrong, the response payload is malformed, the attestation fmt is not a well-formed format identifier, or a user handle is not base64url of random bytes.
ChallengeExpiredNo challenge exists for the ceremony ID — expired or already used.
ChallengeMismatchThe challenge differs, was minted for another user, or for the other ceremony type.
OriginMismatchThe origin is not allow-listed, or a cross-origin ceremony is not allowed.
RpIdMismatchThe RP ID hash in the authenticator data doesn’t match.
InvalidAuthenticatorDataBad authenticator data, no user-presence flag, inconsistent backup flags, a backup eligibility that differs from the one registered, missing attested credential data, or a registration whose credential ID is longer than 1023 bytes.
UserVerificationRequiredUser verification was required but the authenticator didn’t verify the user.
UnsupportedAlgorithmThe credential’s algorithm isn’t in the offered list.
SignatureInvalidThe assertion signature failed verification.
SignCountRegressionThe counter didn’t advance and sign_count_policy is reject.
CredentialAlreadyRegisteredThe credential ID is already stored, revoked ones included — also for the losing request when two registrations race with the same ID.
CredentialNotFoundThe uniform miss: an unknown credential, one whose owner was deleted or soft-deleted, no longer holds its user handle or no longer resolves to a model, one a user-bound ceremony didn’t offer, a usernameless assertion without a userHandle, a failed expectation, a credential revoked mid-assertion, or a rename/revoke of a passkey that isn’t this account’s.
InvalidExpectationAuthenticationExpectation::owner() got an unsaved model — a programming error, never a ceremony outcome.

Configuration, decoding and attestation

ExceptionThrown when
InvalidConfigurationAt boot: a config value of the wrong shape, naming the key (table only when first read), an unvetted algorithm, trust stricter than ignore with attestation none, or clock skew outside 0–3600. At the first ceremony: missing rp.id or origins. At first use: an unreadable trust anchor.
MalformedCborThe CBOR data is malformed.
InvalidCoseKeyThe COSE public key is invalid.
InvalidAttestationThe attestation statement’s maths failed.
AttestationUntrustedThe statement is sound but policy refused it — unanchored root, no anchors configured, self-attestation under basic, an invalid certification path, an expired certificate or a disallowed AAGUID.
AttestationRequiredThe trust tier demands a statement and the authenticator sent none.
UnsupportedAttestationFormatThe format has no verifier (none, packed and apple are supported).
PasskeyAssertionFailedA Passkeys::fake() assertion failed.

Localised messages

Messages resolve through the passkeys::errors.* translation namespace, which ships in English and Slovak and follows the app locale — publish passkeys-translations to adjust them or add another language. A message is worded entirely in the app locale: the package’s own detail fragments — the shape a setting must have, why an attestation statement is malformed, which certificate requirement failed — are translation keys too, while literal tokens such as x5c, CA:FALSE, OIDs and setting keys stay as they are. Integer and enum settings are validated by package-toolkit-for-laravel, whose messages are English only.

Text from a lower layer — the CBOR/COSE decoder or the certificate parser — is not translatable, so it never enters the message. It stays available to developers:

  • $e->getPrevious() — the lower-level exception (for example a CryptoException) behind MalformedCbor, InvalidCoseKey, InvalidAuthenticatorData::because(), UnsupportedAlgorithm::because() and a parser-backed InvalidAttestation::malformedStatement().
  • $e->context() — ['reason' => '…'] for the four reason-carrying factories, [] otherwise. Laravel’s exception handler merges it into the log entry automatically.

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.