---
title: "eID API Reference"
manual: "Passkeys Frontend Authentication"
version: "2.0"
permalink: "https://docs.typo3.org/permalink/netresearch/nr-passkeys-fe:api-reference@2.0"
source: "DeveloperGuide/Api.rst"
rendered: "2026-09-29T22:18:36+00:00"
---

# eID API Reference {#api-reference}

All frontend API endpoints are handled by the eID dispatcher at
`/?eID=nr_passkeys_fe`. The `action` query parameter selects the
controller action. Request bodies are JSON. All responses are JSON with
`Content-Type: application/json`.

## Token-based login flow {#token-based-login-flow}

Both passkey login and recovery code login use a two-phase flow:

1.  **eID verification** -- JavaScript calls the eID endpoint
    (`loginVerify` or `recoveryVerify`). The server verifies the
    assertion or recovery code and stores the authenticated
    `fe_user` UID in a short-lived cache entry
    (`nr_passkeys_fe_nonce` cache, 2-minute TTL). The response
    includes a `loginToken`.
1.  **felogin form submission** -- JavaScript submits a standard
    `logintype=login` form to the current page, including the
    `loginToken` in a hidden field. TYPO3's normal FE authentication
    chain processes the request.
1.  **Auth service** -- `PasskeyFrontendAuthenticationService`
    (priority 80) reads the `loginToken`, looks up the user UID in
    the cache, and returns the `fe_users` row. The token is consumed
    (one-time use).

This ensures users get a proper TYPO3 frontend session with all
middleware (enforcement interstitial, session regeneration) applied.

## Authentication endpoints (public) {#authentication-endpoints-public}

These endpoints do not require a frontend session.

```text
POST /?eID=nr_passkeys_fe&action=loginOptions
```

Request challenge options for passkey login.

**Request:**

```json
{
    "username": "johndoe"
}
```

For discoverable login, omit `username` or send an empty body.

**Response (200):**

```json
{
    "options": {
        "challenge": "...",
        "rpId": "example.com",
        "allowCredentials": []
    },
    "challengeToken": "..."
}
```

---

```text
POST /?eID=nr_passkeys_fe&action=loginVerify
```

Verify a passkey assertion and issue a login token.

**Request:**

```json
{
    "assertion": {
        "id": "...",
        "type": "public-key",
        "response": {
            "clientDataJSON": "...",
            "authenticatorData": "...",
            "signature": "...",
            "userHandle": "..."
        }
    },
    "challengeToken": "..."
}
```

**Response (200):**

```json
{
    "status": "ok",
    "feUserUid": 42,
    "loginToken": "abc123..."
}
```

