Passkeys Frontend Authentication 

Extension key

nr_passkeys_fe

Package name

netresearch/nr-passkeys-fe

Version

main

Language

en

Author

Netresearch DTT GmbH

License

This document is published under the GPL-2.0-or-later license.

Rendered

Tue, 24 Mar 2026 06:24:35 +0000


Passwordless TYPO3 frontend authentication for fe_users via WebAuthn/FIDO2 Passkeys. Enables login with TouchID, FaceID, YubiKey, and Windows Hello on your frontend login page -- with optional felogin integration, self-service management, recovery codes, and per-site enforcement.


Introduction 

What the extension does, which authenticators are supported, and the full feature list.

Installation 

Install via Composer, activate the extension, and run the database schema update.

Configuration 

Extension settings, site configuration (RP ID), and TypoScript reference.

Quick Start 

Five-minute setup: install, add plugins, and log in with a passkey.

Usage 

Login flows, passkey enrollment, recovery mechanisms, and the self-service management plugin.

Administration 

Backend admin module, enforcement settings, and user management.

Developer Guide 

PSR-14 events, eID API reference, extension points, and architecture notes.

Security 

WebAuthn compliance, threat model, and security hardening.

Multi-Site 

Multi-domain RP ID configuration and site-aware authentication.

Troubleshooting 

Common issues and solutions.

Architecture Decision Records 

Design decisions and rationale (ADR-001 to ADR-012).

Changelog 

Version history and release notes.

Introduction 

What does it do? 

Passkeys Frontend Authentication provides passwordless login for TYPO3 frontend users (fe_users) using the WebAuthn/FIDO2 standard. Frontend users can authenticate with a single touch or glance using biometric authenticators such as TouchID, FaceID, Windows Hello, or hardware security keys like YubiKey -- no password required.

The extension ships two frontend plugins:

  • NrPasskeysFe:Login -- A passkey-first login form. Can replace or supplement the standard felogin plugin.
  • NrPasskeysFe:Management -- A self-service credential management panel for logged-in users (enroll, rename, remove passkeys; generate recovery codes).

A third plugin, NrPasskeysFe:Enrollment, is used as the target for the post-login enrollment interstitial when enforcement is active.

Features 

Passkey-first login 

Discoverable (usernameless) login and username-first flows. Works as a standalone plugin or alongside felogin.

felogin integration 

Hooks into the standard TYPO3 felogin plugin via PSR-14 events to inject a passkey button without replacing the entire login form.

Self-service management 

Frontend users can enroll new passkeys, rename existing ones, and revoke credentials they no longer need -- all from the frontend.

Recovery codes 

Users can generate one-time recovery codes (bcrypt hashed) as a fallback when no authenticator device is available.

Per-site enforcement 

Each TYPO3 site can have an independent RP ID and enforcement level. Enforcement levels: Off, Encourage, Required, Enforced.

Per-group enforcement 

Enforcement level can be set per frontend user group with configurable grace periods.

Post-login interstitial 

When enforcement is Required or Enforced, users without a passkey are shown an enrollment interstitial after login.

Backend admin module 

Administrators can view adoption statistics, manage credentials, and configure enforcement from the TYPO3 backend.

PSR-14 events 

Seven events for extensibility: before/after authentication, before/after enrollment, enforcement resolved, passkey removed, recovery codes generated.

Security hardened 

Builds on nr-passkeys-be for HMAC-signed challenges, nonce replay protection, per-IP rate limiting, and account lockout.

Vanilla JavaScript 

Zero npm dependencies at runtime. The frontend JavaScript uses only the native WebAuthn browser API and TYPO3 Ajax helpers.

TYPO3 v13 and v14 

Compatible with TYPO3 13.4 LTS and 14.1+. PHP 8.2, 8.3, 8.4, and 8.5 supported.

Supported authenticators 

Any FIDO2/WebAuthn-compliant authenticator works, including:

  • Apple TouchID and FaceID (macOS, iOS, iPadOS)
  • Windows Hello (fingerprint, face, PIN)
  • YubiKey 5 series and newer
  • Android fingerprint and face unlock
  • Any FIDO2-compliant hardware security key

Browser support 

WebAuthn is supported by all modern browsers:

Browser Version
Chrome / Edge 67+
Firefox 60+
Safari 14+
Chrome for Android 70+
Safari for iOS 14.5+

Screenshots 

Relationship to nr-passkeys-be 

nr_passkeys_fe requires netresearch/nr-passkeys-be as a Composer dependency. It reuses the backend extension's WebAuthn ceremony implementation, challenge service, and rate limiter. The backend extension installs its own login module and BE credential table -- these are present but unused on FE-only sites.

See ADR-001 for the rationale.

Installation 

