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.
The passkey button appears below the Login button with an
"or" divider.
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:
The extension adds a tx_nrpasskeysbe_credential table. After
activation, run the database schema update:
Go to Admin Tools > Maintenance > Analyze Database
Structure
Apply the suggested changes
Or use the CLI:
vendor/bin/typo3 database:updateschema
Copied!
Verify the installation
After activation:
The TYPO3 backend login page should show a
Sign in with a passkey button below the Login button.
The passkey button appears below the Login button, separated
by an "or" divider.
In User Settings, a "Passkeys" section should appear
where authenticated users can register their first passkey.
Note
HTTPS is mandatory for WebAuthn to function. The only exception is
localhost for local development. If you are running TYPO3
behind a reverse proxy, ensure that the TYPO3_SSL environment
variable or the [SYS][reverseProxySSL] configuration is set
correctly.
Configuration
All settings are managed through the TYPO3 Extension Configuration module:
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.
Important
Once passkeys are registered against a specific rpId, changing it
will invalidate all existing registrations. Users would need to register
new passkeys.
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
Enforce passkey-only authentication on a per-user basis. When enabled,
password login is blocked only for users who have registered at least
one passkey. Users without passkeys can still log in with a password,
allowing gradual migration without lockouts.
This enables a smooth onboarding workflow:
Admin creates a new backend user with a password (as usual).
User logs in with password, registers a passkey in User Settings.
From that point on, the user must use their passkey -- password login
is no longer accepted for that account.
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.
Deployment Scenarios
Passkeys are bound to a specific domain (the Relying Party ID). This
chapter explains how to configure the extension across different
environments and how to handle common deployment patterns.
The simplest setup: one TYPO3 instance with one domain.
Leave rpId and origin empty (the default). The
extension auto-detects both values from the incoming HTTP request. Each
passkey is registered against the domain it was created on.
This works for:
A single production instance (e.g. cms.example.com)
A local DDEV site (e.g. mysite.ddev.site)
No additional configuration is needed.
Multi-environment (local / staging / production)
A typical setup has three environments:
Local development: mysite.ddev.site (or mysite.local)
Staging: staging.example.com
Production: www.example.com
Recommended: separate passkeys per environment
Leave rpId empty on all environments. Each environment
auto-detects its own domain, so passkeys are environment-specific. Users
register a separate passkey on each environment they need access to.
Modern authenticators (iCloud Keychain, Windows Hello, 1Password,
YubiKey) make registering on multiple environments trivial -- it takes
about 10 seconds per environment.
Tip
This is the recommended approach. It avoids sharing secrets across
environments and keeps each environment fully independent.
Environment-specific configuration
Use TYPO3_CONTEXT to apply different settings per environment:
config/system/additional.php
// Production and Staging: enforce passkey-only loginif (str_starts_with((string)getenv('TYPO3_CONTEXT'), 'Production')) {
$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['nr_passkeys_be']['disablePasswordLogin'] = '1';
}
// Development: keep password login available for convenience// (disablePasswordLogin defaults to '0', no override needed)
Copied!
This lets you enforce passkey-only login on production while keeping
password login available locally for new users or quick access.
Verify all regular backend users have registered at least one
passkey.
Ensure at least one admin account has multiple passkeys on different
authenticators for emergency recovery.
Communicate the change to all backend users in advance.
Consider enabling on staging first to verify the workflow.
Database synchronisation
When syncing the production database to staging or local (a common
workflow), the passkey credential table will contain credentials bound to
the production domain. These credentials will not work on a different
domain.
Example: exclude in DDEV (via mysql-sync-db custom command)
ignore_tables:-tx_nrpasskeysbe_credential
Copied!
After importing a production database dump, users simply register fresh
passkeys on the local or staging environment. If
disablePasswordLogin is active but environment-specific (see
above), password login is available on non-production environments for
this initial registration.
Note
You do not need to exclude be_users or any other table. Only
tx_nrpasskeysbe_credential is domain-specific. If the credential
table is accidentally included in a sync, the imported credentials
will not work on the different domain -- users simply register fresh
passkeys. No security data is exposed because the public keys are
useless without the private keys stored on users' authenticators.
Shared rpId across subdomains
WebAuthn allows the rpId to be set to a registrable domain
suffix. For example, setting rpId to example.com allows passkeys
registered on staging.example.com to also work on
www.example.com.
Warning
Sharing passkeys across environments is not recommended. It
requires synchronising:
The TYPO3encryptionKey
($GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey']) -- the
extension derives user handles from it cryptographically. Different
keys produce different handles, making credentials unresolvable.
The credential table (tx_nrpasskeysbe_credential) -- the
public key material and metadata must be present on both systems.
The backend user UIDs (be_users.uid) -- user handles are
derived from the UID.
Sharing the encryptionKey between environments creates a
cross-environment attack vector: if a staging environment is
compromised, the attacker can forge CSRF tokens, session tokens, and
passkey challenge tokens that are valid on production. Staging
environments typically have weaker access controls and debug mode
enabled, making them a more attractive target. The encryptionKey
must be unique per environment and treated as a production secret.
If you still need shared subdomains (e.g. staging and www), set
rpId only on those environments and keep local development on
auto-detect:
config/system/additional.php
if (str_starts_with((string)getenv('TYPO3_CONTEXT'), 'Production')) {
$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['nr_passkeys_be']['rpId'] = 'example.com';
// Leave 'origin' empty -- it is auto-detected per subdomain.// Setting it explicitly would break verification on other subdomains.
}
// Development: rpId stays empty -> auto-detect -> mysite.ddev.site
Copied!
Important
Changing the rpId invalidates all existing passkey
registrations. Users must register new passkeys after the change.
User onboarding
Onboarding workflow with disablePasswordLogin
When disablePasswordLogin is enabled, the extension enforces
passkey-only login per user: password login is blocked only for users
who have at least one registered passkey. Users without passkeys can
still log in with a password.
This enables a smooth onboarding workflow:
Admin creates a new backend user with a password (as usual in
TYPO3).
User logs in with their password for the first time.
User registers a passkey in User Settings > Passkeys.
From this point on, the user must use their passkey -- password
login is no longer accepted for this account.
Note
An admin cannot register a passkey on behalf of another user. The
WebAuthn ceremony requires physical interaction with the user's own
authenticator (TouchID, YubiKey, etc.).
Recovery scenarios
If a user loses access to their authenticator:
An admin revokes the user's passkeys via the
Admin API. Each revocation is recorded with
the admin's UID and timestamp for audit purposes.
Once all passkeys are revoked, password login becomes available again
for that user (the per-user enforcement lifts when no active
credentials remain).
The user logs in with their password and registers a new passkey.
Tip
Consider requiring users to register at least two passkeys on
different authenticators (e.g. laptop + phone) for redundancy.
Containerized and multi-server deployments
When running TYPO3 in Docker containers or behind a load balancer,
the file-based cache backends lose state on container restart and are
not shared across servers. This affects nonce replay protection and
rate limiting.
DDEV sites (*.ddev.site) use HTTPS by default and are treated as
secure contexts by browsers. Passkeys work out of the box.
ddev start
# Open https://mysite.ddev.site/typo3 -- passkeys work immediately
Copied!
For http://localhost (without HTTPS), most browsers also treat this
as a secure context, so passkeys will work. However, custom local
domains over plain HTTP (e.g. http://mysite.local) will not
work -- WebAuthn requires a secure context.
The browser may automatically show available passkeys in an
autofill dropdown (Conditional UI).
Select your passkey.
Verify with your authenticator.
You are logged in without typing a username.
Note
Discoverable login requires that the passkey was registered as a
resident credential (stored on the authenticator). Most modern
authenticators do this by default.
Your browser will prompt you to verify with your authenticator.
Upon successful verification, you are logged in.
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:
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.
Important
If disablePasswordLogin is enabled, you cannot remove
your last remaining passkey. This prevents you from locking
yourself out of the system.
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 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:
Registered -- The credential is created via the management API and
stored in the tx_nrpasskeysbe_credential table.
Active -- The credential is used for successful logins. The
last_used_at and sign_count fields are updated on each use.
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.
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:
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
Important
$GLOBALS['TYPO3_REQUEST'] is null during the TYPO3 auth
service chain. Custom POST fields are inaccessible. The only data
available is $this->login with keys status, uname,
uident, and uident_text.
The passkey assertion and challenge token are packed into the
userident field as JSON:
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.
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.
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:
A 32-byte random challenge generated by random_bytes(32)
An expiration timestamp (configurable TTL, default 120 seconds)
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:
The nonce is looked up in the cache.
If found, it is immediately invalidated (removed from cache).
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.
Warning
You must configure trustedHostsPattern in production. Leaving it at
the default .* disables host header validation entirely.
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!
Tip
If you use a CDN or cloud load balancer (e.g. AWS ALB, Cloudflare), ensure
the X-Forwarded-For header chain is properly configured and that TYPO3's
reverseProxyIP matches the load balancer's egress IP range.
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:
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.
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:
Single-server deployments (including DDEV and most small-to-medium
installations) work correctly with the default file-based backends.
This only applies when multiple application servers share the same domain.
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
Open Admin Tools > Settings > Configure Installation-Wide Options.
Set [SYS][encryptionKey] to a random string of at least 64 characters.
The Install Tool offers a "Generate" button for this.
Alternatively, set it in config/system/settings.php:
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.
To see full stack traces in error responses (development only):
Open Admin Tools > Settings > Configure Installation-Wide Options.
Set [SYS][displayErrors] to 1.
Set [SYS][devIPmask] to your IP address or *.
Warning
Never enable displayErrors on production systems.
Detailed error output may expose sensitive configuration details.
Changelog
0.5.0
Features
Per-user password login enforcement: disablePasswordLogin now blocks
passwords only for users who have registered passkeys, enabling gradual
onboarding without locking out new users
Deployment Scenarios documentation chapter covering multi-environment
setup, database sync, user onboarding, and local DDEV development
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