Passkeys Backend Authentication 

Extension key

nr_passkeys_be

Package name

netresearch/nr-passkeys-be

Version

0.4

Language

en

Author

Netresearch DTT GmbH

License

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

Rendered

Tue, 17 Feb 2026 23:13:01 +0000


Passwordless TYPO3 backend authentication via WebAuthn/FIDO2 Passkeys. Enables one-click login with TouchID, FaceID, YubiKey, and Windows Hello -- directly on the standard TYPO3 login form.

TYPO3 login form with Sign in with a passkey button

The passkey button appears below the Login button with an "or" divider.


Introduction 

Learn what the extension does, which authenticators and browsers are supported, and see the full feature list.

Installation 

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

Configuration 

Configure relying party, challenge TTL, discoverable login, rate limiting, account lockout, and cryptographic algorithms.

Usage 

Register passkeys, log in with a single touch, and manage your credentials in User Settings.

Administration 

Admin API for listing, revoking credentials and unlocking locked-out accounts.

Developer Guide 

Architecture overview, authentication service, controllers, services, and how to run tests.

Security 

WebAuthn security model, HMAC-signed challenges, rate limiting, and user enumeration prevention.

Troubleshooting 

Common error messages, encryptionKey issues, HTTPS requirements, and debug logging.

Changelog 

Version history and release notes.

Introduction 

What does it do? 

Passkeys Backend Authentication provides passwordless authentication for the TYPO3 backend using the WebAuthn/FIDO2 standard (Passkeys). Backend users can log in with a single touch or glance using biometric authenticators such as TouchID, FaceID, Windows Hello, or hardware security keys like YubiKey.

The passkey button is injected directly into the standard TYPO3 login form via a PSR-14 event listener -- no login provider switching needed. Users see the familiar login page with a Sign in with a passkey button below the Login button.

Passkeys are a modern, phishing-resistant replacement for passwords. They use public-key cryptography: the private key never leaves the user's device, and the server only stores a public key. This eliminates the risk of credential theft through phishing or database breaches.

Features 

Passwordless login 

Authenticate with TouchID, FaceID, YubiKey, or Windows Hello instead of a password. Injected directly into the standard TYPO3 login form.

Primary credential 

Passkeys are a first-class authentication method (not MFA). The extension registers at priority 80, above the standard password service.

Credential management 

Users can register, rename, and remove their own passkeys through the TYPO3 User Settings module.

Admin panel 

Administrators can list, revoke, and manage passkeys for any backend user, and unlock locked-out accounts.

Discoverable login 

Optional usernameless login (Conditional UI) where the browser auto-suggests available passkeys. Controlled via extension settings.

Security hardened 

HMAC-signed challenges with nonce replay protection, rate limiting by IP, account lockout, user enumeration prevention, and audit logging.

Configurable algorithms 

Supports ES256, ES384, ES512, and RS256 signing algorithms. Configurable user verification requirement.

TYPO3 v12, v13, and v14 

Compatible with TYPO3 12.4 LTS, 13.4 LTS, and 14.x. 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+

Installation 

