Plug that admits a request to the machine API: steps 1 to 3 of the request
flow (API switch, refusal of a credential in the URL, authentication;
PhoenixKit.API.Executor runs the rest). The order below is the order of
the code.
Every transport mounts it first, with the audience of the endpoint:
plug PhoenixKit.API.Auth, audience: :adminIn order, it:
- assigns
:phoenix_kit_api_request_id: thex-request-idresponse header whenPlug.RequestIdset one (an id the client sent is reused there) and it matches\A[A-Za-z0-9_=+/-]+\z; any other id, and no id at all, gets a fresh random one. The id goes into problem bodies and log lines, so nothing else is let through; - answers
404when the audience's switch is off (PhoenixKit.API.Config.admin_enabled?/0,public_enabled?/0; a settings read error counts as off), and, for the public audience, while the website password gate is on and the client's address is not on the gate's allow-list. Nothing else is done for such a request: no database read beyond the cached settings; - refuses a credential in the URL with
400 token_in_url, before any authentication (token_in_url?/1); - for the admin audience, authenticates the
Authorization: Bearer pk_...key withPhoenixKit.API.Keys.verify/2(always, whatever the state of the IP's failure bucket). For the public audience it assigns an anonymous actor.
An optional :deprecated option ([path: ..., successor: ...], set by the
forward of a deprecated path, see PhoenixKit.API.Paths) marks every answer
from step 3 on with Deprecation and a successor Link. The bare 404 of
step 2 does not carry it: a switched-off or gated API reveals nothing.
An optional :before_auth hook, {module, function}, is called with the
conn after step 3 and before step 4. The MCP transport uses it to refuse a
foreign Origin. The function must return a conn; a halted conn ends the
request, so a refusal costs no key lookup and is not charged to the
failed-authentication bucket. A hook that raises answers 500 internal_error.
On success it assigns :phoenix_kit_api_actor, a PhoenixKit.API.Actor
whose scopes are the key's effective scopes of this request. On failure
it sends a problem document and halts: 401 unauthorized for a missing,
malformed, unknown, revoked or expired key or a dead owner (one answer for
all of them, so a probe learns nothing), 403 ip_not_allowed for a key used
from outside its allowed addresses.
Failed-authentication throttle
Failures are counted per client IP (PhoenixKit.API.RateLimit), and only
after the key has been verified and rejected: the plug asks
RateLimit.check_preauth/1 (a peek that charges nothing) after a
failure. If the IP's bucket is exhausted the failure is answered 429 rate_limited and is not charged again; otherwise it is charged
(RateLimit.record_auth_failure/1) and answered 401 or 403. A request
with a valid key never reaches that code, so a neighbour guessing keys from
the same address cannot lock the accounting system out. The throttle
limits the answers, not the lookups: every guess still costs one indexed
read of the key table (an unknown prefix is never cached).
Summary
Functions
Whether name is the name of a credential: one of credential_params/0, or any name
that ends in _token (access_token, session_token, claim_token), compared
without regard to case. token_count and tokens_issued are not.
The credential names core knows by heart. credential_name?/1 adds every name that
ends in _token.
Whether the website password gate hides the API of audience from the
client of conn.
Whether the query string carries a credential: a parameter whose name is a
credential (credential_name?/1: api_key, authorization, token, secret,
anything ending in _token and so on; any case, with or without a [] suffix),
or any parameter whose value starts with pk_ (any case, after trimming spaces).
Functions
Whether name is the name of a credential: one of credential_params/0, or any name
that ends in _token (access_token, session_token, claim_token), compared
without regard to case. token_count and tokens_issued are not.
The one rule behind the refusal of a credential in a URL and the redaction of a
credential from a stored answer (PhoenixKit.API.Idempotency).
@spec credential_params() :: [String.t()]
The credential names core knows by heart. credential_name?/1 adds every name that
ends in _token.
The boot-time log filter (PhoenixKit.boot/1) is built from this list, and Phoenix
filters a parameter whose name contains one of its entries, so token also
keeps every *_token out of the log: a name refused here never reaches the log in
clear text.
@spec gated?(Plug.Conn.t(), :admin | :public) :: boolean()
Whether the website password gate hides the API of audience from the
client of conn.
While the gate is on, the public API is as private as the site: it would otherwise hand a gated pre-production site's catalogue and customer data to anyone. Addresses on the gate's allow-list pass, like they do for the site. The admin API is not gated: it has its own keys.
Only PhoenixKit.WebsiteAccess.Gate.enabled?/0 and the address list are
asked: the gate's session helpers need a fetched session, which an API
request does not have. Both reads have the semantics of the site gate
itself: PhoenixKit.Settings folds a read error into the setting's default
(gate off), exactly as PhoenixKitWeb.Plugs.WebsiteAccess does for the
site, so the gate is not made stricter here than it is for the pages it
protects. The API switch, which is read first, fails closed.
PhoenixKit.API.BodyGuard asks the same question, so a request this
function turns away is not read or judged there either.
@spec token_in_url?(Plug.Conn.t()) :: boolean()
Whether the query string carries a credential: a parameter whose name is a
credential (credential_name?/1: api_key, authorization, token, secret,
anything ending in _token and so on; any case, with or without a [] suffix),
or any parameter whose value starts with pk_ (any case, after trimming spaces).
Names and values are compared after percent-decoding, so %61pi_key=...
is caught too.