---
title: "Security"
manual: "Passkeys Backend Authentication"
version: "1.0"
permalink: "https://docs.typo3.org/permalink/netresearch/nr-passkeys-be:security@1.0"
source: "Security/Index.rst"
rendered: "2026-09-20T16:07:13+00:00"
---

# Security {#security}

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

## WebAuthn security model {#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 {#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)`
1.  An **expiration timestamp** (configurable TTL, default
    120 seconds)
1.  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 {#nonce-replay-protection}

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

1.  The nonce is looked up in the cache.
1.  If found, it is immediately invalidated (removed from
    cache).
1.  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 {#rate-limiting}

### Per-endpoint rate limiting {#per-endpoint-rate-limiting}

Each API endpoint tracks request counts per IP address. When
the configured threshold ([rateLimitMaxAttempts](https://docs.typo3.org/permalink/netresearch/nr-passkeys-be:confval-ratelimitmaxattempts@1.0),
default: 10) is exceeded within the time window
([rateLimitWindowSeconds](https://docs.typo3.org/permalink/netresearch/nr-passkeys-be:confval-ratelimitwindowseconds@1.0), default: 300 seconds),
the endpoint returns HTTP 429 (Too Many Requests).

This limits automated attacks against the login and
registration endpoints.

### Account lockout {#account-lockout}

Failed authentication attempts are counted per username/IP
combination. When the failure count reaches the configured
threshold ([lockoutThreshold](https://docs.typo3.org/permalink/netresearch/nr-passkeys-be:confval-lockoutthreshold@1.0), default: 5), the
account is locked for the configured duration
([lockoutDurationSeconds](https://docs.typo3.org/permalink/netresearch/nr-passkeys-be:confval-lockoutdurationseconds@1.0), default: 900 seconds /
15 minutes).

A separate per-username threshold
([lockoutUserThreshold](https://docs.typo3.org/permalink/netresearch/nr-passkeys-be:confval-lockoutuserthreshold@1.0), default: 15) counts
failures across all IPs. This prevents distributed brute
force attacks where requests come from many different IP
addresses.

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 {#user-enumeration-prevention}

The login endpoints return identical responses regardless of
whether a username exists: an unknown username receives decoy
credentials with the same shape and HTTP status as a real
user's, derived deterministically from the username so
repeated requests stay consistent. An existing user who has
not registered a passkey yet receives the same decoys, since
an empty credential list would otherwise prove the account
exists.

Response timing is normalized by padding **every**
username-first response -- known and unknown alike -- to the
same wall-clock budget (150ms) before returning. A delay on
the unknown-user branch alone would be the signal rather than
a mask.

> [!NOTE]
> Padding assumes the real work stays below the budget. On a
> heavily loaded system a response that exceeds 150ms is
> returned immediately and remains distinguishable, so
> per-IP rate limiting stays the primary control against
> bulk enumeration.

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

## Credential ownership verification {#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 {#last-credential-protection}

When [disablePasswordLogin](https://docs.typo3.org/permalink/netresearch/nr-passkeys-be:confval-disablepasswordlogin@1.0) 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 {#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 {#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 {#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"

> [!NOTE]
> **See also**
>
> [Production deployment requirements](https://docs.typo3.org/permalink/netresearch/nr-passkeys-be:security-deployment@1.0) for trusted hosts pattern,
> reverse proxy configuration, and multi-server cache
> backends.
