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

config/passkeys.php documents every option. It is parsed into a typed PasskeyConfig and validated when the app boots, so a misconfiguration fails there rather than at the first registration. The security-critical values are rp.id and origins — they cannot be safely defaulted and must be set for a ceremony to run. The published file, without its block comments:

use RoundlyConsulting\Crypto\Cose\CoseAlgorithm;
use RoundlyConsulting\Passkeys\Enums\AttestationConveyance;
use RoundlyConsulting\Passkeys\Enums\AttestationTrust;
use RoundlyConsulting\Passkeys\Enums\ResidentKey;
use RoundlyConsulting\Passkeys\Enums\SignCountPolicy;
use RoundlyConsulting\Passkeys\Enums\UserVerification;
use RoundlyConsulting\Passkeys\Models\Passkey;

return [
    'rp' => [
        'id' => env('PASSKEYS_RP_ID'),
        'name' => env('PASSKEYS_RP_NAME', env('APP_NAME', 'Laravel')),
    ],

    'origins' => array_values(array_filter(
        explode(',', (string) env('PASSKEYS_ORIGINS', '')),
    )),
    'allow_cross_origin' => env('PASSKEYS_ALLOW_CROSS_ORIGIN', false),

    'algorithms' => [
        CoseAlgorithm::ES256->value,   // -7
        CoseAlgorithm::RS256->value,   // -257
        // CoseAlgorithm::EdDSA->value, // -8 (requires ext-sodium)
    ],

    'timeout_ms' => env('PASSKEYS_TIMEOUT_MS', 60_000),
    'attestation' => env('PASSKEYS_ATTESTATION', AttestationConveyance::None->value),
    'user_verification' => UserVerification::Required->value,
    'resident_key' => env('PASSKEYS_RESIDENT_KEY', ResidentKey::Required->value),

    'challenge' => [
        'store' => env('PASSKEYS_CHALLENGE_STORE'),
        'ttl' => env('PASSKEYS_CHALLENGE_TTL', 60),
        'bytes' => 32,
    ],

    'sign_count_policy' => SignCountPolicy::Flag->value,

    'attestation_trust' => env('PASSKEYS_ATTESTATION_TRUST', AttestationTrust::Ignore->value),
    'reject_unknown_fmt' => env('PASSKEYS_REJECT_UNKNOWN_FMT', false),
    'attestation_anchors' => [
        'defaults' => env('PASSKEYS_ATTESTATION_DEFAULT_ANCHORS', true),
        'paths' => [
            // 'packed' => [storage_path('webauthn/vendor-fido-ca.pem')],
        ],
    ],
    'attestation_clock_skew' => env('PASSKEYS_ATTESTATION_CLOCK_SKEW', 60),
    'aaguids' => [
        'allowed' => array_values(array_filter(
            explode(',', (string) env('PASSKEYS_AAGUIDS_ALLOWED', '')),
        )),
    ],

    'user' => [
        'handle_column' => env('PASSKEYS_USER_HANDLE_COLUMN', 'passkey_user_handle'),
        'handle_bytes' => 32,
        'name_attribute' => 'email',
        'display_name_attribute' => 'name',
    ],

    'model' => Passkey::class,
    'table' => 'passkeys',
    'key_type' => env('PASSKEYS_KEY_TYPE', 'bigint'),
];

Every key