Prerequisites 

  • TYPO3 12.4 LTS, TYPO3 13.4 LTS, or TYPO3 14.x
  • PHP 8.2, 8.3, 8.4, or 8.5
  • HTTPS is required for WebAuthn (except for 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-be
Copied!

Activate the extension 

After installation, activate the extension in the TYPO3 backend:

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

Or use the CLI:

vendor/bin/typo3 extension:activate nr_passkeys_be
Copied!

Database schema update 

The extension adds a tx_nrpasskeysbe_credential table. 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!

Verify the installation 

After activation:

  1. The TYPO3 backend login page should show a Sign in with a passkey button below the Login button.

    TYPO3 login form with the passkey button visible

    The passkey button appears below the Login button, separated by an "or" divider.

  2. In User Settings, a "Passkeys" section should appear where authenticated users can register their first passkey.

Configuration 

All settings are managed through the TYPO3 Extension Configuration module:

Admin Tools > Settings > Extension Configuration > nr_passkeys_be

Relying Party settings 

rpId

rpId
type

string

Default

(auto-detected from HTTP_HOST)

The Relying Party identifier. This is typically the domain name of your TYPO3 installation (e.g. example.com). If left empty, it is auto-detected from the HTTP_HOST server variable.

rpName

rpName
type

string

Default

TYPO3 Backend

A human-readable name for the Relying Party. This is displayed to users during passkey registration (e.g. in the browser's passkey creation dialog).

origin

origin
type

string

Default

(auto-detected)

The expected origin for WebAuthn operations (e.g. https://example.com). If left empty, it is auto-detected from the current request scheme and host.

Challenge settings 

challengeTtlSeconds

challengeTtlSeconds
type

int

Default

120

The time-to-live for challenge tokens in seconds. After this period, the challenge expires and the user must request a new one. The default of 120 seconds provides enough time for users to interact with their authenticator.

Discoverable login 

discoverableLoginEnabled

discoverableLoginEnabled
type

bool

Default

true

Enable discoverable (identifierless) login. When enabled (default), the browser can suggest available passkeys without the user entering a username first (Conditional UI / Variant B). The user simply clicks a suggested passkey from the browser's autofill dropdown.

When disabled, users must enter their username first, then authenticate with their passkey (Variant A: username-first flow).

Password login control 

disablePasswordLogin

disablePasswordLogin
type

bool

Default

false

Disable traditional password login entirely. When enabled, only passkey authentication is accepted. Non-passkey login attempts are blocked.

When this setting is active, users cannot remove their last passkey to prevent locking themselves out.

Rate limiting 

rateLimitMaxAttempts

rateLimitMaxAttempts
type

int

Default

10

Maximum number of requests allowed per IP address per endpoint within the rate limit window. Exceeding this limit returns HTTP 429 (Too Many Requests).

rateLimitWindowSeconds

rateLimitWindowSeconds
type

int

Default

300

Duration of the rate limiting window in seconds. The attempt 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 combination.

lockoutDurationSeconds

lockoutDurationSeconds
type

int

Default

900

Duration of the account lockout in seconds (default: 15 minutes). After this period the lockout expires automatically. Administrators can also manually unlock accounts via the admin API.

Cryptographic algorithms 

allowedAlgorithms

allowedAlgorithms
type

string

Default

ES256

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

  • ES256 -- ECDSA with SHA-256 (recommended, widely supported)
  • 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. Valid values:

  • required -- The authenticator must verify the user (e.g. biometric or PIN). This is the most secure option.
  • preferred -- The authenticator should verify the user if possible, but authentication proceeds even without verification.
  • discouraged -- The authenticator should not verify the user. Use this only if you want the fastest possible authentication.

Invalid values fall back to required.

Usage 

Registering a passkey 

Before you can use passwordless login, you need to register at least one passkey:

  1. Log in to the TYPO3 backend with your regular password.
  2. Go to User Settings (click your avatar in the top-right corner).
  3. Find the Passkeys section.
  4. Enter a descriptive name in the text field (e.g. "MacBook TouchID" or "Office YubiKey"). The default is "Passkey".
  5. Click Add Passkey.
  6. Your browser will prompt you to create a passkey using your preferred authenticator (TouchID, Windows Hello, YubiKey, etc.).
  7. After successful registration the passkey appears in the list and the name input resets for the next registration.
User Settings page with Passkeys management section

Manage your passkeys in the User Settings module.

You can register multiple passkeys for the same account -- for example, one on your laptop and one on a hardware security key.

Logging in with a passkey 

Discoverable login (default) 

With discoverableLoginEnabled enabled (the default):

  1. Navigate to the TYPO3 backend login page.
  2. The browser may automatically show available passkeys in an autofill dropdown (Conditional UI).
  3. Select your passkey.
  4. Verify with your authenticator.
  5. You are logged in without typing a username.

Username-first flow 

When discoverableLoginEnabled is set to false:

  1. Navigate to the TYPO3 backend login page.
  2. Enter your username.
  3. Click Sign in with a passkey.
  4. Your browser will prompt you to verify with your authenticator.
  5. Upon successful verification, you are logged in.
Login form with username filled and passkey button ready

Enter your username, then click Sign in with a passkey.

Error handling 

If a passkey login fails (for example, the server cannot verify the assertion), a passkey-specific error message is shown on the login page:

Login form showing passkey authentication failed error

A clear error message tells you the passkey was not accepted.

Managing your passkeys 

In User Settings > Passkeys, you can:

  • View all your registered passkeys with their labels, creation dates, and last-used timestamps.
  • Rename a passkey by clicking its label and entering a new name (max 128 characters).
  • Remove a passkey you no longer need.

Fallback to password login 

By default, password login remains available. If a user does not have a passkey registered or their authenticator is unavailable, they can still log in with their regular TYPO3 password.

This fallback can be disabled with the disablePasswordLogin setting.

Administration 

This chapter covers administrator-specific functionality for managing passkeys across all backend users.

Admin API endpoints 

The extension provides admin-only AJAX endpoints for credential and account management. All admin endpoints require the requesting user to have TYPO3 admin privileges. Write operations are protected by Sudo Mode (password re-verification with a 15-minute grant lifetime).

List user credentials 

GET /typo3/ajax/passkeys/admin/list?beUserUid=<uid>
Copied!

Returns all credentials (including revoked ones) for a specific backend user.

Response fields per credential:

  • uid -- Credential record UID
  • label -- User-assigned label
  • createdAt -- Unix timestamp of registration
  • lastUsedAt -- Unix timestamp of last successful login
  • isRevoked -- Whether the credential has been revoked
  • revokedAt -- Unix timestamp of revocation (0 if not revoked)
  • revokedBy -- UID of the admin who revoked the credential

Revoke a credential 

POST /typo3/ajax/passkeys/admin/remove
Content-Type: application/json

{"beUserUid": 123, "credentialUid": 456}
Copied!

Revokes a specific passkey for a backend user. The credential is not deleted but marked as revoked with a timestamp and the revoking admin's UID. Revoked credentials cannot be used for authentication.

This endpoint requires Sudo Mode verification (HTTP 422 if not verified).

Unlock a locked account 

POST /typo3/ajax/passkeys/admin/unlock
Content-Type: application/json

{"beUserUid": 123, "username": "johndoe"}
Copied!

Resets the lockout counter for a specific backend user. Use this when a user has been locked out due to too many failed authentication attempts and cannot wait for the lockout to expire automatically.

This endpoint requires Sudo Mode verification (HTTP 422 if not verified).

Credential lifecycle 

Passkeys go through the following states:

  1. Registered -- The credential is created via the management API and stored in the tx_nrpasskeysbe_credential table.
  2. Active -- The credential is used for successful logins. The last_used_at and sign_count fields are updated on each use.
  3. Revoked -- An administrator revokes the credential via the admin API. The revoked_at timestamp and revoked_by admin UID are recorded. Revoked credentials remain in the database but are rejected during authentication.
  4. Deleted -- A user removes their own credential via the management API. The record is soft-deleted (deleted = 1).

Database table 

The extension uses a single table tx_nrpasskeysbe_credential with the following schema:

Column Type Description
uid int Primary key (auto-increment)
be_user int FK to be_users.uid
credential_id varbinary WebAuthn credential ID (unique)
public_key_cose blob COSE-encoded public key
sign_count int Signature counter (replay detection)
user_handle varbinary WebAuthn user handle (SHA-256 hash)
aaguid char(36) Authenticator attestation GUID
transports text JSON array of transport hints
label varchar(128) User-assigned label
created_at int Unix timestamp of creation
last_used_at int Unix timestamp of last use
revoked_at int Unix timestamp of revocation (0=active)
revoked_by int UID of revoking admin (0=not revoked)
deleted tinyint Soft delete flag

Monitoring 

The extension logs all significant events using the PSR-3 logging interface:

  • Successful passkey registrations
  • Successful passkey logins
  • Failed authentication attempts (with hashed username and IP)
  • Admin credential revocations
  • Admin account unlocks
  • Rate limit and lockout triggers

Configure TYPO3 logging writers to capture these events. Example for file logging:

$GLOBALS['TYPO3_CONF_VARS']['LOG']['Netresearch']['NrPasskeysBe']['writerConfiguration'] = [
    \Psr\Log\LogLevel::INFO => [
        \TYPO3\CMS\Core\Log\Writer\FileWriter::class => [
            'logFileInfix' => 'passkeys',
        ],
    ],
];
Copied!

Developer Guide 

This chapter describes the extension's architecture and provides guidance for developers who want to understand, debug, or extend the extension.

Architecture overview 

The extension consists of these core components:

Classes/
  Authentication/     Auth service (TYPO3 auth chain)
  Configuration/      Extension configuration value object
  Controller/         REST API controllers (Login, Manage, Admin)
  Domain/Model/       Credential entity
  EventListener/      PSR-14 listener (login form injection)
  Middleware/          PSR-15 middleware (public route resolver)
  Service/            Business logic services
Copied!

Login form injection 

The passkey button is injected into the standard TYPO3 login form via the InjectPasskeyLoginFields PSR-14 event listener. It listens to ModifyPageLayoutOnLoginProviderSelectionEvent and:

  • Loads PasskeyLogin.js via PageRenderer::addJsFile()
  • Injects an inline script with window.NrPasskeysBeConfig that provides loginOptionsUrl, rpId, origin, and discoverableEnabled to the JavaScript

The JavaScript builds the passkey UI (button, error area, hidden fields) dynamically via DOM manipulation and inserts it into #typo3-login-form. No Fluid partial or separate template is needed.

The passkey management panel in User Settings also uses loadJavaScriptModule() to load PasskeyManagement.js as an ES module, which imports TYPO3 native APIs (AjaxRequest, Notification, Modal, sudoModeInterceptor, DocumentService).

Authentication data flow 

The passkey assertion and challenge token are packed into the userident field as JSON:

{
    "_type": "passkey",
    "assertion": {"id": "...", "type": "public-key", "response": {}},
    "challengeToken": "..."
}
Copied!

The PasskeyAuthenticationService reads from $this->login['uident'], detects the _type: "passkey" marker, and extracts the assertion and challenge token for verification.

Authentication service 

PasskeyAuthenticationService extends TYPO3's AbstractAuthenticationService and is registered at priority 80 (higher than SaltedPasswordService at 50).

The service implements two methods:

  • getUser() -- Checks if the login data contains a passkey payload (JSON with _type: "passkey"). If it does, the user is looked up by username. If no passkey data is present, the request falls through to the next auth service.
  • authUser() -- Returns:

    • 200 -- Authenticated, stop chain (passkey verified)
    • 100 -- Not responsible (no passkey data, let next service handle it)
    • 0 -- Authentication failed

Because TYPO3 authentication services are instantiated by the service manager (not the DI container), dependencies are obtained via GeneralUtility::makeInstance().

Public route middleware 

PublicRouteResolver is a PSR-15 middleware that allows passkey login API endpoints (/typo3/passkeys/login/*) to be accessed without an authenticated backend session. Without it, TYPO3 would redirect unauthenticated requests to the login page.

Controllers 

The extension registers backend routes for three controller groups. All controllers use the JsonBodyTrait for parsing JSON request bodies. Login routes use Routes.php (public access). Management and admin routes use AjaxRoutes.php (AJAX, with Sudo Mode on write operations).

LoginController (public) 

Handles the passkey login flow. Routes have access: public (via Routes.php).

  • POST /passkeys/login/options
  • POST /passkeys/login/verify

ManagementController (AJAX) 

Passkey lifecycle for the current user (via AjaxRoutes.php). Write operations require Sudo Mode re-authentication.

  • POST /ajax/passkeys/manage/registration/options *
  • POST /ajax/passkeys/manage/registration/verify *
  • GET /ajax/passkeys/manage/list
  • POST /ajax/passkeys/manage/rename *
  • POST /ajax/passkeys/manage/remove *

AdminController (AJAX, admin) 

Administrative operations for any user (via AjaxRoutes.php). Write operations require Sudo Mode re-authentication.

  • GET /ajax/passkeys/admin/list
  • POST /ajax/passkeys/admin/remove *
  • POST /ajax/passkeys/admin/unlock *

Routes marked with * are protected by TYPO3's Sudo Mode. When accessed without a recent password verification, they return HTTP 422 with sudoModeInitialization data. The JavaScript handles this transparently by showing a password dialog and retrying the request.

Service classes 

WebAuthnService 

Orchestrates WebAuthn ceremonies using web-auth/webauthn-lib v5.x. Handles registration options, attestation verification, assertion options, and assertion verification.

ChallengeService 

Generates and verifies HMAC-signed challenge tokens with nonce replay protection.

CredentialRepository 

Database access layer for tx_nrpasskeysbe_credential. Uses ConnectionPool directly (no Extbase).

RateLimiterService 

Per-endpoint rate limiting by IP and account lockout after configurable failed attempts. Uses TYPO3 caching framework.

ExtensionConfigurationService 

Reads extension configuration and computes effective values for rpId and origin (auto-detection from request).

Domain model 

The Credential class is a plain PHP value object (not Extbase) with fromArray()/toArray() for database serialization.

Key fields:

  • credentialId -- WebAuthn credential identifier (binary)
  • publicKeyCose -- COSE-encoded public key (binary blob)
  • signCount -- Counter incremented on each use (clone detection)
  • userHandle -- SHA-256 hash of the user UID + encryption key
  • aaguid -- Authenticator Attestation GUID
  • transports -- JSON array of transport hints

Running tests 

Available test commands
# Unit tests (301 tests, 1060 assertions)
composer ci:test:php:unit

# Fuzz tests (122 tests, 1608 assertions)
composer ci:test:php:fuzz

# Functional tests (24 tests, requires MySQL)
composer ci:test:php:functional

# Static analysis (PHPStan level 10)
composer ci:stan

# Code style (PER-CS3.0)
composer ci:lint:php

# JavaScript unit tests (33 tests, Vitest)
npm run test:js

# Mutation testing (Infection, MSI >= 80%)
composer ci:mutation
Copied!

Security 

This chapter documents the security model and countermeasures implemented by the extension.

WebAuthn security model 

WebAuthn (Web Authentication) is a W3C standard that uses public-key cryptography for authentication:

  • During registration, the authenticator generates a key pair. The private key stays on the device; the public key is sent to the server.
  • During authentication, the server sends a random challenge. The authenticator signs it with the private key. The server verifies the signature with the stored public key.

This provides inherent protection against:

  • Phishing -- The credential is bound to the origin (domain). It cannot be used on a different domain, even if the user is tricked into visiting one.
  • Credential theft -- The private key never leaves the authenticator device. Even if the server database is compromised, attackers cannot impersonate users.
  • Replay attacks -- Each authentication uses a unique challenge, and the signature counter detects cloned authenticators.

HMAC-signed challenge tokens 

Challenge tokens are the core mechanism preventing unauthorized authentication attempts. Each token contains:

  1. A 32-byte random challenge generated by random_bytes(32)
  2. An expiration timestamp (configurable TTL, default 120 seconds)
  3. A single-use nonce (32 hex characters from random_bytes(16))

These components are concatenated and signed with HMAC-SHA256 using the TYPO3 encryption key as the signing secret. The final token is base64-encoded.

Security properties:

  • Integrity -- The HMAC ensures the token cannot be tampered with. Verification uses hash_equals() for constant-time comparison, preventing timing side-channel attacks.
  • Freshness -- The expiration timestamp prevents use of stale tokens.
  • Single-use -- The nonce is stored in a TYPO3 cache and consumed on first use. Subsequent uses of the same token are rejected.
  • Signing key requirements -- The TYPO3 encryption key must be at least 32 characters. The extension throws a clear error if this requirement is not met.

Nonce replay protection 

Each challenge token contains a nonce that is stored in a TYPO3 cache (SimpleFileBackend) upon creation. During verification:

  1. The nonce is looked up in the cache.
  2. If found, it is immediately invalidated (removed from cache).
  3. If not found (already used or expired), the verification fails.

This ensures each challenge token can only be used exactly once, even if an attacker intercepts and replays it.

The nonce cache has a TTL slightly longer than the challenge TTL (extra 60 seconds buffer) to handle clock skew.

Rate limiting 

Per-endpoint rate limiting 

Each API endpoint tracks request counts per IP address. When the configured threshold (rateLimitMaxAttempts, default: 10) is exceeded within the time window (rateLimitWindowSeconds, default: 300 seconds), the endpoint returns HTTP 429 (Too Many Requests).

This limits automated attacks against the login and registration endpoints.

Account lockout 

Failed authentication attempts are counted per username/IP combination. When the failure count reaches the configured threshold (lockoutThreshold, default: 5), the account is locked for the configured duration (lockoutDurationSeconds, default: 900 seconds / 15 minutes).

Lockout entries are tagged with the username, enabling administrators to unlock specific users via the admin API without affecting other users.

On successful authentication, the lockout counter is reset.

User enumeration prevention 

The login endpoints return identical error responses regardless of whether a username exists. Additionally, requests for non-existent users include a randomized delay (50--150ms via usleep(random_int(50000, 150000))) to normalize response timing and prevent timing-based enumeration.

The authentication service logs only hashed usernames (hash('sha256', $username)) for unknown user attempts.

Credential ownership verification 

Before any credential mutation (rename, remove), the extension verifies that the credential belongs to the requesting user. This prevents unauthorized users from modifying other users' credentials, even if they know the credential UID.

Admin operations verify admin status via BackendUserAuthentication::isAdmin() and record the admin's UID in audit trails.

Last credential protection 

When disablePasswordLogin is enabled, users cannot remove their last remaining passkey. This prevents users from accidentally locking themselves out of the system when password login is disabled.

Signature counter validation 

The WebAuthn signature counter (sign_count) is updated after each successful authentication. The web-auth/webauthn-lib validates that the counter is strictly increasing, which helps detect cloned authenticators.

Soft delete and revocation 

The extension supports two credential removal mechanisms:

  • Soft delete (user-initiated): Sets deleted = 1. The credential record is preserved in the database but excluded from all queries.
  • Revocation (admin-initiated): Sets revoked_at and revoked_by without setting the delete flag. Revoked credentials are explicitly checked and rejected during authentication, providing a clear audit trail of who revoked the credential and when.

Label sanitization 

User-provided passkey labels are sanitized:

  • Trimmed of leading/trailing whitespace
  • Truncated to 128 characters maximum (mb_substr)
  • Empty labels default to "Passkey"

Production deployment requirements 

The extension's security mechanisms depend on certain TYPO3 and server configurations being set correctly. Review each section below before deploying to production.

Trusted hosts pattern 

When rpId and origin are left empty (the default), the extension auto-detects them from the HTTP_HOST server variable. An attacker who can inject an arbitrary Host header could cause the extension to generate challenge tokens bound to a malicious origin.

TYPO3 mitigates this with the trustedHostsPattern setting, but the default value .* allows any host header.

config/system/settings.php
$GLOBALS['TYPO3_CONF_VARS']['SYS']['trustedHostsPattern']
    = '(^|\.)example\.com$';
Copied!

Alternatively, set rpId and origin explicitly in the extension configuration. This bypasses auto-detection entirely and removes the dependency on host header validation for passkey operations.

Reverse proxy and IP detection 

Rate limiting and account lockout use the client's IP address (via GeneralUtility::getIndpEnv('REMOTE_ADDR')). Behind a reverse proxy, all requests appear to originate from the proxy's IP address unless TYPO3 is configured to read the real client IP from forwarded headers.

Without this configuration:

  • Rate limiting becomes ineffective -- all clients share a single counter and hit the limit collectively.
  • Account lockout affects all users -- one locked account blocks authentication for every user behind the same proxy.

Configure TYPO3 to trust your reverse proxy:

config/system/settings.php
// IP address(es) of your reverse proxy (comma-separated)
$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP'] = '10.0.0.1,10.0.0.2';

// Use the last (rightmost) value in X-Forwarded-For
$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyHeaderMultiValue']
    = 'last';
Copied!

Multi-server cache backends 

The extension uses two TYPO3 caches:

  • nr_passkeys_be_nonce -- Stores single-use nonces for challenge replay protection (default backend: SimpleFileBackend).
  • nr_passkeys_be_ratelimit -- Stores rate-limit counters and lockout flags (default backend: FileBackend).

File-based cache backends store data on the local filesystem. In a multi-server deployment (multiple TYPO3 instances behind a load balancer), each server maintains its own independent cache. This has two consequences:

  1. Nonce replay across servers -- A challenge token consumed on server A still exists in server B's cache, allowing a replayed token to pass verification on server B.
  2. Rate-limit bypass -- An attacker can distribute requests across servers, with each server tracking only a fraction of the total attempts.

For multi-server deployments, configure a shared cache backend:

config/system/additional.php
// Use Redis for nonce cache (requires typo3/cms-core Redis backend)
$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']
    ['nr_passkeys_be_nonce']['backend']
    = \TYPO3\CMS\Core\Cache\Backend\RedisBackend::class;
$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']
    ['nr_passkeys_be_nonce']['options'] = [
        'database' => 3,
        'defaultLifetime' => 300,
        // 'hostname' => '127.0.0.1',
        // 'port' => 6379,
        // 'password' => '',
    ];

// Use Redis for rate-limit cache
$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']
    ['nr_passkeys_be_ratelimit']['backend']
    = \TYPO3\CMS\Core\Cache\Backend\RedisBackend::class;
$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']
    ['nr_passkeys_be_ratelimit']['options'] = [
        'database' => 4,
        'defaultLifetime' => 600,
        // 'hostname' => '127.0.0.1',
        // 'port' => 6379,
        // 'password' => '',
    ];
Copied!

Troubleshooting 

"Failed to generate options" / encryptionKey too short 

Symptoms

  • The passkey settings panel shows: "Passkey management is unavailable. The TYPO3 encryption key is missing or too short."
  • The management API returns HTTP 500 with `Failed to generate options: TYPO3 encryptionKey is missing or too short (min 32 chars).`

Error codes

  • 1700000040 (WebAuthnService)
  • 1700000050 (ChallengeService)

Cause

Both HMAC-signed challenge tokens and the WebAuthn credential serialization depend on $GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey']. This key must be at least 32 characters long. Fresh TYPO3 installations that skipped the Install Tool wizard may have an empty or very short key.

Fix

  1. Open Admin Tools > Settings > Configure Installation-Wide Options.
  2. Set [SYS][encryptionKey] to a random string of at least 64 characters. The Install Tool offers a "Generate" button for this.
  3. Alternatively, set it in config/system/settings.php:

    return [
        'SYS' => [
            'encryptionKey' => 'your-random-string-at-least-64-chars...',
        ],
    ];
    Copied!

"Passkeys require a secure connection (HTTPS)" 

Symptoms

The login page shows "Passkeys require a secure connection (HTTPS)." and the passkey button is disabled.

Cause

The WebAuthn specification requires a secure context. Browsers block navigator.credentials.create() and navigator.credentials.get() on plain HTTP origins.

Fix

  • Use HTTPS for your TYPO3 backend. In local development https://localhost or https://*.ddev.site satisfies the requirement.
  • http://localhost is also treated as a secure context by most browsers, but other HTTP origins are not.

Extension log location 

The extension logs passkey events (registration, authentication, errors) via the PSR-3 LoggerInterface. With the default TYPO3 logging configuration, messages are written to:

var/log/typo3_<hash>.log
Copied!

If you have configured a custom log file via $GLOBALS['TYPO3_CONF_VARS']['LOG'], check the path set for the Netresearch\NrPasskeysBe namespace.

Example custom configuration:

$GLOBALS['TYPO3_CONF_VARS']['LOG']['Netresearch']['NrPasskeysBe']['writerConfiguration'] = [
    \TYPO3\CMS\Core\Log\LogLevel::DEBUG => [
        \TYPO3\CMS\Core\Log\Writer\FileWriter::class => [
            'logFile' => \TYPO3\CMS\Core\Core\Environment::getVarPath() . '/log/passkey_auth.log',
        ],
    ],
];
Copied!

Enabling debug mode 

To see full stack traces in error responses (development only):

  1. Open Admin Tools > Settings > Configure Installation-Wide Options.
  2. Set [SYS][displayErrors] to 1.
  3. Set [SYS][devIPmask] to your IP address or *.

Changelog 

0.4.0 

Features 

  • TYPO3 12.4 LTS support (PHP 8.2+ required)
  • Event listener registered via Services.yaml tag for v12 compatibility (#[AsEventListener] attribute retained for v13+)
  • PasskeyInfoElement DI-aware FormEngine node with setData() for v12 NodeFactory compatibility
  • CI matrix expanded with TYPO3 v12.4 test jobs
  • DDEV development environment includes v12 installation

0.3.0 

Features 

  • Inline name input for passkey registration -- users can name their passkey before registering (defaults to "Passkey")
  • Accessible aria-label on the name input field
  • Input is disabled during registration and reset after success

Refactoring 

  • Rewrote PasskeyManagement.js from IIFE to ES module using TYPO3 native APIs: AjaxRequest, Notification, Modal, SeverityEnum, sudoModeInterceptor, DocumentService
  • Replaced PageRenderer::addJsFile() with loadJavaScriptModule()
  • Replaced inline style with CSS class

Fixes 

  • Escape label in removal confirmation modal (XSS prevention)
  • Defer DOM initialization with DocumentService.ready()
  • Resolve AjaxRequest responses and check status before showing success notifications

0.2.0 

Features 

  • Warn about short or missing TYPO3 encryption key in the passkey settings panel (minimum 32 characters required)
  • Include exception details in management API error responses for authenticated users

Documentation 

  • Added Troubleshooting section covering encryption key issues, HTTPS requirements, log location, and debug mode

0.1.0 

Initial release.

Features 

  • Passwordless backend authentication via WebAuthn/FIDO2 Passkeys
  • Passkey button injected into the standard TYPO3 login form via PSR-14 event listener (no login provider switching)
  • Support for TouchID, FaceID, YubiKey, Windows Hello, and other FIDO2-compliant authenticators
  • Authentication service at priority 80 (above standard password service)
  • Authentication data packed into userident field as JSON ($GLOBALS['TYPO3_REQUEST'] is null during auth chain)
  • Credential registration, listing, renaming, and removal for users
  • Admin API for listing, revoking credentials and unlocking accounts
  • HMAC-SHA256 signed challenge tokens with nonce replay protection
  • Per-endpoint rate limiting by IP address
  • Account lockout after configurable failed attempt threshold
  • Discoverable login (usernameless, Conditional UI) behind feature flag
  • Option to disable password login entirely (passkey-only mode)
  • Configurable signing algorithms (ES256, ES384, ES512, RS256)
  • Configurable user verification requirement
  • User enumeration prevention with randomized timing
  • Soft delete and admin revocation with audit trails
  • Signature counter tracking for clone detection
  • Passkey-specific error message on failed login attempts via sessionStorage detection
  • Default audit log writer (WARNING+ to typo3temp/var/log/passkey_auth.log)
  • TYPO3 13.4 LTS and TYPO3 14.x compatibility
  • PHP 8.2, 8.3, 8.4, and 8.5 support
  • Comprehensive test suite (unit, fuzz, functional, JavaScript)
  • PSR-3 logging for all significant events