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
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
PasskeyviaLogin. js PageRenderer::addJsFile() - Injects an inline script with
window.NrPasskeysBeConfigthat providesloginOptionsUrl,rpId,origin, anddiscoverableEnabledto 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 Passkey 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:
{
"_type": "passkey",
"assertion": {"id": "...", "type": "public-key", "response": {}},
"challengeToken": "..."
}
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).
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/listPOST /ajax/passkeys/manage/rename*POST /ajax/passkeys/manage/remove*
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
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 keyaaguid-- Authenticator Attestation GUIDtransports-- JSON array of transport hints
Running tests
# 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