ApiClientKeyTrait
Application key (`X-API-Key`) check on the external Api path.
The key only says "this application may call the Api". It travels together with the user's token: rights are still decided by the JWT (user role) or the guest, and the client may only restrict them (allowed roles, ApiClientContext and PermissionResolver).
Runs in BasePresenter::startup(), right after CORS and before JWT and RBAC:
applyCors(): a preflightOPTIONSends with 204 earlier (the browser sends no key in it).- checkClientKey(): a missing, unknown, disabled, revoked or expired key gives one
identical 401; a valid key is subject to the application-wide limit (1 s + 1 min), then 429
with
Retry-After. checkJwtAuth(): who the user is.- checkClientCallerLimit(): limit of one caller under the key (user or address).
checkApiPermission(): what the caller may do (roles restricted by the client).
ApiBridge::internalCall() calls internalAction*() by reflection and does not run startup(),
so the web and the admin never reach this code and need no key (the RBAC log records them as
client=web).
Actions with #[NoClientKey] (callbacks of payment gateways, the SMS aggregator and the
OAuth provider) skip the key entirely: a third party calls them and cannot send our key.
The list is held by ApiClientKeyExemptionTest.
The switch api.clientKey.required: false returns calls without a key to the pre-key state;
calls with a key are still verified and subject to limits and the role ceiling (otherwise a
bad key would pass as no key).
IMPORTANT: the trait expects JwtAuthTrait on the same class (it provides
$authLogManager) and deliberately does not declare its own copy of that #[Inject]
property, because a duplicate would collide.
Table of Contents
Properties
- $apiClientContext : ApiClientContext
- IMPORTANT: access only through {@see self::apiClientContext()}. A presenter built outside DI (unit tests) lacks it, and reading an uninitialized typed property throws `Error`.
- $apiClientKeyConfig : ApiClientKeyConfig
- $apiClientManager : ApiClientManager
- $apiClientRateLimiter : ApiClientRateLimiter
Methods
- getApiClient() : ApiClientIdentity|null
- Returns the client whose key came with this request; `null` means no key (web or admin through the bridge, a `#[NoClientKey]` exception, or `required: false` without a key).
- checkClientCallerLimit() : void
- Step 4 of an external request: limit of one caller under the key. A logged-in caller is counted by `userId` from the JWT (never from the request), an anonymous one by address.
- checkClientKey() : void
- Step 2 of an external request, see the trait docblock. A rejection ends in `sendJson()` (`AbortException`); otherwise the client is set in the request context.
- isClientKeyExempt() : bool
- Returns whether the current action is exempt from the key (`#[NoClientKey]` on its `action*()` method). An unknown action is not exempt (fail-closed, like {@see JwtAuthTrait::isPublicAction()}).
- apiClientContext() : ApiClientContext|null
- refuseClientKey() : void
- Sends one response for a missing, unknown, disabled, revoked and expired key, so the caller cannot tell which key exists. The reason, client id and key id (`cms_system_api_client_keys`, not the key itself) go only to the log; the key or any part of it never does.
- refuseClientLimit() : void
- Sends 429 with `Retry-After`. Only the first refusal in the window goes to the auth log, so a flood does not flood the log too.
Properties
$apiClientContext
IMPORTANT: access only through {@see self::apiClientContext()}. A presenter built outside DI (unit tests) lacks it, and reading an uninitialized typed property throws `Error`.
public
ApiClientContext
$apiClientContext
Attributes
- #[Inject]
$apiClientKeyConfig
public
ApiClientKeyConfig
$apiClientKeyConfig
Attributes
- #[Inject]
$apiClientManager
public
ApiClientManager
$apiClientManager
Attributes
- #[Inject]
$apiClientRateLimiter
public
ApiClientRateLimiter
$apiClientRateLimiter
Attributes
- #[Inject]
Methods
getApiClient()
Returns the client whose key came with this request; `null` means no key (web or admin through the bridge, a `#[NoClientKey]` exception, or `required: false` without a key).
public
getApiClient() : ApiClientIdentity|null
Return values
ApiClientIdentity|nullcheckClientCallerLimit()
Step 4 of an external request: limit of one caller under the key. A logged-in caller is counted by `userId` from the JWT (never from the request), an anonymous one by address.
protected
checkClientCallerLimit() : void
checkClientKey()
Step 2 of an external request, see the trait docblock. A rejection ends in `sendJson()` (`AbortException`); otherwise the client is set in the request context.
protected
checkClientKey() : void
isClientKeyExempt()
Returns whether the current action is exempt from the key (`#[NoClientKey]` on its `action*()` method). An unknown action is not exempt (fail-closed, like {@see JwtAuthTrait::isPublicAction()}).
protected
isClientKeyExempt() : bool
Return values
boolapiClientContext()
private
apiClientContext() : ApiClientContext|null
Return values
ApiClientContext|nullrefuseClientKey()
Sends one response for a missing, unknown, disabled, revoked and expired key, so the caller cannot tell which key exists. The reason, client id and key id (`cms_system_api_client_keys`, not the key itself) go only to the log; the key or any part of it never does.
private
refuseClientKey(ApiClientKeyStatus $status, int|null $clientId[, int|null $keyId = null ]) : void
Parameters
- $status : ApiClientKeyStatus
- $clientId : int|null
- $keyId : int|null = null
refuseClientLimit()
Sends 429 with `Retry-After`. Only the first refusal in the window goes to the auth log, so a flood does not flood the log too.
private
refuseClientLimit(ApiClientIdentity $client, ApiClientLimitResult $limit) : void
Parameters
- $client : ApiClientIdentity
- $limit : ApiClientLimitResult