Plug that admits a request to the shop 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 :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
The query parameter names token_in_url?/1 treats as a credential.
Whether the website password gate hides the API of audience from the
client of conn.
Whether the query string carries a credential: a parameter named
api_key, apikey, api-key, authorization, bearer, access_token,
token, cart_token or confirm_token (any case, with or without a []
suffix), or any parameter whose value starts with pk_ (any case, after
trimming spaces).
Functions
@spec credential_params() :: [String.t()]
The query parameter names token_in_url?/1 treats as a credential.
The boot-time log filter (PhoenixKit.boot/1) is built from this list, so 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 carts 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 named
api_key, apikey, api-key, authorization, bearer, access_token,
token, cart_token or confirm_token (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.