Configuration
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
| Key | Env | Default | Purpose |
|---|---|---|---|
rp.id | PASSKEYS_RP_ID | host of app.url | Relying-party ID — a registrable-domain suffix of every origin (e.g. example.com). |
rp.name | PASSKEYS_RP_NAME | APP_NAME | Human-readable RP name shown by the authenticator. |
origins | PASSKEYS_ORIGINS | [] | Comma-separated allow-list of exact clientData.origin values; at least one is required. |
allow_cross_origin | PASSKEYS_ALLOW_CROSS_ORIGIN | false | Accept a cross-origin (iframe) ceremony. |
algorithms | — | ES256, RS256 | COSE algorithms offered and accepted, in preference order. |
timeout_ms | PASSKEYS_TIMEOUT_MS | 60000 | Ceremony timeout hint sent to the browser, at least 1. |
attestation | PASSKEYS_ATTESTATION | none | Attestation conveyance: none, indirect or direct. |
user_verification | — | required | User-verification requirement: required, preferred or discouraged. |
resident_key | PASSKEYS_RESIDENT_KEY | required | Discoverable-credential posture; required keeps usernameless login. |
challenge.store | PASSKEYS_CHALLENGE_STORE | default store | Cache store name for challenges. |
challenge.ttl | PASSKEYS_CHALLENGE_TTL | 60 | Minimum challenge lifetime in seconds, at least 1; a challenge always lives at least as long as its ceremony’s timeout. |
challenge.bytes | — | 32 | Random challenge length in bytes, at least 16. |
sign_count_policy | — | flag | Counter regression: reject throws, flag fires an event and proceeds. |
attestation_trust | PASSKEYS_ATTESTATION_TRUST | ignore | Attestation trust ladder: ignore, self or basic. |
reject_unknown_fmt | PASSKEYS_REJECT_UNKNOWN_FMT | false | Under ignore, refuse formats the package can’t verify and statements that don’t verify. |
attestation_anchors.defaults | PASSKEYS_ATTESTATION_DEFAULT_ANCHORS | true | Trust the shipped Apple WebAuthn and Google hardware-attestation roots. |
attestation_anchors.paths | — | [] | format => [absolute PEM paths] — your own trust anchors. |
attestation_clock_skew | PASSKEYS_ATTESTATION_CLOCK_SKEW | 60 | Leeway in seconds (0–3600) on both bounds of a certificate’s validity window. |
aaguids.allowed | PASSKEYS_AAGUIDS_ALLOWED | [] | Comma-separated AAGUID allow-list; empty allows every authenticator model. |
user.handle_column | PASSKEYS_USER_HANDLE_COLUMN | passkey_user_handle | Host column holding the opaque user handle. |
user.handle_bytes | — | 32 | Random bytes in a generated user handle, 16–64. |
user.name_attribute | — | Model attribute used as the account name. | |
user.display_name_attribute | — | name | Model attribute used as the display name. |
model | — | Passkey::class | The credential model; point it at a Passkey subclass to add behaviour. Anything else throws InvalidConfigurationException. |
table | — | passkeys | The credential table — set it before migrating (the migration reads it). |
key_type | PASSKEYS_KEY_TYPE | bigint | Owner 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=ignoreBoolean 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=0Strict 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 aboutShow 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 cryptoBy 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.