Authenticators 

Before any access check, the dispatcher resolves the caller of the request, the principal, through a chain of authenticators. Access roles, ownership and the write audit trail all read this principal, so an extension can add a new way to authenticate (API keys, tokens) without touching access control.

For the client side, which credentials to send and which extensions provide them, see Authentication.

Built in is the SessionAuthenticator: the frontend and backend user that TYPO3's authentication middlewares restored from their session cookies. A request without any credentials is anonymous.

API keys 

When the api_key extension is installed (composer require maikschneider/api-key), tca_api registers an ApiKeyAuthenticator (priority 10). It accepts a key sent as X-API-Key: t3key_… or Authorization: Bearer t3key_… and maps it to a principal with source = 'api_key':

Key Principal
Service key (no user) service principal; satisfies API_KEY only
Bound to a frontend user that frontend user with its groups; satisfies FE_USER, FE_GROUP, OWNER and API_KEY
Bound to a backend user that backend user; satisfies BE_USER, BE_ADMIN (for admins) and API_KEY
Invalid, expired, revoked, IP- or site-restricted 401 Unauthorized, never anonymous

The bound user is read from the database, not logged in: keyed requests create no fe_sessions row, send no Set-Cookie and leave lastlogin untouched. The key's scopes become the principal's scopes, so a key bound to a user can never do more than its scopes allow. A key without scopes is unrestricted. Bearer tokens without the t3key_ prefix are left to other authenticators.

The bridge also adds X-API-Key to Access-Control-Allow-Headers and documents the ApiKey and ApiKeyBearer security schemes in the OpenAPI document. Without the extension none of this is registered.

Interface 

namespace MaikSchneider\TcaApi\Security\Authentication;

use Psr\Http\Message\ServerRequestInterface;

interface AuthenticatorInterface
{
    public function authenticate(ServerRequestInterface $request): AuthenticationResult;

    public function getPriority(): int;
}
Copied!

An authenticator that reads a custom request header implements RequestHeaderAuthenticatorInterface instead; its getRequestHeaders() are added to Access-Control-Allow-Headers, so browser clients can send them through a CORS preflight.

Every implementation in any extension is registered automatically. The chain asks the authenticators in descending priority and stops at the first one that is applicable. SessionAuthenticator has priority -10, so credentials sent with the request are checked before a session.

authenticate() returns one of three results:

Result Effect
AuthenticationResult::notApplicable() The request carries no credentials of this kind; the chain continues.
AuthenticationResult::authenticated($principal) The principal is used for this request.
AuthenticationResult::failed($challenge, $reason) Credentials were sent but are invalid. The request is answered with 401 Unauthorized and WWW-Authenticate: $challenge, even if a session exists.

The principal 

Principal can hold a frontend user, a backend user, or both (a browser may hold both sessions). A service principal has neither: it is an authenticated caller without a TYPO3 user record.

Satisfies Principal
FE_USER, OWNER frontendUserUid > 0
FE_GROUP frontendUserUid > 0 and a matching frontendGroupIds entry
BE_USER backendUserUid > 0
BE_ADMIN backendUserUid > 0 and isBackendAdmin
API_KEY any authenticated principal whose source is not session

Writes record the frontend user, otherwise the backend user, otherwise the service as actor. credential is a safe-to-log identifier of the credential the request carried (e.g. api_key:12:t3key_AbCdEfGh); the write audit log records it next to the actor. scopes lists the granted scopes, null means unrestricted. See Scopes.

Example 

use MaikSchneider\TcaApi\Security\Authentication\AuthenticationResult;
use MaikSchneider\TcaApi\Security\Authentication\AuthenticatorInterface;
use MaikSchneider\TcaApi\Security\Authentication\Principal;
use Psr\Http\Message\ServerRequestInterface;

final class PartnerTokenAuthenticator implements AuthenticatorInterface
{
    public function authenticate(ServerRequestInterface $request): AuthenticationResult
    {
        $token = $request->getHeaderLine('X-Partner-Token');
        if ($token === '') {
            return AuthenticationResult::notApplicable();
        }

        $partner = $this->partners->findByToken($token);
        if ($partner === null) {
            return AuthenticationResult::failed('X-Partner-Token realm="api"');
        }

        return AuthenticationResult::authenticated(new Principal(
            frontendUserUid: $partner->frontendUserUid,
            source: 'partner_token',
        ));
    }

    public function getPriority(): int
    {
        return 0;
    }
}
Copied!

Reading the principal 

The resolved principal is the tca_api.principal request attribute. Principal::fromRequest($request) returns it, and falls back to the session for requests that did not pass the dispatcher:

$principal = Principal::fromRequest($request);
if ($principal->isBackendAdmin()) {
    // …
}
Copied!

Documenting the scheme in OpenAPI 

Add the scheme your clients use in a listener for ModifyOpenApiSecuritySchemesEvent. The SecurityScheme flags tell the document which operations it applies to: each operation lists, as alternatives, every scheme that can satisfy its access role, and a credential scheme also lists the scopes the operation needs. Swagger UI then offers the scheme in its "Authorize" dialog.

use MaikSchneider\TcaApi\Event\ModifyOpenApiSecuritySchemesEvent;
use MaikSchneider\TcaApi\OpenApi\SecurityScheme;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener('my-extension/partner-token-scheme')]
final class AddPartnerTokenScheme
{
    public function __invoke(ModifyOpenApiSecuritySchemesEvent $event): void
    {
        $event->addScheme(new SecurityScheme(
            'PartnerToken',
            ['type' => 'apiKey', 'in' => 'header', 'name' => 'X-Partner-Token'],
            authenticatesFrontendUsers: true,
            isCredential: true,
        ));
    }
}
Copied!

The resulting requirements:

Access role security
PUBLIC [] (anonymous)
FE_USER, FE_GROUP schemes with authenticatesFrontendUsers
OWNER schemes with authenticatesFrontendUsers; schemes with authenticatesBackendUsers only while ownership.beAdminBypass is enabled
BE_USER, BE_ADMIN schemes with authenticatesBackendUsers
API_KEY schemes with isCredential
callable voter every scheme, plus {} (anonymous)
DISABLED no requirement

Protected operations also document a 401 response. The Hydra API documentation has no equivalent of security schemes and is unchanged.