One-time codes
Set mode to 'code' (or 'both') to email a short code instead of a link:
'mode' => 'code',
In 'both' mode the request endpoint issues a link by default, or a code when channel=code
is submitted.
The entropy guardrail
Codes are governed by a boot-time entropy guardrail: the package refuses to boot if a
code's keyspace divided by its attempt lockout falls below entropy_safety_factor, naming
the exact keys to fix and the minimum length that would pass. Magic links carry 256 bits of
entropy and pass trivially.
'code_length' => 8,
'code_alphabet' => 'ABCDEFGHJKMNPQRSTUVWXYZ23456789',
'max_attempts_per_token' => 5,
'entropy_safety_factor' => 1_000_000,
The keyspace of a code is (distinct characters) to the power of the length. Codes are drawn
uniformly from the distinct characters of the alphabet, so repeated characters do not add
entropy. The guardrail requires keyspace / max_attempts_per_token >= entropy_safety_factor
— at most a one-in-N chance of guessing a code within the lockout — and it cannot be lowered
to disable the check. Obviously broken combinations (a zero lifetime, a missing attempt cap)
always fail closed with InsecureMagicLinkConfigurationException.
The default alphabet omits visually ambiguous characters (0/O, 1/I/L) so a code is
readable when it is typed by hand.
The alphabet reaches the field
On the WireKit screens the code screen renders the boxed one-character-per-box field for every alphabet, and the alphabet you configure is what the field accepts: the typing filter, the paste filter, the mobile keyboard hint and the browser validation pattern are all derived from it.
An alphabet whose letters are all one case — the ambiguity-free kind, like the default — also accepts the other case and normalizes it, so a code typed in lowercase is not silently refused. The fold follows the alphabet in both directions: an all-uppercase alphabet accepts lowercase input, an all-lowercase one accepts uppercase, and either way the submitted code is folded toward the case the generator actually mints.
An alphabet that writes BOTH cases is not folded at all, and it cannot be. There a and
A are two different characters, the generator mints both, and folding either direction
would turn a valid code into one that can never be redeemed. Such an alphabet is case
sensitive on submission, and the entropy guardrail counts it at full size because every
character it contains is a character an attacker must guess. Digits-only and symbol-only
alphabets are likewise left alone — they have no case to fold.
Since WireKit 2.26.0 the HTML validation pattern carries both cases as well, so the browser
accepts a lowercase code even on a page where the field's Alpine cannot run — a strict
Content-Security-Policy without 'unsafe-eval', see
the WireKit screens. That policy still has good
reasons to prefer ui.mode = 'blade', where the code is one plain field and your own validation
decides; a rejected lowercase code is no longer one of them.
This is worth stating because it was not always true. Before WireKit 2.22.0 the boxed field was digits-only and enforced that in all four of those places, so an alphanumeric code could not be entered into it at all — every keystroke was discarded and the boxes stayed empty with no message. This package worked around it by rendering a single monospace field whenever the alphabet contained letters. That branch is gone; if you published these views before, republish them.
The plain Blade screens use a single field either way, which is unchanged.
Choosing a numeric alphabet is still a security decision, not a cosmetic one. It no longer buys you a different control, and it costs keyspace: eight digits give 100 million combinations, against roughly 850 billion for the default 31-character alphabet — nearly four orders of magnitude. The entropy guardrail above holds either way; it will refuse to boot rather than let a numeric alphabet fall below your safety factor.
The per-token lockout
max_attempts_per_token is a hard per-token lockout: after that many wrong guesses the token
is burned, and the failure surfaces as ClaimFailure::LockedOut on the
MagicLinkConsumptionFailed event — the
precise signal to alert on.
Give codes their own lifetime
A code is typed by hand, so it often wants a shorter window than a link:
'code_ttl' => 300, // five minutes; links keep the default ttl
A null or non-positive value inherits ttl.
Codes are always single-use
max_uses raises the redemption count of links only. A code is
single-use regardless, and issuing a new code for the same user and guard supersedes the
previous one.