The `loginToken` is a one-time token valid for 2 minutes. The
JavaScript must submit it via a standard felogin form to complete the
login (see [Token-based login flow](https://docs.typo3.org/permalink/netresearch/nr-passkeys-fe:token-based-login-flow@2.0) above).

## Recovery code endpoint (public) {#recovery-code-endpoint-public}

```text
POST /?eID=nr_passkeys_fe&action=recoveryVerify
```

Login using a one-time recovery code.

**Request:**

```json
{
    "username": "johndoe",
    "code": "XXXX-XXXX"
}
```

**Response (200):**

```json
{
    "status": "ok",
    "feUserUid": 42,
    "loginToken": "abc123..."
}
```

The `loginToken` is consumed via felogin form submission, identical
to the passkey login flow.

## Enrollment endpoints (requires session) {#enrollment-endpoints-requires-session}

```text
POST /?eID=nr_passkeys_fe&action=registrationOptions
```

Request challenge options for passkey enrollment. Requires an active
frontend session.

**Response (200):**

```json
{
    "options": {
        "challenge": "...",
        "rp": {"id": "example.com", "name": "My Site"},
        "user": {"id": "...", "name": "johndoe", "displayName": "John Doe"},
        "pubKeyCredParams": [{"type": "public-key", "alg": -7}]
    },
    "challengeToken": "..."
}
```

---

```text
POST /?eID=nr_passkeys_fe&action=registrationVerify
```

Verify an attestation and save the new credential.

**Request:**

```json
{
    "attestation": {
        "id": "...",
        "type": "public-key",
        "response": {
            "clientDataJSON": "...",
            "attestationObject": "..."
        }
    },
    "challengeToken": "...",
    "name": "My MacBook"
}
```

**Response (200):**

```json
{
    "status": "ok",
    "credentialId": "..."
}
```

## Management endpoints (requires session) {#management-endpoints-requires-session}

```text
GET /?eID=nr_passkeys_fe&action=manageList
```

Returns the list of passkeys for the current user.

```text
POST /?eID=nr_passkeys_fe&action=manageRename
```

**Request:** `{"credentialId": "...", "name": "New Name"}`

```text
POST /?eID=nr_passkeys_fe&action=manageRemove
```

**Request:** `{"credentialId": "..."}`

```text
POST /?eID=nr_passkeys_fe&action=recoveryGenerate
```

Generates a new set of 10 recovery codes. Returns the plaintext codes
(shown once only).

**Response (200):**

```json
{
    "status": "ok",
    "codes": ["XXXX-XXXX", "..."],
    "count": 10
}
```

## Enrollment status endpoints (requires session) {#enrollment-status-endpoints-requires-session}

```text
GET /?eID=nr_passkeys_fe&action=enrollmentStatus
```

Returns the current user's enrollment and enforcement status.

```text
POST /?eID=nr_passkeys_fe&action=enrollmentSkip
```

Skips the enrollment interstitial (only available during grace period
when enforcement level is `required`).

## Action routing summary {#action-routing-summary}

The eID dispatcher routes the `action` query parameter to controllers:

| Action | Controller method | Auth |
| --- | --- | --- |
| `loginOptions` | LoginController::optionsAction | Public |
| `loginVerify` | LoginController::verifyAction | Public |
| `recoveryVerify` | RecoveryController::verifyAction | Public |
| `recoveryGenerate` | RecoveryController::generateAction | Session |
| `registrationOptions` | ManagementController::regOptions | Session |
| `registrationVerify` | ManagementController::regVerify | Session |
| `manageList` | ManagementController::listAction | Session |
| `manageRename` | ManagementController::renameAction | Session |
| `manageRemove` | ManagementController::removeAction | Session |
| `enrollmentStatus` | EnrollmentController::statusAction | Session |
| `enrollmentSkip` | EnrollmentController::skipAction | Session |

## Backend AJAX routes {#backend-ajax-routes}

The admin module calls these routes through `TYPO3.settings.ajaxUrls`.
Each URL carries the route token TYPO3 issues to the backend session, and
every action answers `403` unless the backend user is an administrator.

| Route | Method | Parameters |
| --- | --- | --- |
| `nr_passkeys_fe_admin_list` | GET | `feUserUid` (query argument) |
| `nr_passkeys_fe_admin_remove` | POST | `feUserUid`, `credentialUid` |
| `nr_passkeys_fe_admin_revoke_all` | POST | `feUserUid` |
| `nr_passkeys_fe_admin_unlock` | POST | `feUserUid`, `username` |
| `nr_passkeys_fe_admin_update_enforcement` | POST | `groupUid`, `enforcement` |
| `nr_passkeys_fe_admin_reset_grace_period` | POST | `feUserUid` |

`nr_passkeys_fe_admin_update_enforcement` accepts `off`, `encourage`,
`required` and `enforced`, answers `400` for any other value or a
group uid that is not a plain positive integer, `404` for an unknown or
deleted group, and `409` while the administrator works in a workspace
without live editing (`fe_groups` is not versioned). It writes
`fe_groups.passkey_enforcement` through DataHandler, so the change appears
in the record history and the system log.

`nr_passkeys_fe_admin_reset_grace_period` sets
`fe_users.passkey_grace_period_start` of the user to `0`, so the next
request the enrollment interstitial handles under `required` starts a new
grace period. It answers `400` without a `feUserUid` and `404` for an
unknown user.

## Error responses {#error-responses}

All error responses follow this format:

```json
{
    "error": "Human-readable error message"
}
```

Common HTTP status codes:

| Code | Meaning |
| --- | --- |
| 400 | Invalid request (missing/malformed fields) |
| 401 | Not authenticated (session required) |
| 403 | Forbidden (insufficient privileges) |
| 429 | Rate limit exceeded |
| 500 | Internal server error |
