---
title: "Authentication"
manual: "TCA_API"
version: "main"
permalink: "https://docs.typo3.org/permalink/maikschneider/tca-api:authentication@main"
source: "Configuration/Authentication.rst"
rendered: "2026-10-27T16:14:15+00:00"
---

# Authentication {#authentication}

[Security](https://docs.typo3.org/permalink/maikschneider/tca-api:security@main) decides which caller may perform an operation. This chapter
explains how a client becomes that caller: as a frontend user, a backend user,
or a script holding a key.

tca_api does not log anyone in itself. It reads the identity that TYPO3 or an
installed extension established for the request.

-   [How tca_api sees identity](https://docs.typo3.org/permalink/maikschneider/tca-api:how-tca-api-sees-identity@main)
-   [Browser and single-page apps](https://docs.typo3.org/permalink/maikschneider/tca-api:browser-and-single-page-apps@main)
-   [Scripts and machine-to-machine](https://docs.typo3.org/permalink/maikschneider/tca-api:scripts-and-machine-to-machine@main)
-   [Other extensions](https://docs.typo3.org/permalink/maikschneider/tca-api:other-extensions@main)
-   [Swagger UI](https://docs.typo3.org/permalink/maikschneider/tca-api:swagger-ui@main)

## How tca_api sees identity {#authentication-identity}

Before any access check, tca_api resolves the caller of the request, the
**principal**, through a chain of authenticators:

| Authenticator | Identity |
| --- | --- |
| API keys | A key sent as `X-API-Key` or `Authorization: Bearer`. Only registered when the [api_key](https://github.com/maikschneider/api-key) extension is installed, see [Scripts and machine-to-machine](https://docs.typo3.org/permalink/maikschneider/tca-api:authentication-api-key@main). |
| Session | The frontend user and the backend user that TYPO3 restored from their session cookies (`fe_typo_user`, `be_typo_user`). Always active. |
| Custom | Any `AuthenticatorInterface` of another extension, see [Authenticators](https://docs.typo3.org/permalink/maikschneider/tca-api:authenticators@main). |

Credentials sent with the request are checked before the session. A request
without any credentials is anonymous and can only reach `PUBLIC` operations.

The two error codes mean different things:

| Status | Meaning |
| --- | --- |
| `401 Unauthorized` | Credentials were sent, but they are invalid: a wrong, expired or revoked key, or a key used from a forbidden IP address or site. The `WWW-Authenticate` header names the scheme. The request is rejected even if a valid session exists. |
| `403 Forbidden` | The caller is anonymous, or authenticated but not allowed: the role does not match, the record belongs to another user, or the key's scopes do not cover the operation. |

> [!NOTE]
> An anonymous request to a protected operation answers `403`, not
> `401`. `401` always means "your credentials are wrong", so a client can
> tell a broken key from a missing permission.

## Browser and single-page apps {#authentication-browser}

In the browser the regular TYPO3 login is the authentication. A user who
logged in through [felogin](https://docs.typo3.org/c/typo3/cms-felogin/main/en-us/)
holds a frontend session cookie, and every API request that carries the cookie
acts as that user. A backend editor who is logged in to the TYPO3 backend in
the same browser also carries the backend session cookie and satisfies
`BE_USER`, and `BE_ADMIN` for administrators.

On the same origin nothing has to be configured: `fetch()` sends the cookies
of the own origin by default.

### Cross-origin clients {#cross-origin-clients}

A single-page app on another origin needs three things:

1.  Allow credentials and name the app's origin in the site settings:

    **config/sites/\<site>/settings.yaml**

    ```yaml
    tca_api:
      corsEnabled: true
      corsOrigin: 'https://app.example.com'
      corsAllowCredentials: true
    ```

    > [!WARNING]
    > With `corsOrigin: '*'` and `corsAllowCredentials` enabled, tca_api
    > echoes the request's `Origin` back. Any website can then read the
    > API on behalf of a logged-in visitor. Always name the origin.
1.  Send the cookies with every request:

    ```javascript
    const response = await fetch('https://www.example.com/_api/articles', {
        credentials: 'include',
    });
    ```
1.  Make sure the browser sends the cookie at all. The frontend session cookie
    defaults to `SameSite=Lax`, which browsers only send to the *same site*
    (`app.example.com` and `www.example.com` are the same site). For an
    app on a different domain, the cookie must be `SameSite=None`, which
    requires HTTPS:

    **config/system/additional.php**

    ```php
    $GLOBALS['TYPO3_CONF_VARS']['FE']['cookieSameSite'] = 'none';
    ```

    This affects every frontend session of the installation, so prefer
    hosting the app on a subdomain of the site.

### Logging in from a script {#logging-in-from-a-script}

Logging in a frontend user programmatically, e.g. with curl, is impractical.
Since TYPO3 v13 every frontend login has to carry a signed request token
(`__RequestToken`) that is bound to a nonce cookie. A script would first
have to load a page with the felogin form, extract the token, keep the cookie
jar and then post the credentials. For scripts and server-to-server calls use
API keys instead.

## Scripts and machine-to-machine {#authentication-api-key}

The [api_key](https://github.com/maikschneider/api-key) extension
([documentation](https://docs.typo3.org/p/maikschneider/api-key/main/en-us/))
adds stateless API keys. tca_api suggests it but does not require it; once it
is installed, tca_api picks it up automatically.

```bash
composer require maikschneider/api-key
```

Create a key on the command line. The key is printed once and cannot be
recovered:

```bash
# Service key: no user, satisfies AccessRole::API_KEY
vendor/bin/typo3 apikey:create --title="CI deploy" --scope=articles:write --expires="+90 days"

# Acts as frontend user 12, with its groups
vendor/bin/typo3 apikey:create --title="Partner import" --fe-user=12

# Acts as backend user 3
vendor/bin/typo3 apikey:create --title="Editorial sync" --be-user=3
```

Send it in either header:

```bash
curl -H "X-API-Key: ${TYPO3_API_KEY}" https://example.com/_api/articles
curl -H "Authorization: Bearer ${TYPO3_API_KEY}" https://example.com/_api/articles
```

What a key may do depends on what it is bound to:

| Key | Satisfies |
| --- | --- |
| Service key | `API_KEY` |
| Bound to a frontend user | `FE_USER`, `FE_GROUP`, `OWNER` and `API_KEY` |
| Bound to a backend user | `BE_USER`, `BE_ADMIN` (for admins) and `API_KEY` |

The key's scopes always restrict it, also for a key bound to a user, see
[Scopes](https://docs.typo3.org/permalink/maikschneider/tca-api:scopes@main). A key without scopes is unrestricted, so give every key the
narrowest scopes it needs.

Keyed requests are stateless: they create no `fe_sessions` row, send no
`Set-Cookie` and leave `lastlogin` untouched.

> [!TIP]
> Behind Apache, the `Authorization` header only reaches PHP with
> `CGIPassAuth On`. The `X-API-Key` header always works.

How the bridge maps keys to principals, adds `X-API-Key` to the CORS
headers and documents the security schemes is described in
[API keys](https://docs.typo3.org/permalink/maikschneider/tca-api:authenticators-api-key@main).

## Other extensions {#authentication-extensions}

Extensions that log a user in through TYPO3's authentication services work
as well, because tca_api reads the resulting session:

| Extension | Notes |
| --- | --- |
| [kuhschnappel/authtoken](https://extensions.typo3.org/extension/authtoken) | Tokens for frontend users, sent in an `X-User-Token` header. Supports TYPO3 up to v13.4. It relies on `FE_alwaysFetchUser`, so every cookie-less request creates an `fe_sessions` row and sends `Set-Cookie`. |
| [r3h6/oauth2-server](https://packagist.org/packages/r3h6/oauth2-server) | An OAuth2 server for frontend users. Supports TYPO3 up to v13.4. |

> [!WARNING]
> **Attention**
>
> [in2frontendauthentication](https://extensions.typo3.org/extension/in2frontendauthentication)
> does **not** satisfy `FE_USER` or `FE_GROUP`. It adds frontend groups
> for matching IP addresses but never logs in a user record, so the request
> stays without a frontend user.

Session-based extensions that only log in backend users, such as OAuth2 or
OpenID Connect login providers for the backend, apply to browsers, not to
scripts: the frontend only restores a backend user when the backend session
cookie is present.

## Swagger UI {#authentication-swagger-ui}

Swagger UI at `{apiPrefix}swagger-ui` offers the authentication schemes of
the OpenAPI document in its **Authorize** dialog:

-   **Sessions** (`FrontendSession`, `BackendSession`): log in on the
    website or in the backend in the same browser and reload Swagger UI. The
    browser sends the session cookie by itself.
-   **API keys** (`ApiKey`, `ApiKeyBearer`): click **Authorize** and paste
    a key. The key is kept across reloads until you click **Logout**.
