# `PhoenixKit.API.Auth`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v2.64.1/lib/phoenix_kit/api/auth.ex#L1)

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: :admin

In order, it:

  1. assigns `:phoenix_kit_api_request_id`: the `x-request-id` response
     header when `Plug.RequestId` set 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;
  2. answers `404` when 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;
  3. refuses a credential in the URL with `400 token_in_url`, before any
     authentication (`token_in_url?/1`);
  4. for the admin audience, authenticates the `Authorization: Bearer
     pk_...` key with `PhoenixKit.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).

# `credential_name?`

```elixir
@spec credential_name?(term()) :: boolean()
```

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`).

# `credential_params`

```elixir
@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.

# `gated?`

```elixir
@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.

# `token_in_url?`

```elixir
@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.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
