major
#30074
React Interface: Signing In with Passkeys (WebAuthn) — passwordless or as a second factor alongside TOTP; managing your own passkeys in the settings
Context
TopLogic's multi-factor authentication (#29092) previously only supported a time-based one-time password (TOTP, RFC 6238) from an authenticator app as the second factor (tl.accounts:Person#mfaSecret, #mfaRequirement). Passkeys (FIDO2/WebAuthn)—key pairs tied to a device, a security key, or a password manager, which the browser unlocks using a PIN or biometrics—were not supported.
Using the tl-security-passkey module, a user logs in to the React interface (.view.xml level) with a passkey. The classic interface continues to use a password and TOTP.
Behavior
- Intended use per application: The application uses the `usage` option of the `PasskeyService` to specify the purpose of passkeys and offers only the appropriate setup: `login ` (default)—logging in without a password—or `second-factor `—a passkey as a second factor after the password. Never both at the same time (#30118).
- Password-free login (login): The login form displays “Log in with Passkey” below the single sign-on buttons, with the same width as those buttons. The browser prompts for a passkey registered for the application (access credentials that can be retrieved; no username required); after authorization on the device, the user is logged in. Passkeys are created to be discoverable and always require user verification (PIN/biometrics); the login is therefore considered two-factor and meets the mfaRequirement required. A passkey is not a second factor here: After logging in with a password, no passkey is required.
- Second factor (second-factor): The login form does not offer login via passkey. Passkeys are created as non-discoverable. An account with a passkey confirms the login with the passkey after entering the password; an account with a passkey and an authenticator app selects the method in a selection dialog. An account with “required” but no second factor allows the user to choose during sign-in whether to set up an authenticator app or a passkey; creating the passkey completes the sign-in process.
- The service rejects any authentication method that does not match the intended use. Saved passkeys are retained when switching; a passkey set up as a second factor will not allow login without a password after switching to “login,” so the user must then create a new one.
- Application address: Passkeys are bound to the application’s public address (%HOST%, set via the tl_host variable, e.g., https://apps.example.com); the address of an individual HTTP request is irrelevant, so passkeys also work behind a reverse proxy that terminates TLS. If the address is not set, the application reports this as an error at startup and does not offer passkeys.
- Incorrect or insecure page: If the user opens the application at an address other than the one configured, the passkey buttons are disabled from the start and display the address where passkeys work in a tooltip; no passkey is created that the application does not recognize. On a page opened via HTTP using a computer name, the tooltip explains: “Passkeys require a secure connection. Open the application via HTTPS (or on localhost).”
- Messages: A rejected registration or login will specify the reason: incorrect address, passkey unknown to the application (“This passkey is not registered with this application. It may not have been fully set up, may have been deleted, or may belong to a different installation. …”), a passkey from another account, user not verified, signature counter, or expired request. Errors reported by browsers or authenticators appear with the browser’s text. Each rejection is logged as a warning with the reason, credential ID, account, and page address.
- Cleaning Up the Password Manager: If registration fails after the browser has already saved the passkey, or if the browser presents a passkey unknown to the application during login (e.g., a deleted one), the application notifies the browser (via the WebAuthn Signal API), and the password manager removes it. After logging in with a passkey, the browser receives the list of valid passkeys for the account. Browsers without the Signal API ignore this.
- Settings → Security is divided into sections, each with a heading, current status, and actions: Password (Change Password), Proxies (Proxies, Represented Persons, Appoint Proxy), and Two-Factor Authentication with the status “Active: Authenticator App, Passkey" or "Not active" and the sub-section Authenticator App (Set up?, Set up/Turn off). Your own passkeys (Name, Created, Last Used; Add, Rename, Delete) appear in a separate section titled “Sign In with Passkey” after “Password” upon login, and as the “Passkeys” sub-section under “Two-Factor Authentication” when used as a second factor. Adding and deleting a passkey requires the password (<verify-identity>). The last second factor for an account marked as “required” cannot be deleted; the Delete button is then disabled and displays the reason.
- Disabling the Authenticator App: An account marked as “required” may disable the Authenticator App as long as another method remains configured; the requirement remains “required.”
- Administration by the administrator: The account management interface (admin/access-control/accounts.view.xml) displays an account’s passkeys and can delete them (without the “last factor” rule: the account will set up a new factor upon the next login).
- An account with the "disabled" setting cannot create passkeys when "second-factor" is enabled; existing passkeys remain usable (like a saved TOTP secret). When "login" is set, the passkey is a login method like a password and is also permitted for an account with the "disabled" setting.
Technical Implementation
- Library: The WebAuthn ceremonies (challenge, Origin and RP-ID verification, signature, signature counter) are handled by Yubico’s java-webauthn-server (com.yubico:webauthn-server-core 2.10.1, Apache-2.0), version listed in tl-bom (along with com.upokecenter:cbor, which declares the library as a version range). The supported signature algorithms are ES256, EdDSA, ES384, ES512, and RS256.
- Generic client element in com.top_logic.layout.react: ReactWebAuthnControl / TLWebAuthn is a standard button that executes `navigator.credentials.create ` / `get` with the server’s options directly within the click handler (Safari requires user interaction) and reports the result to a ` WebAuthnListener `. Before the options expire, it automatically retrieves new ones (refresh); an insecure page or a browser without WebAuthn is reported upon display (unavailable), as is the page’s origin (origin, WebAuthnListener#onPageOrigin). A hidden button does not draw anything or initiate any actions. `signalUnknownCredential ` and ` signalAllAcceptedCredentials` call the functions of the same name in the browser’s WebAuthn Signal API (a one-time `FunctionCall` event to `TLReact.webAuthnSignal`). The owner reports errors via ViewMessages.
- Configurable second factor in com.top_logic.layout.view: SecondFactor (set up? / configurable? / confirm / set up), SecondFactorsConfig (list; modules contribute via autoconf ), SecondFactorStep (decision and selection dialog second-factor-choice.view.xml), TotpSecondFactor. LoginAction does not recognize passkeys. TL script secondFactorEnrolled(account) for the status bar; DisableMfaAction checks the remaining configured methods.
- TypeObservingRule (com.top_logic.layout.view): an intrinsic executability rule that reads additional objects and re-evaluates when they change (counterpart to observed-types). Used by <delete-passkey/>.
- Empty elements take up no space: A <switch> without a matching case (and <visible-if> in the hidden state) remains hidden; an empty <object-list> displays only its placeholder text; TLStack, TLGrid, and TLDeckPane respect the hidden flag; <login-commands> and <login-methods> are hidden when empty. Previously, they left behind empty elements that created additional spacing in their container.
- Extension points: login-form.view.xml contains the IDs credentials and login-methods; the “Security” tab in settings-tabs.view.xml contains the IDs security-sections, password-section, deputies-section, two-factor-section, two-factor-methods, and authenticator-app; tl-security-passkey extends both views via an overlay.
- tl-security-passkey module: Type tl.security.passkey:Passkey (credential ID with unique index, COSE public key, signature counter, transports, backup flags, name, creation date, and usage date) as a composition of tl.accounts:Person#passkeys; Permissions via the account (access-parent). PasskeyService with RelyingParty and CredentialRepository. The Relying Party is determined once at startup from %HOST% (RelyingPartyAddress: Origin = %HOST% normalized, RP-ID = its hostname), never from a request; Override rp-id (e.g., parent domain) and origins (replacing the Origin from %HOST%); an RP-ID must be the computer name or parent domain of each Origin. Additional configuration options: usage (PasskeyUsage: login / second-factor; determines the residentKey and user verification for registration and accepted logins), rp-name, user-verification (for second-factor), timeout. Rejections are handled as ` PasskeyRefusedException ` with a category and credential ID; the service determines the category through its own checks prior to the library. The user handle is the object ID of the account. View elements : <passkey-login> (hidden if the intended use does not permit this type of login), <passkey-registration> (complete-login, <on-success>), and <if-passkey-usage usage="…"> (displays its content only for the specified use case), PasskeyCeremony (verification of the page’s origin, signals to the browser), PasskeySecondFactor (only for second-factor), <delete-passkey/>, TL script passkeyMayUse(account), overlays for the login form, settings, and account management. tl-demo-react integrates the module.
- Fixed: Person.setMFASecret was writing the secret to the mfaRequirement attribute instead of mfaSecret.
- Documentation: Developer articles on authentication/passkeys (tl-security-passkey), revised authentication/mfa, authentication, view-layer/commands, and view-layer/overlays.
Not part of this ticket: Passkey suggestions in the username field’s autofill (autocomplete="username webauthn"), attestation checks / device allowlists, recovery codes, and the classic interface.
Verification
Tests: TestReactWebAuthnControl, webauthn-json.test.ts, webauthn-signals.test.ts, options-expiry.test.ts, hidden-containers.test.tsx, TLWebAuthn.test.tsx, TestSecondFactorStep, TestDisableMfaAction, TestSecondFactorFunctions, TestSwitchElement, TestObjectListItems, TestRelyingPartyAddress, TestPasskeyService (registration and sign-in with a software authenticator for each use case; Relying Party from %HOST%, missing address, incorrect Origin, unknown passkey, repeated response, third-party account, missing user verification, counter rollback, sign-in for the wrong use case), TestPasskeyCeremony (including page origin, signals, button per use case), TestPasskeyUsageElement, TestDeletePasskeyAction, TestPasskeyFunctions, TestPerson#testSetMFASecret. Tested in tl-demo-react (/view/, root / root1234) using Chrome’s virtual authenticator, for both use cases: Setup during login, second factor, passwordless login, cancellation, login after the form has been open for over 5 minutes, disabled button on an HTTP page and at a different address (127.0.0.1 instead of localhost), Login with a passkey deleted on the server (message, warning in the log, passkey removed from the authenticator), Adding/renaming/deleting in Settings, the “Last Factor” rule, deletion by the administrator, and intervals in the sign-in form and the “Security” tab.