KeyEnvDefaultPurpose
rp.idPASSKEYS_RP_IDhost of app.urlRelying-party ID — a registrable-domain suffix of every origin (e.g. example.com).
rp.namePASSKEYS_RP_NAMEAPP_NAMEHuman-readable RP name shown by the authenticator.
originsPASSKEYS_ORIGINS[]Comma-separated allow-list of exact clientData.origin values; at least one is required.
allow_cross_originPASSKEYS_ALLOW_CROSS_ORIGINfalseAccept a cross-origin (iframe) ceremony.
algorithms—ES256, RS256COSE algorithms offered and accepted, in preference order.
timeout_msPASSKEYS_TIMEOUT_MS60000Ceremony timeout hint sent to the browser, at least 1.
attestationPASSKEYS_ATTESTATIONnoneAttestation conveyance: none, indirect or direct.
user_verification—requiredUser-verification requirement: required, preferred or discouraged.
resident_keyPASSKEYS_RESIDENT_KEYrequiredDiscoverable-credential posture; required keeps usernameless login.
challenge.storePASSKEYS_CHALLENGE_STOREdefault storeCache store name for challenges.
challenge.ttlPASSKEYS_CHALLENGE_TTL60Minimum challenge lifetime in seconds, at least 1; a challenge always lives at least as long as its ceremony’s timeout.
challenge.bytes—32Random challenge length in bytes, at least 16.
sign_count_policy—flagCounter regression: reject throws, flag fires an event and proceeds.
attestation_trustPASSKEYS_ATTESTATION_TRUSTignoreAttestation trust ladder: ignore, self or basic.
reject_unknown_fmtPASSKEYS_REJECT_UNKNOWN_FMTfalseUnder ignore, refuse formats the package can’t verify and statements that don’t verify.
attestation_anchors.defaultsPASSKEYS_ATTESTATION_DEFAULT_ANCHORStrueTrust the shipped Apple WebAuthn and Google hardware-attestation roots.
attestation_anchors.paths—[]format => [absolute PEM paths] — your own trust anchors.
attestation_clock_skewPASSKEYS_ATTESTATION_CLOCK_SKEW60Leeway in seconds (0–3600) on both bounds of a certificate’s validity window.
aaguids.allowedPASSKEYS_AAGUIDS_ALLOWED[]Comma-separated AAGUID allow-list; empty allows every authenticator model.
user.handle_columnPASSKEYS_USER_HANDLE_COLUMNpasskey_user_handleHost column holding the opaque user handle.
user.handle_bytes—32Random bytes in a generated user handle, 16–64.
user.name_attribute—emailModel attribute used as the account name.
user.display_name_attribute—nameModel attribute used as the display name.
model—Passkey::classThe credential model; point it at a Passkey subclass to add behaviour. Anything else throws InvalidConfigurationException.
table—passkeysThe credential table — set it before migrating (the migration reads it).
key_typePASSKEYS_KEY_TYPEbigintOwner morph key type: bigint, uuid or ulid (case-insensitive; unset or blank reads as bigint, anything else throws InvalidConfigurationException) — set it before migrating.

Environment

The common knobs are env-driven, so most hosts never publish the config at all:

PASSKEYS_RP_ID=example.com
PASSKEYS_RP_NAME="Example"
PASSKEYS_ORIGINS=https://example.com,https://www.example.com
PASSKEYS_TIMEOUT_MS=60000
PASSKEYS_CHALLENGE_TTL=60
PASSKEYS_ATTESTATION=none
PASSKEYS_ATTESTATION_TRUST=ignore

Boolean switches

The three switches — allow_cross_origin, reject_unknown_fmt and attestation_anchors.defaults — accept the usual env spellings: true/false, 1/0, on/off and yes/no. Not set — absent, null or blank ('', what KEY= in .env gives) — reads as the default shown in the table; anything else (a typo such as disabled) throws InvalidConfiguration naming the key instead of quietly reading as the default:

PASSKEYS_ALLOW_CROSS_ORIGIN=off
PASSKEYS_REJECT_UNKNOWN_FMT=yes
PASSKEYS_ATTESTATION_DEFAULT_ANCHORS=0

Strict values

Every other key is just as strict. Not set (absent, null or blank) reads as the default; a present value of the wrong shape throws InvalidConfiguration naming the key:

  • Integers — timeout_ms ≥ 1, challenge.ttl ≥ 1, challenge.bytes ≥ 16, user.handle_bytes 16–64, attestation_clock_skew 0–3600 — take an int or a canonical integer string, so “five”, “1.5” or “1e3” throws rather than becoming 0 or being clamped.
  • The enum keys — attestation, attestation_trust, user_verification, resident_key and sign_count_policy — take a case or its exact value; a typo throws, it never reads as the default.
  • origins, aaguids.allowed, algorithms and attestation_anchors.paths must be lists of valid entries; a non-list or a bad entry throws rather than being dropped. An empty algorithms list throws too — leave the key unset or blank for the defaults.
  • The string keys — rp.name, the user.* column and attributes, and table — must be strings, and a blank one is not set, so its default applies (rp.name → APP_NAME); rp.id and challenge.store must be strings when set (blank is not set, so rp.id is derived from app.url and challenges use the default store).

Fail-fast validation

  • A ceremony without rp.id or without any origins throws InvalidConfiguration (missingRpId / emptyOrigins).
  • The zero-config defaults always boot — only rp.id and origins wait for a ceremony. The checks below run when the app boots.
  • A COSE algorithm outside ES256, RS256 and EdDSA throws InvalidConfiguration at boot.
  • attestation_trust stricter than ignore while attestation is none fails at boot — otherwise every registration would be refused for a reason nobody could see.
  • attestation_clock_skew outside 0–3600 fails at boot; an unreadable or PEM-less trust-anchor path throws at first use.
  • challenge.bytes below 16 or user.handle_bytes outside 16–64 throws InvalidConfiguration at boot instead of being clamped.

Checking your setup

The package contributes a section to php artisan about. It reports the security posture — trust tier, conveyance, verification requirements, counts, TTLs and SET/MISSING presence — but never the RP ID, the origins, anchor paths or the AAGUIDs themselves:

php artisan about

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.