.. include:: ../Includes.rst.txt .. _security: ======== 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 (:confval:`rateLimitMaxAttempts`, default: 10) is exceeded within the time window (:confval:`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 (:confval:`lockoutThreshold`, default: 5), the account is locked for the configured duration (:confval:`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 :confval:`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" .. _security-deployment: 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. .. _security-trusted-hosts: Trusted hosts pattern --------------------- When :confval:`rpId` and :confval:`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. .. code-block:: php :caption: config/system/settings.php $GLOBALS['TYPO3_CONF_VARS']['SYS']['trustedHostsPattern'] = '(^|\.)example\.com$'; Alternatively, set :confval:`rpId` and :confval:`origin` explicitly in the extension configuration. This bypasses auto-detection entirely and removes the dependency on host header validation for passkey operations. .. _security-reverse-proxy: 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: .. code-block:: php :caption: 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'; .. 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. .. _security-multi-server-caching: 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: .. code-block:: php :caption: 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' => '', ]; .. note:: 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.