Developer guide
APIs, middlewares and authentication services verify keys through one
service,
\Maik.
use MaikSchneider\ApiKey\Authentication\ApiKeyAuthenticatorInterface;
final class MyApiController
{
public function __construct(
private readonly ApiKeyAuthenticatorInterface $apiKeyAuthenticator,
) {}
public function handle(ServerRequestInterface $request): ResponseInterface
{
$result = $this->apiKeyAuthenticator->authenticate($request);
if ($result->isInvalid()) {
return new JsonResponse(['error' => 'Unauthorized'], 401, [
'WWW-Authenticate' => 'Bearer realm="api"',
]);
}
if ($result->isValid()) {
$apiKey = $result->getApiKey();
// $apiKey->scopes, $apiKey->frontendUserUid, ...
}
// no key: try other authentication or continue anonymously
}
}
Reading the result in the frontend
In the frontend you don't need to call the service yourself. The middleware attaches the result to the request, so middlewares placed after it and controllers read it:
use MaikSchneider\ApiKey\Middleware\ApiKeyAuthentication;
$result = ApiKeyAuthentication::getResult($request);
// or: $request->getAttribute('api_key')
Three outcomes
authenticate returns an
Authentication in one of three
states:
- Not present (
isisPresent () false) - The request carries no key of this extension. Try other authentication
mechanisms or treat the request as anonymous. A bearer token without the
t3key_prefix counts as not present, so OAuth2 or JWT tokens reach their own authenticator. - Valid (
is)Valid () getreturns the authenticated key.Api Key () - Invalid (
is)Invalid () - A key was presented but rejected. Answer
401and never fall back to anonymous access.gettells why, for logging only; send clients the same response for every reason.Invalid Reason ()
Invalid reasons
| Reason | Cause |
|---|---|
Malformed | The X- header or a t3key_ bearer token
is not a well-formed key, e.g. followed by more text
or comma-joined with another token |
Ambiguous | Two different keys, a repeated X- header,
or a t3key_ bearer token in one of several
Authorization headers |
Key | A key appears in the query string. Query strings end
up in access logs and Referer headers |
Unknown | No key with this hash exists |
Revoked | The key record is hidden or deleted |
Expired | The expiry date has passed |
Ip | The client IP is outside the key's allowed ranges |
Site | The request targets a site the key is not allowed for |
User | The bound frontend or backend user is deleted, disabled or outside its start and end time |
Misconfigured | Scopes, allowed IPs or sites are filled but contain no value, e.g. only commas |
The key object
\Maik never carries the secret
or its hash.
| Property | Type | Description |
|---|---|---|
uid | int | Record uid |
title | string | Label of the key |
identifier | string | Prefix of the key, safe to log |
scopes | list<string> | Opaque to this extension; empty means unrestricted |
frontend | ?int | Bound fe_ record |
backend | ?int | Bound be_ record |
expires | ?Date | Expiry, null for none |
The extension does not log users in. Map the bound user to your own authorization model.
Restrictions
The authenticator enforces the restrictions stored with a key, and rejects the key if one fails:
- Expiry
- The key is invalid from its expiry date on (
Expired). - Allowed IPs
- The client address must match one of the key's IP addresses or CIDR
ranges (
Ip). The address comes from the request'sNot Allowed normalizedattribute, which resolves the client behind proxies listed inParams $GLOBALS. Without that attribute['TYPO3_ CONF_ VARS'] ['SYS'] ['reverse Proxy IP'] REMOTE_is used.ADDR - Sites
- The request's
siteattribute must be one of the key's sites (Site). CallNot Allowed authenticateafter the site is resolved, e.g. in a middleware placed after() typo3/. A request without a site fails a site-restricted key.cms- frontend/ site - Bound user
- A key bound to a frontend or backend user stops working while that user
is deleted, disabled or outside its start and end time
(
User). The creator of a key (Unavailable created_) is only recorded and does not affect authentication.by
Scopes are not checked by the authenticator, because only the consumer knows which scope an endpoint needs:
if (!$apiKey->hasScope('articles:write')) {
return new JsonResponse(['error' => 'Forbidden'], 403);
}
A key without scopes has every scope. Scopes are compared exactly. Scopes,
allowed IPs and sites are separated by line breaks or commas. A record whose
field holds only separators is rejected (Misconfigured) instead of being
treated as unrestricted.
Side effects
Authenticating a request creates no session, sends no cookie and does not
update the user record. The only write is the key's last_, at most
once every five minutes per key. Rejected keys are logged as warnings with the
reason, the identifier and the client IP, never with the key itself.
Caching
A response to an authenticated request depends on the key, so it must not be
shared with other clients. For a valid key, the middleware disables the page
cache for the request through the frontend. request
attribute. TYPO3 then neither stores the page nor serves a cached one, and
sends Cache-.
If you call the authenticator yourself, take care of caching yourself:
-
In the frontend rendering chain, disable the page cache before the page is rendered:
$cacheInstruction = $request->getAttribute('frontend.cache.instruction', new CacheInstruction()); $cacheInstruction->disableCache('EXT:my_extension: Authenticated with an API key'); $request = $request->withAttribute('frontend.cache.instruction', $cacheInstruction);Copied! - Don't let a key-dependent response enter a response cache that other clients share, or include the key uid in its cache identifier.
- Send
Cache-or aControl: private, no- store Varyheader that coversX-andAPI- Key Authorization, so reverse proxies and CDNs don't store the response for everyone.