CoolCMS API

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:

  1. applyCors(): a preflight OPTIONS ends with 204 earlier (the browser sends no key in it).
  2. 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.
  3. checkJwtAuth(): who the user is.
  4. checkClientCallerLimit(): limit of one caller under the key (user or address).
  5. 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]

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|null

checkClientCallerLimit()

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
bool

refuseClientKey()

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
On this page

Search results