Prerequisites 

  • TYPO3 13.4 LTS or TYPO3 14.1+
  • PHP 8.2, 8.3, 8.4, or 8.5
  • netresearch/nr-passkeys-be ^0.6 (installed automatically)
  • HTTPS is required for WebAuthn (except localhost during development)
  • A configured TYPO3 encryption key ($GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'], minimum 32 characters)

Installation via Composer 

This is the recommended way to install the extension:

composer require netresearch/nr-passkeys-fe
Copied!

This also installs netresearch/nr-passkeys-be as a dependency.

Activate the extension 

After installation, activate the extension in the TYPO3 backend:

  1. Go to Admin Tools > Extensions
  2. Search for "Passkeys Frontend Authentication"
  3. Click the activate button

Or use the CLI:

vendor/bin/typo3 extension:activate nr_passkeys_fe
Copied!

Database schema update 

The extension adds two tables and extends two core tables:

  • tx_nrpasskeysfe_credential -- Frontend passkey credentials
  • tx_nrpasskeysfe_recovery_code -- Bcrypt-hashed recovery codes
  • fe_users -- Adds passkey_grace_period_start and passkey_nudge_until columns for enforcement tracking
  • fe_groups -- Adds passkey_enforcement and passkey_grace_period_days columns for per-group enforcement

After activation, run the database schema update:

  1. Go to Admin Tools > Maintenance > Analyze Database Structure
  2. Apply the suggested changes

Or use the CLI:

vendor/bin/typo3 database:updateschema
Copied!

Include TypoScript 

Include the extension's TypoScript in your site configuration:

  1. Go to Site Management > TypoScript
  2. Edit your root TypoScript record
  3. Add the static template Passkeys Frontend Authentication (nr_passkeys_fe)

Or add it manually:

@import 'EXT:nr_passkeys_fe/Configuration/TypoScript/setup.typoscript'
@import 'EXT:nr_passkeys_fe/Configuration/TypoScript/constants.typoscript'
Copied!

Add the plugins 

Three frontend plugins are available. Add them to your pages as content elements:

NrPasskeysFe:Login
The passkey login form. Place on your login page. Supports both discoverable (usernameless) and username-first login.
NrPasskeysFe:Management
Self-service credential management. Place on a page accessible only to logged-in users.
NrPasskeysFe:Enrollment
Enrollment form used as the interstitial target. Required when enforcement is active.

See Quick Start for a step-by-step walkthrough.

Verify the installation 

After activation:

  1. Visit the login page with the NrPasskeysFe:Login plugin. You should see a Sign in with a passkey button.
  2. The backend module Admin Tools > Passkey Management FE should appear.

Configuration 

Configuration happens at three levels:

  1. Extension settings -- Global defaults (algorithm, challenge TTL, rate limiting)
  2. Site configuration -- Per-site RP ID and origin
  3. TypoScript -- Plugin view settings and page UIDs

Extension Settings 

Global extension settings are managed in Admin Tools > Settings > Extension Configuration > nr_passkeys_fe.

Challenge settings 

challengeTtlSeconds

challengeTtlSeconds
type

int

Default

120

Time-to-live for challenge tokens in seconds. After expiry the user must request a new challenge. 120 seconds is sufficient for most authenticators.

Rate limiting 

rateLimitMaxAttempts

rateLimitMaxAttempts
type

int

Default

10

Maximum requests allowed per IP per endpoint within the rate limit window. Exceeding this limit returns HTTP 429.

rateLimitWindowSeconds

rateLimitWindowSeconds
type

int

Default

300

Duration of the rate limiting window in seconds. The counter resets after this period.

Account lockout 

lockoutThreshold

lockoutThreshold
type

int

Default

5

Number of consecutive failed authentication attempts before the account is temporarily locked. Applies per username/IP.

lockoutDurationSeconds

lockoutDurationSeconds
type

int

Default

900

Duration of the account lockout in seconds (default: 15 minutes). Administrators can unlock accounts manually from the backend module.

Cryptographic algorithms 

allowedAlgorithms

allowedAlgorithms
type

string

Default

ES256

Comma-separated list of allowed signing algorithms. Supported values:

  • ES256 -- ECDSA with SHA-256 (recommended)
  • ES384 -- ECDSA with SHA-384
  • ES512 -- ECDSA with SHA-512
  • RS256 -- RSA with SHA-256

Example for multiple algorithms: ES256,RS256

User verification 

userVerification

userVerification
type

string

Default

required

The user verification requirement for WebAuthn ceremonies:

  • required -- Authenticator must verify the user (biometric or PIN). Most secure option.
  • preferred -- Verify if possible; proceed without if not.
  • discouraged -- Skip user verification for fastest flow.

Invalid values fall back to required.

Site Configuration 

Each TYPO3 site can have an independent relying party configuration. This is essential for multi-site installations where different domains need separate WebAuthn origins.

Settings are added to the site's config.yaml file or via the Sites module in the TYPO3 backend.

config/sites/my-site/config.yaml
settings:
  nr_passkeys_fe:
    rpId: 'example.com'
    origin: 'https://example.com'
    enforcementLevel: 'off'
    enrollmentPageUrl: '/passkey-setup'
Copied!

nr_passkeys_fe.rpId

nr_passkeys_fe.rpId
type

string

Default

(auto-detected from HTTP_HOST)

The WebAuthn Relying Party identifier. Must match the domain of the site. Use just the domain name, not the full URL.

nr_passkeys_fe.origin

nr_passkeys_fe.origin
type

string

Default

(auto-detected from request)

The expected WebAuthn origin (e.g. https://example.com). Must include the scheme and port if non-standard. Leave empty for auto-detection.

nr_passkeys_fe.enforcementLevel

nr_passkeys_fe.enforcementLevel
type

string

Default

off

The site-level enforcement level. Valid values:

  • off -- Passkeys are optional; no prompts or interstitials.
  • encourage -- Users without passkeys see a dismissible banner.
  • required -- Users without passkeys see an enrollment interstitial after login. Skippable during the grace period.
  • enforced -- Users without passkeys cannot bypass the interstitial. Grace period skipping is disabled.

Per-group enforcement can override this for specific user groups (strictest level wins). See Enforcement.

nr_passkeys_fe.enrollmentPageUrl

nr_passkeys_fe.enrollmentPageUrl
type

string

Default

(empty)

URL path to the passkey enrollment page (e.g. /passkey-setup). Used by the enrollment banner to link users to the enrollment flow.

Multi-site example 

For a multi-site installation with different enforcement levels:

config/sites/main-site/config.yaml (strict)
settings:
  nr_passkeys_fe:
    rpId: 'company.example'
    origin: 'https://company.example'
    enforcementLevel: 'enforced'
    enrollmentPageUrl: '/passkey-setup'
Copied!
config/sites/public-site/config.yaml (soft rollout)
settings:
  nr_passkeys_fe:
    rpId: 'www.example.com'
    origin: 'https://www.example.com'
    enforcementLevel: 'encourage'
    enrollmentPageUrl: '/passkey-setup'
Copied!

See Multi-Site for details on cross-domain passkey handling.

TypoScript Reference 

The extension provides TypoScript constants and setup for configuring plugin paths and page UIDs for redirects.

Constants 

Available TypoScript constants
plugin.tx_nrpasskeysfe.settings.loginPageUid = 0
plugin.tx_nrpasskeysfe.settings.managementPageUid = 0
plugin.tx_nrpasskeysfe.settings.enrollmentPageUid = 0
Copied!

plugin.tx_nrpasskeysfe.settings.loginPageUid

plugin.tx_nrpasskeysfe.settings.loginPageUid
type

int

Default

0

Page UID of the page containing the NrPasskeysFe:Login plugin. Used for redirect after logout and for the enrollment interstitial "back to login" link.

plugin.tx_nrpasskeysfe.settings.managementPageUid

plugin.tx_nrpasskeysfe.settings.managementPageUid
type

int

Default

0

Page UID of the page containing the NrPasskeysFe:Management plugin. Used for redirect after successful enrollment.

plugin.tx_nrpasskeysfe.settings.enrollmentPageUid

plugin.tx_nrpasskeysfe.settings.enrollmentPageUid
type

int

Default

0

Page UID of the page containing the NrPasskeysFe:Enrollment plugin. Required when enforcement is active. After login, users without a passkey are redirected here.

Setup 

The setup configures view paths for the Fluid templates:

EXT:nr_passkeys_fe/Configuration/TypoScript/setup.typoscript
plugin.tx_nrpasskeysfe {
    view {
        templateRootPaths.0 = EXT:nr_passkeys_fe/Resources/Private/Templates/
        partialRootPaths.0 = EXT:nr_passkeys_fe/Resources/Private/Partials/
        layoutRootPaths.0 = EXT:nr_passkeys_fe/Resources/Private/Layouts/
    }
    settings {
        loginPage = {$plugin.tx_nrpasskeysfe.settings.loginPageUid}
        managementPage = {$plugin.tx_nrpasskeysfe.settings.managementPageUid}
        enrollmentPage = {$plugin.tx_nrpasskeysfe.settings.enrollmentPageUid}
        css.includeDefault = 1
    }
}
Copied!

Overriding templates 

To override a template, add a custom path at a higher index:

plugin.tx_nrpasskeysfe {
    view {
        templateRootPaths.10 = EXT:my_site/Resources/Private/Templates/NrPasskeysFe/
    }
}
Copied!

Then create the template in the same directory structure, e.g.: EXT:my_site/Resources/Private/Templates/NrPasskeysFe/Login/Index.html

Disabling default CSS 

To include your own styles instead of the extension's default CSS:

plugin.tx_nrpasskeysfe.settings.css.includeDefault = 0
Copied!

Quick Start 

This guide gets you from installation to your first passkey login in five minutes.

DDEV demo (fastest option) 

If you have DDEV installed, get a fully working demo in one command:

make up
Copied!

This starts DDEV, installs TYPO3 v13 and v14, creates demo pages with all plugins preconfigured, and renders documentation. Visit https://nr-passkeys-fe.ddev.site/ when complete.

Manual setup 

Prerequisites 

  • TYPO3 13.4 LTS or 14.1+ with HTTPS
  • Composer-based installation

Step 1: Install 

composer require netresearch/nr-passkeys-fe
vendor/bin/typo3 extension:activate nr_passkeys_be
vendor/bin/typo3 extension:activate nr_passkeys_fe
vendor/bin/typo3 database:updateschema
Copied!

Step 2: Include TypoScript 

In your site's root TypoScript record, add:

@import 'EXT:nr_passkeys_fe/Configuration/TypoScript/setup.typoscript'
@import 'EXT:nr_passkeys_fe/Configuration/TypoScript/constants.typoscript'
Copied!

Then set the page UIDs for your login and management pages:

plugin.tx_nrpasskeysfe.settings.loginPageUid = 42
plugin.tx_nrpasskeysfe.settings.managementPageUid = 43
plugin.tx_nrpasskeysfe.settings.enrollmentPageUid = 44
Copied!

Step 3: Add plugins to pages 

Create three pages in your TYPO3 page tree:

  1. Login page (e.g. UID 42): Add the content element Plugin > Passkeys Frontend Authentication > Login. This is your passkey login page.
  2. Management page (e.g. UID 43): Add Plugin > Passkeys Frontend Authentication > Management. Restrict access to logged-in frontend users only.
  3. Enrollment page (e.g. UID 44): Add Plugin > Passkeys Frontend Authentication > Enrollment. Used for the post-login interstitial. Can be the same as the management page.

Step 4: Configure the site 

Add the following to your site's config.yaml:

settings:
  nr_passkeys_fe:
    rpId: 'your-domain.example'
    origin: 'https://your-domain.example'
    enforcementLevel: 'encourage'
    enrollmentPageUrl: '/passkey-setup'
Copied!

Replace your-domain.example with your actual domain.

Step 5: Log in with a passkey 

  1. Visit your login page (e.g. /login).
  2. Click Sign in with a passkey.
  3. If you have no passkey yet, you will be prompted to create one.
  4. Follow the browser's passkey creation dialog (TouchID, Windows Hello, security key, etc.).
  5. After enrollment, click Sign in with a passkey again. The browser will present your passkey. Authenticate with the biometric prompt.

Next steps 

Login 

The NrPasskeysFe:Login plugin provides two login flows depending on site configuration.

Discoverable login (Variant A) 

When the browser supports WebAuthn Conditional UI and the site has no pre-filled username, the login page shows a passkey autofill option in the username input field. The browser presents available passkeys matching the site's RP ID without requiring a username.

This is the recommended flow for passkey-only sites.

  1. Open the login page.
  2. The browser automatically suggests a passkey in the username field (autofill dropdown).
  3. Select the passkey from the dropdown.
  4. Authenticate with the biometric prompt (TouchID, Windows Hello, etc.).
  5. You are logged in.

Username-first login (Variant B) 

If the browser does not support Conditional UI, or the user prefers to type their username first:

  1. Enter your username in the login form.
  2. Click Sign in with a passkey.
  3. The browser prompts for your passkey.
  4. Authenticate with the biometric prompt.
  5. You are logged in.

Password fallback 

When no passkey is available and the enforcement level allows it, users can still log in with a password via the standard felogin plugin or the password fallback link on the passkey login form.

When enforcement level is required or enforced, users without a passkey are redirected to the enrollment page after password login.

Error states 

The login form displays user-friendly error messages for:

  • No passkey found -- Passkey not registered for this site or device.
  • Authentication cancelled -- User dismissed the browser prompt.
  • Challenge expired -- The challenge timed out (120 seconds by default). Try again.
  • Account locked -- Too many failed attempts. Wait for the lockout to expire or contact an administrator.

felogin integration 

If you use the standard felogin plugin, the extension can inject a passkey button below the password login form. To enable this, ensure nr_passkeys_fe is active and the felogin plugin is on the same page.

The passkey button is added via a PSR-14 event listener on the felogin rendering event. The button opens the same WebAuthn flow as the standalone login plugin.

Enrollment 

Passkey enrollment is the process of creating a new passkey credential on a device and registering the public key with the TYPO3 site.

Prerequisites 

  • The user must be logged in to a frontend session.
  • The browser must support WebAuthn (all modern browsers do).
  • HTTPS is required (or localhost).

Enrolling a passkey 

Users enroll passkeys from the NrPasskeysFe:Management plugin or the dedicated enrollment page:

  1. Navigate to the management page (or the enrollment interstitial).
  2. Click Register a new passkey.
  3. The browser opens the passkey creation dialog.
  4. Choose an authenticator (TouchID, Windows Hello, YubiKey, etc.).
  5. Optionally enter a name for the passkey (e.g. "MacBook Pro").
  6. Confirm with the biometric prompt.
  7. The passkey is registered and appears in the credential list.

Post-login enrollment interstitial 

When the site enforcement level is required or enforced, users who log in without a passkey are redirected to the enrollment page before accessing the site. This interstitial:

  • Explains why a passkey is required.
  • Provides the enrollment form.
  • Shows remaining grace period days (for required level).
  • When the grace period expires or level is enforced, skipping is disabled.

See Enforcement for details on configuring enforcement levels.

Naming passkeys 

During enrollment, users can give each passkey a name. This name appears in the management panel to help users identify which authenticator each passkey belongs to (e.g. "iPhone 16", "YubiKey 5C NFC").

Names can be renamed later in the management panel.

Recovery 

If a user loses access to all their passkey devices, they can regain access using recovery codes.

Recovery codes 

Recovery codes are one-time-use alphanumeric codes generated by the user. Each set contains 10 codes. The codes are stored in the TYPO3 database as bcrypt hashes -- the plaintext is shown only once at generation time.

Generating recovery codes 

  1. Navigate to the passkey management page.
  2. Click Generate recovery codes.
  3. The codes are shown once. Save them in a secure location.
  4. Click I have saved my recovery codes to confirm.

Using a recovery code 

  1. Visit the login page.
  2. Click Use a recovery code (on the passkey login form or felogin integration).
  3. Enter your username and one recovery code.
  4. You are logged in.
  5. The used code is marked as consumed and cannot be used again.

After using a recovery code, enroll a new passkey immediately to restore full passkey-first authentication.

Code lifecycle 

  • Each code can be used exactly once.
  • Generating a new set invalidates all previous codes.
  • Codes do not expire (but are invalidated on new set generation).
  • Used codes are stored as consumed (not deleted) for audit purposes.

See ADR-003 for the design decision on the triple recovery mechanism.

Management 

The NrPasskeysFe:Management plugin allows logged-in frontend users to manage their own passkeys and recovery codes.

Accessing the management panel 

Place the NrPasskeysFe:Management plugin on a page restricted to authenticated frontend users (e.g. a "My Account" page).

Available actions 

List passkeys
The panel shows all registered passkeys with their name, registration date, last used date, and the authenticator type.
Enroll a new passkey
Click Register a new passkey to add another device. See Enrollment.
Rename a passkey
Click the edit icon next to a passkey to rename it.
Remove a passkey

Click the delete icon next to a passkey to revoke it.

Generate recovery codes
Generates a new set of 10 one-time recovery codes. See Recovery.

Passkey list fields 

Field Description
Name User-defined label (e.g. "iPhone 16")
Registered Date the passkey was enrolled
Last used Date of the most recent successful authentication
Authenticator AAGUID-based device type, if known

Dashboard 

The Passkey Management FE backend module is accessible at Admin Tools > Passkey Management FE.

The dashboard provides:

Adoption statistics 

Overview of passkey adoption across all frontend users:

  • Total users -- All active fe_users records
  • Users with passkeys -- Users with at least one enrolled passkey
  • Adoption rate -- Percentage with a passkey
  • Users in grace period -- Users enrolled but within their grace period (enforcement only)

Per-group breakdown 

For each frontend user group with enforcement configured:

  • Group name and enforcement level
  • Number of users in the group with / without passkeys
  • Users in grace period

Recent activity 

Recent passkey events for audit and monitoring:

  • Passkey enrollments (user, device, date)
  • Passkey removals
  • Successful and failed authentication attempts
  • Recovery code usage

The dashboard auto-refreshes every 30 seconds.

Site selector 

When multiple TYPO3 sites are configured, a site selector allows switching between site-specific statistics.

Enforcement 

The enforcement system controls how strongly passkeys are required for frontend users. Enforcement can be set at the site level and overridden per frontend user group.

Enforcement levels 

Four levels are available:

Off
Passkeys are completely optional. No prompts, banners, or interstitials. Users can log in with a password as normal.
Encourage
Users without a passkey see a dismissible banner after login suggesting they enroll one. No access is blocked. The banner can be dismissed and will not re-appear for the session.
Required
Users without a passkey see an enrollment interstitial after login. They can skip the interstitial during the grace period (configurable, default 14 days). After the grace period expires, they must enroll to continue.
Enforced
Users without a passkey cannot bypass the enrollment interstitial. Grace period skipping is disabled. This level is suitable for high-security sites.

Enforcement resolution 

The effective enforcement level for a user is determined by:

  1. Site configuration -- The site-level enforcementLevel setting (see Site Configuration).
  2. User group overrides -- Each frontend user group can have an enforcement level. The strictest level across all groups the user belongs to wins.
  3. Grace period -- The shortest grace period across applicable groups wins.

The EnforcementLevelResolvedEvent PSR-14 event allows listeners to further override the resolved level (see PSR-14 Events).

Configuring per-group enforcement 

In the TYPO3 backend:

  1. Go to Web > List and open the fe_groups record.
  2. The TCA record shows a Passkey Enforcement section.
  3. Set the enforcement level and grace period for the group.

Or use the backend module:

  1. Go to Admin Tools > Passkey Management FE.
  2. In the Enforcement tab, select a site.
  3. Adjust enforcement levels per group.

Interstitial behaviour 

When a user triggers the enrollment interstitial (level required or enforced):

  • The full-page interstitial is shown.
  • It explains why a passkey is required.
  • It shows the enrollment form (links to the enrollment page).
  • For required level: a Skip for now button is shown with the remaining grace period.
  • For enforced level: no skip button.
  • API endpoints, AJAX requests, and the login/logout pages are exempted from the interstitial.

Grace period tracking 

Grace periods are tracked per user in the fe_users table via the passkey_grace_period_start column (unix timestamp of the first login without a passkey after enforcement was enabled). The enforcement middleware computes the expiry from passkey_grace_period_start + gracePeriodDays.

Per-group grace period days are stored in fe_groups.passkey_grace_period_days. The shortest grace period across all applicable groups wins.

User Management 

Administrators can manage frontend user passkeys from the backend module or via the fe_users record in the list module.

Viewing credentials in fe_users 

When editing a fe_users record in Web > List, a read-only info element shows:

  • Number of registered passkeys
  • Last used date
  • Enforcement level applicable to the user

This uses a custom TCA element (passkey_fe_info) registered by the extension.

Backend module actions 

From Admin Tools > Passkey Management FE:

List credentials
View all passkeys registered by a specific user.
Revoke a credential
Immediately invalidate a specific passkey. The user must re-enroll from that device.
Revoke all credentials
Remove all passkeys for a user. Use when a user's device is lost or stolen.
Reset grace period
Reset the grace period start date to give a user more time to enroll (for required enforcement only).
Unlock account
If the user's account is locked due to too many failed attempts, unlock it immediately.

Invalidating passkeys via database 

In an emergency, you can revoke all passkeys for a user directly:

-- View credentials
SELECT * FROM tx_nrpasskeysfe_credential
WHERE fe_user = <uid>;

-- Revoke all credentials for a user
UPDATE tx_nrpasskeysfe_credential
SET deleted = 1
WHERE fe_user = <uid>;

-- Or hard-delete
DELETE FROM tx_nrpasskeysfe_credential
WHERE fe_user = <uid>;
Copied!

Developer Guide 

This chapter is for developers who want to understand, debug, or extend the extension.

Architecture overview 

The extension is layered as follows:

Classes/
  Authentication/     PSR-7-based auth service (TYPO3 auth chain)
  Configuration/      Configuration value objects
  Controller/         eID dispatcher + Extbase-less controllers
  Domain/
    Dto/              Request/response DTOs
    Enum/             EnforcementLevel enum (re-exported)
    Model/            FrontendCredential, RecoveryCode
  Event/              PSR-14 event classes (7 events)
  EventListener/      PSR-14 listeners (felogin integration, banner)
  Form/Element/       PasskeyFeInfoElement (TCA read-only display)
  Middleware/         PasskeyPublicRouteResolver + Interstitial
  Service/            Business logic (8 services)
Copied!

Key services:

  • FrontendWebAuthnService -- WebAuthn ceremony orchestration
  • SiteConfigurationService -- Per-site RP ID and origin resolution
  • FrontendCredentialRepository -- Credential CRUD operations
  • FrontendUserLookupService -- fe_users lookup by username/UID (separated from credential repository for single-responsibility)
  • FrontendEnforcementService -- Enforcement level resolution
  • RecoveryCodeService -- Recovery code generation and verification
  • PasskeyEnrollmentService -- Enrollment ceremony coordination
  • FrontendAdoptionStatsService -- Adoption statistics for admin module

All services are wired via Symfony DI (Configuration/Services.yaml). The auth service and eID dispatcher use GeneralUtility::makeInstance for compatibility with the TYPO3 auth chain.

Token-based login flow 

The extension uses a two-phase login flow:

  1. eID verification: The JavaScript calls the eID endpoint (loginVerify or recoveryVerify). The eID controller verifies the WebAuthn assertion (or recovery code) and stores the authenticated fe_user UID in a short-lived cache token (nr_passkeys_fe_nonce cache, 2-minute TTL).
  2. felogin form submission: The JavaScript submits a standard logintype=login form to the current page, passing the cache token in a hidden passkeyLoginToken field. TYPO3's normal authentication chain picks this up.
  3. Auth service resolution: PasskeyFrontendAuthenticationService (priority 80) reads the passkeyLoginToken from $loginData, looks up the UID in the cache, and returns the user row. No site context or WebAuthn libraries are needed at this stage.

This approach ensures the user gets a proper TYPO3 frontend session with all middleware (enforcement interstitial, session regeneration) applied, rather than a bare eID-only response.

PSR-14 Events 

The extension dispatches seven PSR-14 events that third-party extensions can listen to. All events are in the Netresearch\NrPasskeysFe\Event namespace.

Register a listener in Configuration/Services.yaml:

MyVendor\MyExt\EventListener\MyListener:
  tags:
    - name: event.listener
      identifier: 'my-ext/passkey-auth'
      event: Netresearch\NrPasskeysFe\Event\AfterPasskeyAuthenticationEvent
Copied!

BeforePasskeyAuthenticationEvent 

Dispatched before a passkey assertion is verified. The feUserUid is null for discoverable-credential (usernameless) logins.

final readonly class BeforePasskeyAuthenticationEvent
{
    public function __construct(
        public readonly ?int $feUserUid,
        public readonly string $assertionJson,
    ) {}
}
Copied!

Use cases: audit logging, custom rate limiting, anomaly detection.

AfterPasskeyAuthenticationEvent 

Dispatched after a successful passkey authentication.

final readonly class AfterPasskeyAuthenticationEvent
{
    public function __construct(
        public readonly int $feUserUid,
        public readonly FrontendCredential $credential,
    ) {}
}
Copied!

Use cases: security dashboards, post-login workflows, notifications.

BeforePasskeyEnrollmentEvent 

Dispatched before a passkey enrollment ceremony begins. Throw an exception in the listener to abort enrollment.

final readonly class BeforePasskeyEnrollmentEvent
{
    public function __construct(
        public readonly int $feUserUid,
        public readonly string $siteIdentifier,
        public readonly string $attestationJson,
    ) {}
}
Copied!

Use cases: enrollment rate limiting, allowed-device policies.

AfterPasskeyEnrollmentEvent 

Dispatched after a passkey is successfully enrolled.

final readonly class AfterPasskeyEnrollmentEvent
{
    public function __construct(
        public readonly int $feUserUid,
        public readonly FrontendCredential $credential,
        public readonly string $siteIdentifier,
    ) {}
}
Copied!

Use cases: confirmation emails, audit logs, enforcement re-evaluation.

PasskeyRemovedEvent 

Dispatched after a passkey credential is revoked (by the user or an admin). revokedBy is the UID of the actor (user UID for self-service, admin UID for admin-initiated removal).

final readonly class PasskeyRemovedEvent
{
    public function __construct(
        public readonly FrontendCredential $credential,
        public readonly int $revokedBy,
    ) {}
}
Copied!

Use cases: security alerts, audit logging.

RecoveryCodesGeneratedEvent 

Dispatched when a new set of recovery codes is generated. The actual code values are never included for security reasons.

final readonly class RecoveryCodesGeneratedEvent
{
    public function __construct(
        public readonly int $feUserUid,
        public readonly int $codeCount,
    ) {}
}
Copied!

Use cases: email notification, audit logging.

EnforcementLevelResolvedEvent 

Mutable event. Dispatched when the effective enforcement level has been computed for a user. Listeners can call setEffectiveLevel() to override the resolved level.

final class EnforcementLevelResolvedEvent
{
    public function __construct(
        public readonly int $feUserUid,
        private string $effectiveLevel,
    ) {}

    public function getEffectiveLevel(): string { ... }
    public function setEffectiveLevel(string $level): void { ... }
}
Copied!

The level is a string (off, encourage, required, enforced) to avoid a hard compile-time dependency on the EnforcementLevel enum from nr-passkeys-be.

Use cases: custom enforcement overrides (e.g. exempting staff users, IP-based enforcement).

Extension Points 

Beyond PSR-14 events, the extension provides several extension points.

Overriding Fluid templates 

All frontend output is rendered via Fluid templates located in EXT:nr_passkeys_fe/Resources/Private/. Override any template by registering a higher-priority path:

plugin.tx_nrpasskeysfe {
    view {
        templateRootPaths.10 = EXT:my_ext/Resources/Private/Templates/NrPasskeysFe/
        partialRootPaths.10 = EXT:my_ext/Resources/Private/Partials/NrPasskeysFe/
        layoutRootPaths.10 = EXT:my_ext/Resources/Private/Layouts/NrPasskeysFe/
    }
}
Copied!

Available templates:

  • Login/Index.html -- Passkey login form
  • Login/Recovery.html -- Recovery code login form
  • Enrollment/Index.html -- Passkey enrollment form
  • Enrollment/Success.html -- Post-enrollment success page
  • Management/Index.html -- Self-service management panel
  • Management/RecoveryCodes.html -- Recovery code generation
  • AdminModule/Index.html -- Backend admin module shell

Replacing services 

All services are registered in Configuration/Services.yaml with standard Symfony DI. To replace a service, add an alias in your extension's Services.yaml:

# Override the enforcement service
Netresearch\NrPasskeysFe\Service\FrontendEnforcementService:
  class: MyVendor\MyExt\Service\CustomEnforcementService
Copied!

Custom enforcement via event 

The recommended approach for custom enforcement logic is to listen to EnforcementLevelResolvedEvent and call setEffectiveLevel():

use Netresearch\NrPasskeysFe\Event\EnforcementLevelResolvedEvent;

final class CustomEnforcementListener
{
    public function __invoke(EnforcementLevelResolvedEvent $event): void
    {
        // Exempt staff members from enforcement
        if ($this->isStaffMember($event->feUserUid)) {
            $event->setEffectiveLevel('off');
        }
    }
}
Copied!

Adding custom eID endpoints 

The extension's eID dispatcher (EidDispatcher) is registered at eID=nr_passkeys_fe. It routes requests to controllers based on the action POST parameter.

To add custom eID actions without modifying the extension, listen to BeforePasskeyAuthenticationEvent for pre-auth hooks, or register your own eID handler in ext_localconf.php:

$GLOBALS['TYPO3_CONF_VARS']['FE']['eID_include']['my_passkey_ext']
    = \MyVendor\MyExt\Eid\MyEidHandler::class . '::handle';
Copied!

JavaScript module overrides 

The extension's JavaScript modules are loaded as TYPO3 ES modules (@netresearch/nr-passkeys-fe/*). To customize behavior, extend the module via @typo3/core/event/regular-event.js hooks or override the import map in Configuration/JavaScriptModules.php.

PasskeyUtils.js shared module 

All frontend JavaScript modules share a common utility module, PasskeyUtils.js, exposed as window.NrPasskeysFe. It provides:

  • base64url encoding/decoding -- base64urlToBuffer() and bufferToBase64url() for converting between WebAuthn binary formats and JSON-safe strings.
  • DOM helpers -- showError(), showStatus(), showLoading() for consistent UI state management across the login, enrollment, management, and recovery modules.
  • URL validation -- Shared URL handling utilities.

PasskeyUtils.js is loaded before the module-specific scripts via f:asset.script in Fluid templates. If you override templates, ensure PasskeyUtils.js is still included before other passkey scripts.

The eight JavaScript modules are:

  • PasskeyLogin.js -- Login form and WebAuthn assertion flow
  • PasskeyEnrollment.js -- Enrollment ceremony
  • PasskeyManagement.js -- Credential list, rename, remove
  • PasskeyRecovery.js -- Recovery code login form
  • PasskeyRecoveryCodes.js -- Recovery code generation UI
  • PasskeyBanner.js -- Encourage-level dismissible banner
  • PasskeyFeAdmin.js -- Backend admin module
  • PasskeyUtils.js -- Shared utilities (described above)

eID API Reference 

All frontend API endpoints are handled by the eID dispatcher at /?eID=nr_passkeys_fe. The action query parameter selects the controller action. Request bodies are JSON. All responses are JSON with Content-Type: application/json.

Token-based login flow 

Both passkey login and recovery code login use a two-phase flow:

  1. eID verification -- JavaScript calls the eID endpoint (loginVerify or recoveryVerify). The server verifies the assertion or recovery code and stores the authenticated fe_user UID in a short-lived cache entry (nr_passkeys_fe_nonce cache, 2-minute TTL). The response includes a loginToken.
  2. felogin form submission -- JavaScript submits a standard logintype=login form to the current page, including the loginToken in a hidden field. TYPO3's normal FE authentication chain processes the request.
  3. Auth service -- PasskeyFrontendAuthenticationService (priority 80) reads the loginToken, looks up the user UID in the cache, and returns the fe_users row. The token is consumed (one-time use).

This ensures users get a proper TYPO3 frontend session with all middleware (enforcement interstitial, session regeneration) applied.

Authentication endpoints (public) 

These endpoints do not require a frontend session.

POST /?eID=nr_passkeys_fe&action=loginOptions
Copied!

Request challenge options for passkey login.

Request:

{
    "username": "johndoe"
}
Copied!

For discoverable login, omit username or send an empty body.

Response (200):

{
    "options": {
        "challenge": "...",
        "rpId": "example.com",
        "allowCredentials": []
    },
    "challengeToken": "..."
}
Copied!

POST /?eID=nr_passkeys_fe&action=loginVerify
Copied!

Verify a passkey assertion and issue a login token.

Request:

{
    "assertion": {
        "id": "...",
        "type": "public-key",
        "response": {
            "clientDataJSON": "...",
            "authenticatorData": "...",
            "signature": "...",
            "userHandle": "..."
        }
    },
    "challengeToken": "..."
}
Copied!

Response (200):

{
    "status": "ok",
    "feUserUid": 42,
    "loginToken": "abc123..."
}
Copied!

The loginToken is a one-time token valid for 2 minutes. The JavaScript must submit it via a standard felogin form to complete the login (see Token-based login flow above).

Recovery code endpoint (public) 

POST /?eID=nr_passkeys_fe&action=recoveryVerify
Copied!

Login using a one-time recovery code.

Request:

{
    "username": "johndoe",
    "code": "XXXX-XXXX"
}
Copied!

Response (200):

{
    "status": "ok",
    "feUserUid": 42,
    "loginToken": "abc123..."
}
Copied!

The loginToken is consumed via felogin form submission, identical to the passkey login flow.

Enrollment endpoints (requires session) 

POST /?eID=nr_passkeys_fe&action=registrationOptions
Copied!

Request challenge options for passkey enrollment. Requires an active frontend session.

Response (200):

{
    "options": {
        "challenge": "...",
        "rp": {"id": "example.com", "name": "My Site"},
        "user": {"id": "...", "name": "johndoe", "displayName": "John Doe"},
        "pubKeyCredParams": [{"type": "public-key", "alg": -7}]
    },
    "challengeToken": "..."
}
Copied!

POST /?eID=nr_passkeys_fe&action=registrationVerify
Copied!

Verify an attestation and save the new credential.

Request:

{
    "attestation": {
        "id": "...",
        "type": "public-key",
        "response": {
            "clientDataJSON": "...",
            "attestationObject": "..."
        }
    },
    "challengeToken": "...",
    "name": "My MacBook"
}
Copied!

Response (200):

{
    "status": "ok",
    "credentialId": "..."
}
Copied!

Management endpoints (requires session) 

GET /?eID=nr_passkeys_fe&action=manageList
Copied!

Returns the list of passkeys for the current user.

POST /?eID=nr_passkeys_fe&action=manageRename
Copied!

Request: {"credentialId": "...", "name": "New Name"}

POST /?eID=nr_passkeys_fe&action=manageRemove
Copied!

Request: {"credentialId": "..."}

POST /?eID=nr_passkeys_fe&action=recoveryGenerate
Copied!

Generates a new set of 10 recovery codes. Returns the plaintext codes (shown once only).

Response (200):

{
    "status": "ok",
    "codes": ["XXXX-XXXX", "..."],
    "count": 10
}
Copied!

Enrollment status endpoints (requires session) 

GET /?eID=nr_passkeys_fe&action=enrollmentStatus
Copied!

Returns the current user's enrollment and enforcement status.

POST /?eID=nr_passkeys_fe&action=enrollmentSkip
Copied!

Skips the enrollment interstitial (only available during grace period when enforcement level is required).

Action routing summary 

The eID dispatcher routes the action query parameter to controllers:

Action Controller method Auth
loginOptions LoginController::optionsAction Public
loginVerify LoginController::verifyAction Public
recoveryVerify RecoveryController::verifyAction Public
recoveryGenerate RecoveryController::generateAction Session
registrationOptions ManagementController::regOptions Session
registrationVerify ManagementController::regVerify Session
manageList ManagementController::listAction Session
manageRename ManagementController::renameAction Session
manageRemove ManagementController::removeAction Session
enrollmentStatus EnrollmentController::statusAction Session
enrollmentSkip EnrollmentController::skipAction Session

Error responses 

All error responses follow this format:

{
    "error": "Human-readable error message"
}
Copied!

Common HTTP status codes:

Code Meaning
400 Invalid request (missing/malformed fields)
401 Not authenticated (session required)
403 Forbidden (insufficient privileges)
429 Rate limit exceeded
500 Internal server error

WebAuthn Compliance 

The extension implements the W3C WebAuthn Level 2 specification and the FIDO2 Client-to-Authenticator Protocol (CTAP2).

Implemented ceremonies 

Registration (attestation)
The full attestation verification ceremony. Supported formats: none, packed, FIDO-U2F, Apple, TPM, and Android SafetyNet. The extension uses web-auth/webauthn-lib v5.x for all cryptographic verification.
Authentication (assertion)
Full assertion verification including signature counter checks for authenticator clone detection.

Relying Party configuration 

  • rpId: Domain-only (no scheme/port). Per-site configurable.
  • Origin: Full URL including scheme. Verified against registered origins during ceremonies.
  • User verification: Configurable (required / preferred / discouraged). Defaults to required.

Challenge handling 

Challenges are generated as 32-byte cryptographically random values (random_bytes(32)), then wrapped in an HMAC-SHA256 signed token:

challengeToken = base64url(nonce || HMAC-SHA256(encryptionKey, nonce || challenge || timestamp))
Copied!

The token includes:

  • A nonce (replay protection -- each token can be used once)
  • A timestamp (challenge TTL enforcement)
  • HMAC signature (tampering detection)

The plaintext challenge is sent to the browser; the HMAC-signed token is included in the eID response and must be returned verbatim during verification.

Credential storage 

Frontend credentials are stored in tx_nrpasskeysfe_credential:

  • credential_id -- WebAuthn credential ID (binary, indexed)
  • public_key_cose -- COSE-encoded public key (binary blob)
  • sign_count -- Usage counter for clone detection
  • user_handle -- SHA-256(uid || encryptionKey) -- not the UID itself
  • aaguid -- Authenticator AAGUID for device identification
  • transports -- JSON transport hints

The user_handle is a one-way hash to prevent user enumeration from credential lookups.

Recovery codes 

Recovery codes are stored as bcrypt hashes (cost factor 12). Plain text is generated server-side, displayed once to the user, then immediately discarded. The bcrypt hash is stored. On verification, the submitted code is compared against the hash in constant time.

See ADR-010 for the design decision.

Threat Model 

This page describes the primary threats and the mitigations implemented in the extension.

Phishing 

Threat: An attacker creates a fake login page to steal credentials.

Mitigation: WebAuthn binds the credential to the Relying Party ID (domain). A passkey registered for example.com cannot be used on evil.example.com. The browser enforces this binding; no user action is required.

Credential theft via database breach 

Threat: Attacker reads the credential database and impersonates users.

Mitigation: The database stores only public keys. The corresponding private key never leaves the authenticator device and cannot be extracted. A leaked public key is useless for authentication.

Recovery code theft 

Threat: Attacker reads stored recovery codes and uses them.

Mitigation: Recovery codes are stored as bcrypt hashes (cost 12). Brute-forcing the hash is computationally infeasible. Users should store plaintext codes offline in a password manager.

Challenge replay 

Threat: Attacker intercepts a challenge/assertion and replays it.

Mitigation: Each challenge token includes a nonce stored in the TYPO3 session. A nonce can be verified exactly once (`TYPO3 rate limiter` + session check). The challenge has a 120-second TTL.

Brute-force / DoS 

Threat: Attacker floods the authentication endpoints.

Mitigation:

  • Rate limiting per IP per endpoint (configurable, default 10 req/5 min)
  • Account lockout after configurable failed attempts (default 5)
  • HTTP 429 responses with Retry-After header

User enumeration 

Threat: Attacker discovers valid usernames by observing differing error responses.

Mitigation: The login/options endpoint returns the same response structure regardless of whether the username exists. The user_handle field in stored credentials is a SHA-256 hash of the UID + encryption key, not the username.

Session fixation 

Threat: Attacker fixes a session ID and then authenticates to take over the session.

Mitigation: TYPO3's built-in session management regenerates the session ID after authentication. The extension does not bypass this.

CSRF 

Threat: Attacker tricks an authenticated user into performing unwanted actions.

Mitigation: Management endpoints require a valid frontend session cookie. The eID endpoint validates the Content-Type: application/json header, which browsers do not set for cross-origin form submissions.

HTTPS requirement 

WebAuthn operations are only allowed on secure origins (HTTPS or localhost). Any attempt to use the passkey API over plain HTTP will be rejected by the browser before reaching the server.

Multi-Site 

TYPO3 multi-site installations require each site to have its own passkey configuration because WebAuthn credentials are bound to a specific domain (Relying Party ID).

How site-aware RP ID works 

When a passkey authentication or enrollment request arrives, the extension resolves the current TYPO3 site from the request and reads the nr_passkeys_fe.rpId value from the site's config.yaml.

If no per-site RP ID is configured, the extension falls back to the global extension setting (auto-detected from HTTP_HOST).

This means:

  • Credentials registered on site-a.example cannot be used on site-b.example.
  • Each site maintains its own independent credential store (filtered by site RP ID).
  • Users who access multiple sites must enroll a passkey separately on each site.

Configuration example 

For a TYPO3 installation with two separate domains:

config/sites/company-intranet/config.yaml
base: 'https://intranet.example.com/'
settings:
  nr_passkeys_fe:
    rpId: 'intranet.example.com'
    origin: 'https://intranet.example.com'
    enforcementLevel: 'enforced'
    enrollmentPageUrl: '/passkey-setup'
Copied!
config/sites/public-shop/config.yaml
base: 'https://shop.example.com/'
settings:
  nr_passkeys_fe:
    rpId: 'shop.example.com'
    origin: 'https://shop.example.com'
    enforcementLevel: 'encourage'
    enrollmentPageUrl: '/passkey-setup'
Copied!

Shared RP ID across subdomains 

WebAuthn allows the RP ID to be a registrable domain suffix. For example, credentials registered with rpId: 'example.com' can be used on both app.example.com and api.example.com.

To enable this, set all sites to the same parent domain:

# site-a/config.yaml
settings:
  nr_passkeys_fe:
    rpId: 'example.com'
    origin: 'https://app.example.com'

# site-b/config.yaml
settings:
  nr_passkeys_fe:
    rpId: 'example.com'
    origin: 'https://api.example.com'
Copied!

Database isolation 

Credentials are stored with a site_identifier column in tx_nrpasskeysfe_credential. The credential lookup in the auth service always filters by the current site's identifier. There is no cross-site credential leakage.

Migration from single-site to multi-site 

If you initially deployed without per-site RP ID configuration (using auto-detected RP ID) and later add multiple sites:

  1. The auto-detected RP ID was HTTP_HOST from the original request.
  2. Existing credentials have a site_identifier matching the original site.
  3. When adding a new site with a different domain, users must re-enroll (their existing credentials won't match the new RP ID).

Plan migrations carefully and communicate re-enrollment to users in advance.

Troubleshooting 

This page covers the most common issues encountered when setting up or using the extension.

"Not allowed" / SecurityError in browser 

Symptom: The browser throws NotAllowedError or similar when attempting passkey login or enrollment.

Causes and fixes:

  1. Not on HTTPS. WebAuthn requires HTTPS. Exception: localhost works over HTTP for development. Check your URL.
  2. Wrong RP ID. The configured rpId must match the current domain. If rpId: 'example.com' but the page is served from sub.example.com, registration will fail unless the RP ID is the registrable suffix.
  3. User cancelled the prompt. The user dismissed the authenticator dialog. Not a server error.
  4. Cross-origin iframe. WebAuthn cannot be invoked from a cross-origin iframe. Ensure the login plugin is on the same origin as the page.

Challenge expired 

Symptom: Login fails with "Challenge token expired or invalid."

Fix: The challenge TTL is 120 seconds by default. If users take longer to authenticate (e.g. slow hardware key), increase challengeTtlSeconds in the extension settings.

"Invalid origin" error in logs 

Symptom: Authentication fails with an origin mismatch in the TYPO3 logs.

Fix: The passkeys.origin in the site's config.yaml must exactly match the scheme + domain + port combination the browser sees. Include the port if non-standard (e.g. https://example.com:8443).

Account locked 

Symptom: User sees "Account locked" after multiple failed attempts.

Fix: Wait for the lockout duration to expire (default: 15 minutes), or unlock via the backend module: Admin Tools > Passkey Management FE > Users.

Login plugin shows no passkey button 

Symptom: The login page shows the standard felogin form but no passkey button.

Checklist:

  1. Is nr_passkeys_fe activated? Check Admin Tools > Extensions.
  2. Is the NrPasskeysFe:Login plugin (not felogin) added to the page?
  3. Is TypoScript included? Check Web > Template > TypoScript Object Browser.
  4. Are there JavaScript errors in the browser console?

Recovery codes not accepted 

Symptom: Recovery code login fails with "Invalid recovery code."

Causes:

  1. The code was already used. Each code is one-time only.
  2. The user generated a new set, invalidating all previous codes.
  3. The code was entered incorrectly (check for 0 vs O, 1 vs l).

Enrollment interstitial appears after every login 

Symptom: User is redirected to the enrollment page on every login even after enrolling.

Causes:

  1. The enrolled credential's site_identifier does not match the current site. This can happen if the RP ID was changed after enrollment.
  2. The TYPO3 SYS.encryptionKey was changed, invalidating the user_handle lookup.

Fix: Have the user revoke the old credential and re-enroll. If encryptionKey was changed, all credentials must be re-enrolled.

Debug logging 

Enable TYPO3 debug logging to see detailed authentication errors:

config/system/additional.php
$GLOBALS['TYPO3_CONF_VARS']['LOG']['Netresearch']['NrPasskeysFe'] = [
    'writerConfiguration' => [
        \TYPO3\CMS\Core\Log\LogLevel::DEBUG => [
            \TYPO3\CMS\Core\Log\Writer\FileWriter::class => [
                'logFileInfix' => 'nr_passkeys_fe',
            ],
        ],
    ],
];
Copied!

Log file: var/log/typo3_nr_passkeys_fe_*.log

Architecture Decision Records 

This section documents the key architectural decisions made during the design and implementation of the extension.

Summary 

ADR Title
ADR-001 Depend on nr-passkeys-be as Composer Dependency
ADR-002 felogin Integration and Standalone Plugin
ADR-003 Triple Recovery Mechanisms
ADR-004 Enrollment-Only, No Registration
ADR-005 Site-Configurable RP ID
ADR-006 Dual Enforcement Model
ADR-007 Post-Login Enrollment Interstitial
ADR-008 Credential-ID to UID Resolution
ADR-009 Vanilla JavaScript Frontend
ADR-010 Recovery Codes Bcrypt Hashed
ADR-011 Magic Link Deferred to v0.2
ADR-012 Authentication Service Priority 80

ADR-001: Depend on nr-passkeys-be as Composer Dependency 

Status

Accepted

Date

2026-03-14

Decision-makers

Sebastian Mendel

Context 

nr_passkeys_fe needs WebAuthn ceremony implementation (registration/assertion), HMAC-SHA256 challenge token generation with replay protection, and per-IP rate limiting with account lockout. These are security-critical primitives already proven in netresearch/nr-passkeys-be.

Three approaches were considered:

A. nr-passkeys-be as composer dependency — Reuse services directly B. Standalone extension — Copy patterns, own code C. Shared library extraction (nr-passkeys-core) — Extract common code into third package

Decision 

Option A: nr-passkeys-be as composer dependency.

The FE extension requires netresearch/nr-passkeys-be and reuses:

  • WebAuthnService — Registration/assertion ceremonies
  • ChallengeService — HMAC-SHA256 signed challenge tokens with nonce
  • RateLimiterService — Per-IP/endpoint rate limiting and lockout
  • ExtensionConfigurationService — RP ID, origin, algorithm configuration

The FE extension does NOT reuse BE-specific classes (controllers, middleware, TCA, credential repository, auth service).

Consequences 

Positive:

  • No duplication of security-critical code ( 700 lines of WebAuthn + challenge + rate limit)
  • Security fixes in BE propagate to FE automatically
  • Single web-auth/webauthn-lib version across both extensions
  • Faster initial development

Negative:

  • BE extension installed even on FE-only sites (unused backend module, BE TCA)
  • Version lock-step: FE releases may be blocked by BE version constraints
  • BE service API changes can break FE

Mitigation:

  • Plan for future extraction to nr-passkeys-core shared library (Option C)
  • Define interface contracts for shared services to ease future refactoring
  • Accept BE overhead as minimal (no runtime cost, only disk space)

Alternatives Considered 

Option B (Standalone): Would require duplicating  700 lines of security-critical code. Any bug fix would need to be applied twice. Rejected for maintenance burden.

Option C (Shared library): Architecturally cleanest, but requires refactoring nr-passkeys-be first. Deferred to a future release when both extensions are stable.

ADR-002: Both felogin Extension and Standalone Plugin 

Status

Accepted

Date

2026-03-14

Decision-makers

Sebastian Mendel

Context 

TYPO3 frontend login is typically handled by ext:felogin. The question is how nr_passkeys_fe integrates its passkey login UI with the existing login flow.

Three approaches were considered:

A. Extend felogin only — Inject via ModifyLoginFormViewEvent B. Own Extbase plugin only — Independent login plugin C. Both — felogin integration AND standalone plugin

Decision 

Option C: Both felogin extension and standalone plugin.

  • felogin integration: PSR-14 listener on ModifyLoginFormViewEvent injects a passkey login button into the existing felogin form.  50 lines of code.
  • Standalone plugin: PasskeyLoginPlugin (Extbase) that can be placed on any page independently of felogin.

Consequences 

Positive:

  • Sites using felogin get seamless integration with zero template changes
  • New sites can use the standalone plugin without felogin dependency
  • Mirrors nr-passkeys-be pattern (injects into BE login + provides own UI)
  • Minimal code overhead ( 50 lines for felogin hook)

Negative:

  • Two login UI paths to maintain and test
  • Potential confusion for integrators ("which one do I use?")

Mitigation:

  • Clear documentation: "Using felogin? It just works. Custom login? Use the plugin."
  • Shared Fluid partials between both paths to minimize template duplication

Alternatives Considered 

Option A (felogin only): Would exclude sites not using felogin. Some TYPO3 installations use custom login forms or third-party login extensions.

Option B (Standalone only): Would force felogin users to add a second plugin alongside felogin, creating an awkward dual-form UX.

ADR-004: Enrollment Only, No User Registration 

Status

Accepted

Date

2026-03-14

Decision-makers

Sebastian Mendel

Context 

TYPO3 core has no built-in FE user registration. Extensions like femanager and sf_register fill this gap. The question is whether nr_passkeys_fe should include its own registration flow (create new fe_user + passkey in one step).

Three approaches were considered:

A. Enrollment only — Passkeys for existing fe_users, PSR-14 hooks for integrations B. Enrollment + basic registration — Minimalist registration form C. Enrollment + registration hooks — No form, but callable service

Decision 

Option A with Option C's hooks.

  • Only existing fe_users can enroll passkeys through the management plugin or post-login enrollment flow.
  • A PasskeyEnrollmentService is provided as a public service that registration extensions can call programmatically.
  • PSR-14 events (BeforePasskeyEnrollmentEvent, AfterPasskeyEnrollmentEvent) allow third-party extensions to hook into the enrollment lifecycle.

Consequences 

Positive:

  • Focused scope: no competition with femanager/sf_register
  • Clean separation of concerns (registration vs. authentication)
  • PSR-14 events enable any registration extension to add "Register with Passkey"
  • Smaller codebase, fewer security surfaces

Negative:

  • No out-of-box "Register with Passkey" experience
  • Integrators must configure registration extensions separately
  • Higher setup effort for new installations

Mitigation:

  • Documentation includes integration guides for femanager and sf_register
  • PasskeyEnrollmentService has a simple API: enroll(int $feUserUid, ...): FrontendCredential
  • Example code in developer guide for calling enrollment from custom registration forms

Alternatives Considered 

Option B (Basic registration): Tempting for simplicity, but any registration form immediately requires: email verification, CAPTCHA, terms acceptance, profile fields, GDPR consent. This is a rabbit hole that existing extensions handle better.

ADR-005: Site-Configurable RP ID with Storage PID Credential Isolation 

Status

Accepted

Date

2026-03-14

Decision-makers

Sebastian Mendel

Context 

WebAuthn credentials are bound to the Relying Party ID (RP ID), which is effectively the domain. Unlike the TYPO3 backend (single domain), the frontend can serve multiple domains. Existing WebAuthn extensions (e.g., bnf/mfa-webauthn) document explicit HTTPS/single-domain limitations.

Additionally, TYPO3 supports multiple FE user pools via storage PIDs (page IDs where fe_users are stored). A credential registered on shop.example.com (storage PID 42) must not authenticate a user on portal.example.com (storage PID 99), even if both happen to share the same RP ID.

Decision 

Site-configurable RP ID + storage PID credential isolation.

  1. Default RP ID: Derived from the TYPO3 site configuration's base URL domain. Zero configuration needed for single-domain sites.
  2. Override RP ID: Configurable per site in settings.yaml:

    nr_passkeys_fe:
      rpId: 'example.com'
    Copied!

    This allows shop.example.com and blog.example.com to share credentials when the RP ID is set to the parent domain example.com (permitted by WebAuthn spec).

  3. Storage PID scoping: Every credential query includes the storage PID and site identifier. The FrontendCredentialRepository enforces this at the query level.
  4. Credential-ID-to-UID resolution: Always resolves by fe_user.uid, never by username. See ADR-008.

Consequences 

Positive:

  • Zero-config for 90% of sites (single domain)
  • Multi-subdomain support via RP ID override
  • Storage PID isolation prevents cross-pool credential leakage
  • Follows WebAuthn spec for RP ID parent domain rule

Negative:

  • Admins must understand WebAuthn RP ID rules for multi-domain setups
  • RP ID changes invalidate all existing credentials (WebAuthn spec limitation)
  • Storage PID adds WHERE clause to all credential queries (negligible performance impact)

Mitigation:

  • Documentation with clear examples for single-domain, multi-subdomain, and multi-site
  • Admin module shows RP ID per credential for debugging
  • Warning in admin module if RP ID mismatch detected

Alternatives Considered 

Single domain only (Option A): Too restrictive for real-world TYPO3 installations with multi-site setups.

No storage PID scoping: Dangerous. TYPO3-SA-2024-006 demonstrated that ambiguous user resolution across storage folders creates security vulnerabilities.

ADR-006: Dual Enforcement Model (Site + FE Groups, Strictest Wins) 

Status

Accepted

Date

2026-03-14

Decision-makers

Sebastian Mendel

Context 

nr-passkeys-be uses per-group enforcement on be_groups with four levels: Off, Encourage, Required, Enforced. For frontend users, there is an additional dimension: site-level enforcement, since TYPO3 can serve multiple sites with different security requirements.

Decision 

Dual enforcement: per ``fe_groups`` + per site configuration. Strictest wins.

Enforcement levels (reusing EnforcementLevel from nr-passkeys-be):

  • Off: No passkey prompts or requirements.
  • Encourage: Dismissible enrollment banner after login.
  • Required: Enrollment interstitial after login. Skippable during grace period. After grace period, passkey enrollment is mandatory but password fallback remains.
  • Enforced: Mandatory passkey enrollment, no skip. Password login blocked for users who have at least one passkey registered.

Resolution algorithm:

$effectiveLevel = max(
    $siteEnforcementLevel->severity(),
    $strictestGroupLevel->severity()
);
Copied!

Site-level enforcement is set in config/sites/*/settings.yaml:

nr_passkeys_fe:
  enforcement: 'encourage'
Copied!

Group-level enforcement is set on fe_groups.passkey_enforcement.

Grace period is per-group (fe_groups.passkey_grace_period_days), tracked per-user (fe_users.passkey_grace_period_start).

Consequences 

Positive:

  • Site-level gives integrators a global switch for the whole portal
  • Group-level gives granular control for privileged user groups
  • Consistent with nr-passkeys-be enforcement model
  • EnforcementLevelResolvedEvent allows custom business logic to override

Negative:

  • Two places to configure = potential admin confusion
  • "Strictest wins" may surprise admins (group Required + site Off = Required)

Mitigation:

  • Admin module shows effective enforcement per user with source breakdown
  • Documentation with examples: "Site Off + Group Required = Required for that group"
  • FrontendEnforcementStatus DTO exposes both siteLevel and groupLevel for transparency

Alternatives Considered 

Per-group only (Option A): No way to enforce site-wide without touching every group.

Per-site only (Option B): No granular control. Cannot require passkeys for admins but not regular users.

ADR-007: Post-Login Enrollment Interstitial via Middleware 

Status

Accepted

Date

2026-03-14

Decision-makers

Sebastian Mendel

Context 

Users who log in with a password and don't yet have passkeys registered need to be prompted to enroll. This is the primary adoption driver. nr-passkeys-be solves this with a PSR-15 middleware (PasskeySetupInterstitial) that intercepts backend navigation for users who must register passkeys.

In the frontend, this is more complex because:

  • FE pages are public-facing, not a controlled backend UI
  • Redirect loops must be avoided (enrollment page itself must be exempt)
  • The enrollment experience must be self-contained and user-friendly

Decision 

PSR-15 middleware (``PasskeyEnrollmentInterstitial``) in the FE request chain.

The middleware runs after FrontendUserAuthenticator and checks:

  1. Is the user authenticated? (No → skip)
  2. Does the user have passkeys? (Yes → skip)
  3. What is the effective enforcement level?

    • Off: Skip
    • Encourage: Skip (banner handles this, not interstitial)
    • Required: Redirect to enrollment page. Skip allowed with session nonce during grace period. After grace period, redirect is persistent.
    • Enforced: Redirect to enrollment page. No skip option.
  4. Is the current request to an exempt path? (passkey API routes, logout, enrollment page itself → skip)

Enrollment page: A dedicated page with the PasskeyEnrollmentPlugin that integrators configure via TypoScript constant:

plugin.tx_nrpasskeysfe.settings.enrollmentPage = 42
Copied!

Skip mechanism: Session-based nonce (CSRF-protected). Skipping sets a session flag that suppresses the redirect for the remainder of the session.

Consequences 

Positive:

  • Proven pattern from nr-passkeys-be, adapted for FE
  • Enforcement levels give gradual adoption control
  • Skip mechanism prevents user frustration during grace period
  • Exempt paths prevent redirect loops

Negative:

  • Requires integrator to create a dedicated enrollment page
  • Redirect may confuse users unfamiliar with passkeys
  • Session-based skip resets on new session (intentional for Required level)

Mitigation:

  • Quick start documentation with step-by-step enrollment page setup
  • Clear messaging on the enrollment page explaining why the user is there
  • Encourage level uses non-intrusive banner instead of redirect

Alternatives Considered 

Banner only (no interstitial): Banners are easily dismissed and forgotten. For Required and Enforced levels, a redirect is the only way to ensure users actually enroll.

JavaScript-only prompt: Fragile (can be blocked by ad blockers), not accessible, and can't enforce enrollment for Enforced level.

ADR-008: Credential-ID-to-UID Resolution (Not Username) 

Status

Accepted

Date

2026-03-14

Decision-makers

Sebastian Mendel

Context 

When a passkey assertion is received (especially in discoverable login mode), the system must resolve which fe_user the credential belongs to. There are two approaches:

A. Resolve by credential_idfe_user.uid (direct UID lookup) B. Resolve by credential_idusernamefe_user (username lookup)

TYPO3 has a known security issue with username-based FE authentication across multiple storage folders. TYPO3-SA-2024-006 addressed a vulnerability where ambiguous usernames in different storage folders could lead to authentication bypass or privilege escalation.

Decision 

Always resolve by credential_id → fe_user.uid.

The FrontendCredentialRepository.findByCredentialId() returns the fe_user UID directly from the credential record. The auth service then loads the fe_user by UID, not by username.

Additionally, all credential queries include the storage_pid to prevent cross-pool resolution:

$credential = $this->credentialRepository->findByCredentialId(
    $credentialId,
    $storagePid  // Always scoped
);

if ($credential === null) {
    return null; // Unknown credential
}

// Load fe_user by UID, not username
$feUser = $this->loadFeUserByUid($credential->getFeUser());
Copied!

Consequences 

Positive:

  • Eliminates username ambiguity vulnerability entirely
  • Storage PID scoping prevents cross-pool credential leakage
  • Simpler and faster (single indexed lookup vs. username search)
  • Consistent with WebAuthn spec (credentials are identified by ID, not username)

Negative:

  • If fe_user.uid changes (e.g., import/migration), credentials break
  • Cannot use TYPO3's built-in username resolution logic

Mitigation:

  • fe_user UIDs rarely change in practice
  • Migration documentation for credential re-linking if UIDs change
  • Admin module shows credential-to-user mapping for debugging

Alternatives Considered 

Option B (Username resolution): Vulnerable to the same class of bugs that TYPO3-SA-2024-006 addressed. Multiple fe_users with the same username in different storage folders would create ambiguity. Rejected.

ADR-009: Vanilla JavaScript for Frontend (No Framework Dependencies) 

Status

Accepted

Date

2026-03-14

Decision-makers

Sebastian Mendel

Context 

The frontend JavaScript needs to handle WebAuthn ceremonies (navigator.credentials API), form interactions, and API calls. Unlike the TYPO3 backend, the frontend has no built-in JavaScript infrastructure (no TYPO3 backend module loader, no Lit, no RequireJS).

Options considered:

A. Vanilla ES6 modules — No framework, plain JavaScript B. Lit/Web Components — Lightweight, standards-based C. React/Vue/Svelte — Full SPA framework D. TYPO3 backend JS modules — Reuse backend patterns

Decision 

Option A: Vanilla ES6 modules for frontend, TYPO3 backend JS modules for admin UI.

Frontend (5 modules):

  • PasskeyLogin.js — Login ceremony (navigator.credentials.get)
  • PasskeyEnrollment.js — Registration ceremony (navigator.credentials.create)
  • PasskeyManagement.js — Self-service CRUD
  • PasskeyRecovery.js — Recovery code entry
  • PasskeyBanner.js — Enrollment prompt banner

All modules:

  • Use progressive enhancement (forms work without JS, JS enhances with WebAuthn)
  • Use data-* attributes for configuration (no inline scripts)
  • Use fetch() for API calls
  • No build step required (directly loadable as ES modules)

Backend admin (2 modules):

  • PasskeyFeAdmin.js — Admin dashboard
  • PasskeyFeAdminInfo.js — User info element

Backend modules use TYPO3's @typo3/ import system, consistent with nr-passkeys-be.

Consequences 

Positive:

  • Zero dependencies: no npm install needed for production
  • Works in any TYPO3 frontend theme (no framework conflicts)
  • Progressive enhancement: accessible without JavaScript
  • Small bundle size ( 15-20KB total)
  • No build step needed
  • Easy to override/customize for integrators

Negative:

  • More verbose than framework-based code
  • No reactive state management (manual DOM updates)
  • No component model for complex UIs

Mitigation:

  • Frontend JS is simple (ceremony calls + form handling), doesn't need a framework
  • Vitest tests mock navigator.credentials for unit testing
  • CSS classes follow BEM convention for easy styling override

Alternatives Considered 

Option B (Lit/Web Components): Adds a dependency and build step for marginal benefit. WebAuthn ceremonies are procedural, not component-oriented.

Option C (React/Vue/Svelte): Massive overkill for form handling + API calls. Would conflict with the site's existing frontend framework.

Option D (TYPO3 backend JS): Not available in frontend context. TYPO3's backend JS infrastructure (@typo3/ modules, Lit elements) is backend-only.

ADR-010: Recovery Codes Hashed with bcrypt 

Status

Accepted

Date

2026-03-14

Decision-makers

Sebastian Mendel

Context 

Recovery codes are a fallback authentication mechanism. When a user generates recovery codes, the system stores them and the user saves the plaintext codes offline. At login time, the user enters a code which must be verified against the stored value.

The storage format must be:

  1. One-way (codes cannot be recovered from the database)
  2. Resistant to brute force (if database is compromised)
  3. Verifiable at login time

Options:

A. Plaintext storage — Trivial, but any database leak exposes all codes B. SHA-256 hash — Fast, but vulnerable to rainbow tables / brute force C. bcrypt hash — Intentionally slow, resistant to brute force D. Argon2id hash — Modern, memory-hard, strongest protection

Decision 

Option C: bcrypt with cost factor 12.

Each recovery code is stored as a bcrypt hash in tx_nrpasskeysfe_recovery_code.code_hash.

Code format: XXXX-XXXX (8 alphanumeric characters, grouped for readability). This gives 36^8 ≈ 2.8 trillion possible codes, making brute force infeasible even with fast hashing.

// Generation
$plaintext = $this->generateRandomCode(); // e.g., "A7K2-M9P4"
$hash = password_hash($plaintext, PASSWORD_BCRYPT, ['cost' => 12]);

// Verification
$valid = password_verify($inputCode, $storedHash);
Copied!

Consequences 

Positive:

  • Industry standard for credential storage
  • Cost factor 12 makes brute force impractical ( 250ms per hash on modern hardware)
  • password_hash() / password_verify() are PHP built-ins, no dependencies
  • Automatic salt generation per hash

Negative:

  •  250ms per verification attempt (acceptable for login, rate-limited anyway)
  • Cannot bulk-verify codes (each hash is unique due to salt)
  • bcrypt has 72-byte input limit (not an issue for 8-char codes)

Mitigation:

  • Rate limiting on recovery code verification endpoint
  • Account lockout after N failed recovery code attempts
  • Codes are single-use (marked used_at after successful verification)

Alternatives Considered 

Option A (Plaintext): Unacceptable for any credential storage.

Option B (SHA-256): Too fast. An attacker with the database could brute-force all possible 8-character codes in minutes on a GPU.

Option D (Argon2id): Stronger than bcrypt, but requires libargon2 which may not be available on all PHP installations. bcrypt is universally available and sufficient for recovery codes with rate limiting.

ADR-012: Authentication Service Priority 80 

Status

Accepted

Date

2026-03-14

Decision-makers

Sebastian Mendel

Context 

TYPO3's frontend authentication service chain processes services in descending priority order. SaltedPasswordService runs at priority 50. Third-party extensions commonly use various priorities:

  • LDAP extensions: typically 80-90
  • OAuth/OIDC extensions: typically 70-80
  • SAML extensions: typically 80-90
  • nr-passkeys-be: priority 80

The passkey auth service must intercept login requests before the password service but cooperate with other auth providers.

Decision 

Priority 80, matching nr-passkeys-be.

The PasskeyFrontendAuthenticationService runs at priority 80:

  • Above SaltedPasswordService (50) — passkey payloads are checked first
  • At same level as nr-passkeys-be (80) — consistent behavior
  • Below most LDAP/SAML providers (90+) — SSO providers take precedence

When the service receives a request without a passkey payload (_type: "passkey"), it returns 100 (continue chain), passing control to the next service. This means:

  • LDAP/SSO at 90: processes first, passkey at 80 only sees non-SSO requests
  • Passkey at 80: checks for passkey payload, passes non-passkey requests to password
  • Password at 50: handles traditional password authentication

Consequences 

Positive:

  • Consistent with nr-passkeys-be, reducing confusion
  • SSO providers at 90+ take precedence (correct: if IdP handles auth, passkey is irrelevant)
  • Non-passkey logins fall through cleanly to password service

Negative:

  • Priority collision possible if another extension also uses 80
  • TYPO3 does not guarantee order for same-priority services

Mitigation:

  • Document the priority in extension settings (not currently configurable, but could be)
  • The service's getUser() only activates when _type: "passkey" is present in loginData['uident'], so a priority collision with a non-passkey service (e.g., LDAP) is harmless — both services check for their own payload type

Alternatives Considered 

Priority 90 (above LDAP): Would intercept passkey payloads before LDAP, which is correct, but could interfere with SSO flows that should take absolute precedence.

Priority 60 (just above password): Would work but wouldn't match nr-passkeys-be, and would let more services process the request before passkeys are checked, adding unnecessary latency for passkey logins.

Configurable priority: Over-engineering for a niche concern. If an integrator needs a custom priority, they can override the service registration in ext_localconf.php.

Changelog 

Version 0.1.0 

Initial release

This is the first public release of Passkeys Frontend Authentication (nr_passkeys_fe). It provides passkey-first login for TYPO3 frontend users with all core features.

Features 

  • Passkey-first login -- Discoverable (usernameless) and username-first login flows via the NrPasskeysFe:Login plugin. Supports all FIDO2/WebAuthn-compliant authenticators.
  • felogin integration -- Injects a passkey button into the standard felogin plugin via PSR-14 event listener. No login provider switching required.
  • Self-service management -- Frontend users can enroll, rename, and revoke their own passkeys via the NrPasskeysFe:Management plugin.
  • Recovery codes -- Users can generate 10 one-time recovery codes (bcrypt hashed) as a fallback when no authenticator device is available.
  • Per-site RP ID -- Each TYPO3 site has an independent WebAuthn Relying Party configuration via config.yaml.
  • Per-group enforcement -- Four enforcement levels (Off, Encourage, Required, Enforced) configurable per site and per frontend user group with configurable grace periods.
  • Post-login interstitial -- Users without a passkey are shown an enrollment interstitial when enforcement level is Required or Enforced.
  • Backend admin module -- Administrators can view adoption statistics, manage credentials, and configure enforcement from Admin Tools > Passkey Management FE.
  • PSR-14 events -- Seven events for extensibility: before/after authentication, before/after enrollment, enforcement level resolved, passkey removed, recovery codes generated.
  • Security hardened -- HMAC-signed challenges, nonce replay protection, per-IP rate limiting, and account lockout (shared with nr-passkeys-be).
  • Vanilla JavaScript -- Zero runtime npm dependencies. The frontend JavaScript uses only the native WebAuthn browser API.

Requirements 

  • TYPO3 13.4 LTS or 14.1+
  • PHP 8.2+
  • netresearch/nr-passkeys-be ^0.6
  • HTTPS

Known limitations 

  • Magic link login is deferred to v0.2 (ADR-011). The event class and service will be added in v0.2.
  • No admin-initiated passkey registration on behalf of users.