Configuration — Passkey Settings
Everything is configured from the Passkey Settings single DocType (Desk → Passkey Settings). This page documents every field, its default, and the security consequence of changing it, then the way the settings interact with each other and with core’s Two Factor Authentication.
The fieldnames below are the app’s stored fieldnames. The upstream documents propose reusing them in System Settings, but no current-core compatibility or merge is assumed.
Login Modes
| Field | Default | What it does / consequence of changing it |
|---|---|---|
Login with Passkey (login_with_passkey) |
Off | Master switch for passwordless first-factor login and all of its login-page UI. Turning it on puts the passkey button and conditional-UI autofill on the login page and activates begin_login / verify_login. Turning it off removes the UI and refuses those ceremonies (existing credential rows are kept). It cannot be turned off if that would leave no passkey-capable mode enabled while any user is Passkey Only Login (see the matrix below). |
Passkey as Second Factor (passkey_as_second_factor) |
Off | Adds a passkey step-up after a correct password. Requires core Two Factor Authentication to stay on — enabling this is refused if System Settings → Enable Two Factor Authentication is off (see “Two-factor floor” below). Off by default. |
Allow OTP Fallback for Passkey Second Factor (passkey_2fa_allow_otp_fallback) |
On | When on, a user doing the passkey second factor can choose “use a one-time code instead” and complete with core’s OTP. Turning it off removes the downgrade path for passkey holders — a lost authenticator then needs admin recovery. This is enforced server-side, not just in the UI: fallback_to_otp re-checks the live setting, and final session minting re-checks that core 2FA still covers the user before consuming the one-time marker. |
Relying Party
Enabling either authentication mode also uses the site’s encryption_key: second-factor ceremonies
bind password-hash versions with it, and lifecycle exports use it for integrity. When
site_config.json has no key, Frappe core’s get_encryption_key() creates and saves one on save, as
it does for any encrypted field. An entry that is present but empty is refused. Back the key up
separately before enrolling production users, and restore the original key (not a new one) when
recovering existing encrypted data or a credential export.
| Field | Default | What it does / consequence of changing it |
|---|---|---|
Passkey RP ID (passkey_rp_id) |
blank ⇒ resolved from host_name |
The bare host a passkey is bound to (no scheme, port, or path). Blank means “use the exact host of the site’s host_name”. Changing it makes passkeys for the previous RP stale until it is changed back; retained enabled rows work again on revert. The confirmation previews enabled passkeys/users that stop or start working. Widening to a parent RP still requires new passkeys. See domain moves. |
Passkey Origins (passkey_origins) |
blank ⇒ no additional origins | Exact web origins, one per line; explicit ports are allowed. The exact origin from host_name is included automatically only when its host equals the RP ID or is a subdomain of it. The RP ID never implies trust in https://<rp_id>. Enabling a login mode is refused when the resolved web-origin set is empty. Each origin must be HTTPS (http://localhost is allowed only under developer_mode) and within RP-ID scope. |
For example, host_name = https://login.example.com and RP ID example.com trusts
https://login.example.com; it does not also trust https://example.com. Add the apex origin
explicitly only if the site or an iOS app genuinely uses it. The read-only Resolved
Configuration panel shows the exact server-resolved set, plus the RP-change warning and stale
inventory. The System Manager Desk alert distinguishes
no working path, an app-only path, and web access at other configured origins. It uses configured
host_name, not the request Host header.
Mobile Apps
If a native iOS / Android app should share the site’s passkeys, the Mobile
Apps fields configure the trusted native origins and the two well-known
association files: passkey_app_origins, passkey_android_package_name,
passkey_android_cert_fingerprints, passkey_ios_team_id, and
passkey_ios_bundle_id. They are documented in full — including how to obtain the
Android signing-certificate fingerprint and how the association files are served —
in mobile-apps.md. Leave them blank for a web-only site.
Enrollment
Require a passkey from decides who must enroll after they sign in. That prompt is a post-login interstitial: the session already exists, so it raises friction toward enrollment and is never a server-side login block. Phone / QR enrollment is always offered; a device counts as capable when it has a platform authenticator or a cross-device option. Users outside the scope take Everyone else. A blank Starting on means immediately; until that date, in-scope users are nudged instead of required. A per-user exemption beats every scope rule, including System Managers.
| Field | Default | What it does / consequence of changing it |
|---|---|---|
Require a passkey from (passkey_enforce_scope) |
No one | No one, Selected roles, or All users. In-scope users must enroll after signing in, after grace sign-ins. |
Roles (passkey_enforce_roles) |
empty | Shown when scope is Selected roles. A user is in scope if they hold any of these roles. |
Always require a passkey from System Managers (passkey_enforce_privileged_always) |
On | Adds System Managers (and Administrator) to the scope above, whatever it is. Hidden when scope is All users, because they are already included. Turning it off under Selected roles warns. A per-user exemption still wins. |
Starting on (passkey_enforce_after) |
blank | Blank means immediately. Until this date, in-scope users are nudged instead of required. A past date is immediate. Shown when scope is not No one, or System Managers are always included. |
Everyone else (passkey_everyone_else) |
Nudge | Users outside the scope: Nudge (a dismissible prompt, capped below) or Off (nothing). Hidden when scope is All users. The exempt marker takes this rung too. |
Grace sign-ins (passkey_enforce_grace_logins) |
3 | How many more sign-ins an in-scope user may defer the enrollment prompt before it becomes blocking. A defer can consume at most one grace login per session, even if the endpoint is retried or multiple tabs render the prompt. 0 blocks immediately. Shown under When a passkey is required. |
Device cannot create a passkey (passkey_enforce_incapable) |
Degrade to Nudge | What to do when a device genuinely cannot create a passkey (no platform authenticator and no cross-device option). Degrade to Nudge uses the ordinary opt-out, prompt cap and cooldown on both Desk and portal, and never locks the device out; Block + Notify Admin keeps prompting and records a risk event instead. |
Maximum prompts per user (passkey_nudge_max_prompts) |
3 | How many times the dismissible prompt may appear. Applies to Everyone else = Nudge, to in-scope users before Starting on, and to incapable devices under Degrade to Nudge. Counters are server-side per user (a three-browser user gets 3 prompts total, not 9). |
Days between prompts (passkey_nudge_cooldown_days) |
30 | Minimum days between those prompts. |
Conditional Create (passkey_conditional_create) |
On | Lets the browser silently create a passkey after a password login when the platform supports it (no dialog). The server only allows this off a password-seeded fresh-login window. Off: only the explicit nudge/enroll flow creates passkeys. |
The enforcement preview counts users in the background on save, daily, or when you select Recalculate; opening the page only reads the cached count. Only the saved policy’s count is shown, with the time counting started; a policy without one shows Not counted yet. After a save or recount the open form checks every three seconds, up to ten times, for the new count. A recount that started earlier never replaces a newer one, and if the cache is cleared the preview shows Not counted yet until the next count. A full job queue does not prevent saving the policy, but its preview may remain uncounted or keep the previous count for that policy. Recalculate reports when the recount cannot start; retry after queue capacity is available.
For sites upgrading from before 15.0.0, the migration maps the old enrollment policy and scope to these settings. Non-enforcing policies also turn off Always require a passkey from System Managers. Users with the legacy Passkey Enforcement Exempt role keep their exemption as a per-user flag; the role assignments are left in place.
A user can’t get past enforcement — what to do
Enforce is a post-login interstitial, not a pre-session authentication block, but Block + Notify Admin can still prevent useful application access. The levers below go from least to most drastic. Pick the narrowest one that fits.
- They said “I can’t set one up here.” With the default Device cannot create a passkey (Degrade to Nudge) the interstitial already let them through as a dismissible nudge (or silently when opted out, capped or cooling down), without emailing administrators. Nothing is blocking; help them enroll on a capable device (or issue a security key) when convenient. Only Block + Notify Admin keeps the gate up; there the button reads Contact administrator, because clicking it emails the System Managers. The fixes below apply then.
- Exempt this one user (one click). Open the user’s User form → the
Passkeys section (System Managers see it on anyone’s form). While a passkey
is required from someone, it shows two admin actions; click Exempt
from passkey enforcement. The exemption is stored per user by the app, not as a role,
so a Role Profile cannot undo it. The user takes the Everyone else rung immediately,
including a
System Managercovered by Always require a passkey from System Managers. Remove enforcement exemption reverses it. There is no role-wide exemption setting. - Give them more grace logins. In the same section, Reset grace logins restores the user’s full deferral budget (the Grace sign-ins count) so the interstitial goes back to “Remind me later” instead of blocking. Use it to buy a capable-but-not-right-now user time to enroll.
- Adjust scope or roles. If a whole non-privileged group is caught wrongly, set Require a passkey from to Selected roles and list Roles. This takes effect on the next login. System Managers stay in scope while Always require a passkey from System Managers is on.
- If no System Manager can reach Desk, follow No System Manager can sign in. Lifting the requirement for the whole site is its last resort, not a way to let one user through.
There are no standing role-wide exemptions. Administrators are enforced first by default; recovery is an explicit per-user exemption.
Enforcement, Passkey Only Login and disabled password login govern interactive sign-in only; API-key and OAuth-token requests are not subject to them (see API keys and OAuth tokens).
Security
| Field | Default | What it does / consequence of changing it |
|---|---|---|
Hard-fail on Sign Count Regression (passkey_sign_count_hard_fail) |
Off | Controls what happens when an authenticator presents a signature counter lower than the stored value (a possible clone signal). Off (default): the sign-in proceeds but the credential is flagged and its owner is emailed. On: such an assertion is rejected. A counter that is equal and non-zero (a replay) is always rejected regardless of this knob. |
Maximum Passkeys per User (passkey_max_per_user) |
10 | Per-user credential cap; registration is refused once a user reaches it. Lowering it does not delete existing rows. |
Re-authentication Window (Seconds) (passkey_reauth_window) |
600 | Lifetime of the “sudo” window — how long after a fresh password or passkey login (or a password / passkey re-auth) the management surface lets a user add or delete passkeys without confirming again. Longer is more convenient and less strict; shorter re-prompts sooner. Does not affect action-confirmation grants, which are always single-use and short-lived. |
Allow First Passkey After Email-Link or Social Sign-in (passkey_allow_first_enrollment_on_external_login) |
On | When Login with Passkey is also on, lets a user with no usable passkey who signed in by email link, social/OAuth, LDAP or any other login the app does not classify as a password or passkey login, enroll a first passkey within the Re-authentication Window. The app cannot re-verify those sign-ins itself, so they unlock only this one step, and each use records a risk event. It is refused on second-factor-only sites because the same external login would be vetoed after enrollment while no passkey first-factor route exists. Off: social-only accounts with no password cannot bootstrap a passkey. Subsequent adds always need a full sudo window. |
Notifications
| Field | Default | What it does / consequence of changing it |
|---|---|---|
Notify on Passkey Changes (passkey_notify_on_change) |
On | Sends the account owner an email (with label, time, and IP) whenever a passkey is added, removed, disabled, or flagged. This email is the compensating control for registration hijack — a user who did not initiate the change can react. Turning it off is warned against; only do so on mail-less development sites. Activity Log rows are written either way. |
Notify on Password Fallback (passkey_notify_password_fallback) |
Off | When on, emails a passkey holder if a sign-in used a one-time code instead of their passkey. Opt-in telemetry; off by default. |
Notify on Password Login by Passkey Holder (passkey_notify_password_login) |
Off | When on, records an Activity Log risk event whenever a user who holds at least one usable passkey signs in with their password instead. This is telemetry, not an email — it surfaces accounts still leaning on the password so an operator can drive enrollment. Opt-in and off by default specifically so the “does this user have a passkey?” lookup never touches the hot login path on a default site; a site that wants the signal turns it on. |
The two-factor floor (both directions)
“Passkey as Second Factor” is enforced structurally, by keeping core’s Two Factor Authentication turned on. The app guards this pairing from both sides:
-
Enabling passkey 2FA requires System Settings → Enable Two Factor Authentication to already be on. Saving Passkey Settings with
passkey_as_second_factoron while core 2FA is off is refused. The reason: the app relies on core’s 2FA machinery as a structural floor in addition to its own login veto. If core 2FA is on but no role has two-factor enabled, the save proceeds with an orange warning (the backstop then covers nobody — enable it for roleAll). -
Disabling core Two Factor Authentication is refused while
passkey_as_second_factoris on. Attempting to turn Enable Two Factor Authentication from on to off in System Settings throws, because it would silently evaporate the backstop while the passkey-2FA UI kept working. Turn off “Passkey as Second Factor” in Passkey Settings first.
A raw bench console / db_set edit can bypass both validators and leave the
two desynced (passkey_as_second_factor=1 while core 2FA is off). The final login
veto still blocks enrolled users, and the security posture panel on the Passkey
Settings page flags the missing floor.
For a user with a usable passkey, the second-factor requirement is enforced at the final login
hook, not only by the app’s login-page JavaScript. Password, email-link, social/OAuth, LDAP, and
other core login paths are vetoed unless this app completed the passkey step or explicitly handed
the user to core OTP via fallback_to_otp. That OTP handoff creates a short-lived, one-time marker
bound to core’s tmp_id; a direct call to core OTP without that marker remains blocked. The rule
also applies to Administrator after Administrator has explicitly enrolled a usable passkey.
Every password-to-passkey ceremony captures a keyed, non-reversible version of the current password hash at the password leg and compares it again immediately before session minting. A password rotation during the ceremony therefore fails closed without retaining the password for the normal passkey-only leg. The password itself is retained only where an allowed OTP fallback, or an external authentication account without a local hash marker, requires core to re-authenticate.
Settings interaction matrix (operator terms)
| Situation | What happens |
|---|---|
| Both login modes off (freshly installed) | Legal “paused” state. Login and second-factor ceremonies refuse, all UI removes itself, credential rows are preserved, and the action-confirmation primitive still works. |
| Only “Passkey as Second Factor” on (“2FA-only” site) | Legal for users who can start the app’s local password flow. The passwordless login button does not appear. An enrolled social-, LDAP-, or email-link-only account cannot complete those alternate paths because the final veto requires this app’s passkey/OTP handoff; keep “Login with Passkey” on for such accounts. |
| Enable passkey 2FA while core 2FA is off | Refused at save. Turn on Two Factor Authentication first. |
| Turn core Two Factor Authentication off while passkey 2FA is on | Refused at save. Turn off “Passkey as Second Factor” first. |
| Turn off the last passkey-capable mode while a Passkey-Only user exists | Refused at save, with the flagged users listed. Keep a passkey login mode on, or clear “Passkey Only Login” on those users first. This holds whether the last mode is “Login with Passkey” or “Passkey as Second Factor”. |
| A Passkey-Only user does the password→passkey second-factor flow | Allowed — that path is a passkey login, so it satisfies the account’s passkey-only rule. |
| An enrolled second-factor user uses password, email link, social/OAuth, LDAP, or another core login path | The final login is vetoed unless this app completed passkey verification or issued and then consumed the one-time OTP fallback marker. This includes an enrolled Administrator. |
| A Passkey-Only user with Login With Email Link on | The email-link path is closed for that user; only their passkey gets them in. |
| OTP fallback off and a passkey holder loses their authenticator | No self-service downgrade — admin recovery only (see recovery.md). |
| “Passkey as Second Factor” on together with “Disable Username/Password Login” (core) | A dead combination for enrolled users — the app’s password leg has nothing to run against, and alternate login completions are final-vetoed. Refused at save, from either settings page, while “Passkey as Second Factor” is the only passkey mode. With “Login with Passkey” also on, the save proceeds with an orange warning. |
| Change notifications off while a login mode is on | The save proceeds with an orange warning: this weakens the main defence against registration hijack. |
Login-veto rows above apply to users with usable credentials. Stale-only users are not subject to those per-user vetoes; the stored flags and settings-save guards remain in place.
Per-user “Passkey Only Login”
On /passkeys, the switch is labelled Passwordless login only; it is the same
setting as Passkey Only Login on the WebAuthn User Handle.
This is a per-user switch (on the user’s WebAuthn User Handle row), not a
site setting. It disables password / email-link / social first-factor login for
that one user while a usable passkey exists. An existing password hash is kept. With only stale
keys, the flag persists but its veto pauses; it applies again when a usable key exists. It is the
released-branch lever
for “no passwords for this account”; the site-wide equivalent needs the
self-hoster override in operations.md.
- The user’s own switch needs a fresh passkey confirmation — never a password and never a sudo window. (A password must not be able to switch off the very flag that says “a password is not enough”.) Turning it on there requires at least two usable passkeys, so a single lost device never locks the account out.
- A System Manager can clear it on the user’s WebAuthn User Handle form; that is how a user who lost every passkey is recovered (A user lost their passkey). Turning it on from that form requires at least one usable credential and a passkey login mode.
- Administrator is exempt only from this per-user passkey-only login veto. That preserves the
password break-glass path for an Administrator who has not opted into the passkey second factor.
Once Administrator explicitly enrolls a usable credential while Passkey as Second Factor is
active, alternate/core login paths are protected exactly like any other enrolled user’s. The
site-wide
disable_user_pass_loginflag also has no Administrator exemption. Seesecurity.mdandrecovery.md.
Transactional race protection
Security-sensitive read/modify/write paths take database locks in a consistent order. Credential
assertion bookkeeping re-locks the credential before advancing counters; UV setup locks the user and
credential; registration locks the user, handle, and credential census before applying the per-user
cap and insert; passkey-only credential deletion/toggling locks the shared login floor; Passkey
Settings mode changes lock the Single rows before checking flagged users; and the Passkey Settings /
System Settings floor checks read each other’s values with locking reads. These locks protect the credential and mode floors. RP reads in ordinary census and ceremony
effect checks are uncached but non-locking; they can retain a request transaction snapshot. Only
bulk stale removal serializes its RP read with settings saves. See the
RP-change residual risks, including per-request host_name overlap.
Request hardening and numeric limits
Guest endpoint limits also count IPv6 addresses together by /64; IPv4 limits are unchanged.
Credential audit records and notification emails retain the full client IP. Frappe’s own
IP-based login tracker remains under core control; the app does not monkeypatch it.
Email login identifiers are trimmed and lowercased before authentication. Non-email usernames
(including Administrator) are trimmed but retain their case. Discoverable passkey login takes
no identifier; subsequent fallback legs use the canonical user stored by the server.
Login redirects remain core-sanitised by frappe.utils.sanitise_redirect; the app leaves core’s login handler unchanged.
All five integer settings require values within these inclusive ranges when saved:
| Setting | Range | Reason for upper bound |
|---|---|---|
| Maximum Passkeys per User | 0–100 | Allows many devices while bounding credential lists. |
| Maximum prompts per user | 0–100 | Keeps dismissible prompting finite and practical. |
| Grace sign-ins | 0–100 | Allows a staged rollout without an effectively unlimited grace budget. |
| Days between prompts | 0–365 | Allows up to annual reminders. |
| Re-authentication Window (Seconds) | 0–3600 | Limits reuse of fresh authentication to one hour. |
Zero retains existing semantics: the credential cap defaults to 10, the re-authentication window defaults to 600 seconds, zero prompts disables nudges, zero cooldown allows immediate repeat prompting, and zero grace blocks deferral immediately. Existing out-of-range settings must be corrected before their next save; this change does not rewrite stored configuration.