# `AttestoPhoenix.ClientAuthentication`
[🔗](https://github.com/XukuLLC/attesto_phoenix/blob/v0.19.0/lib/attesto_phoenix/client_authentication.ex#L1)

OAuth 2.0 client authentication (RFC 6749 §2.3), as conn-free core.

This is the single place that turns the request's `Authorization` header and
body parameters into either an authenticated client or an
`AttestoPhoenix.OAuthError`. It is shared by the token endpoint
(RFC 6749 §3.2) and the Pushed Authorization Request endpoint (RFC 9126):
both authenticate the client identically; only the policy around the
secretless/public path and the event/wire rendering differ, and those are
the caller's concern.

## Methods

Accepts HTTP Basic credentials (RFC 6749 §2.3.1, RFC 7617), request-body
credentials (RFC 6749 §2.3.1), and `private_key_jwt` assertions (RFC 7523 /
OIDC Core §9). Presenting more than one client-authentication method is
rejected (RFC 6749 §2.3).

## Client ID Metadata Documents (CIMD)

When CIMD (`draft-ietf-oauth-client-id-metadata-document-01`) is enabled and
the presented `client_id` is a CIMD URL, the client is dereferenced from that
URL (`AttestoPhoenix.ClientIdMetadata`) rather than looked up in the host
registry. A CIMD client carries no shared symmetric secret (the document
validation strips `client_secret_*` and the symmetric auth methods), so it can
only authenticate as a **public client** (`none` + PKCE) or with
**`private_key_jwt`** keyed by the document's `jwks` / `jwks_uri`. The Basic /
body-secret paths therefore never resolve a CIMD client: a `client_secret`
presented for a CIMD `client_id` finds no secret to verify and fails with the
generic `invalid_client` message like any other failed authentication. CIMD
resolution is consulted only on the secretless (`none`) and `private_key_jwt`
paths, where the host registry would not hold the URL.

## Policy

The one decision that differs between callers is carried as data on
`AttestoPhoenix.ClientAuthentication.Policy`:

  * `:allow_public` - whether a client identified without a secret/assertion
    may authenticate as a public client (RFC 6749 §2.1), relying on PKCE
    (RFC 7636) downstream. The token endpoint allows this; the PAR endpoint
    does not, because a request reference established without proof of
    possession of the client secret would let anyone who knows a
    confidential client's `client_id` push requests in its name. When
    `false`, a body `client_id` without a secret is rejected with
    `invalid_client` "client authentication required".
  * `:assertion_audiences` - the acceptable `aud` values for a
    `private_key_jwt` assertion (RFC 7523 §3 / FAPI 2: the issuer
    identifier, not the endpoint URL).
  * `:assertion_max_lifetime` - the maximum assertion lifetime, in seconds,
    and the replay-record TTL (RFC 7523 §3).

## Return value

`authenticate/4` returns `{:ok, %Result{client, client_id, method}}` or
`{:error, %AttestoPhoenix.OAuthError{}}`. `authenticate_with_context/4`
keeps the same successful return and adds transport context to errors for
endpoints that must distinguish `Authorization`-header authentication attempts
while rendering. Both functions read only data: the
`Authorization` header values and the parsed body params. It never touches a
conn and never emits an event - the caller renders the result/error and
emits whatever audit event it owns.

## Security details preserved

  * On an unknown/revoked client, a dummy `verify_client_secret/2` call
    against `:unknown_client` runs so the lookup-failure path matches the
    wrong-secret path in observable timing (RFC 6749 §2.3 / OWASP).
  * Every client-authentication failure returns the single generic
    `invalid_client` "client authentication failed" message, so an attacker
    cannot tell an unknown client from a wrong secret.
  * Presenting more than one authentication method is rejected with
    `invalid_request` (RFC 6749 §2.3).

# `authenticate`

```elixir
@spec authenticate(
  [String.t()],
  map(),
  AttestoPhoenix.Config.t(),
  AttestoPhoenix.ClientAuthentication.Policy.t()
) ::
  {:ok, AttestoPhoenix.ClientAuthentication.Result.t()}
  | {:error, AttestoPhoenix.OAuthError.t()}
```

Authenticate the client from the request's `Authorization` header values and
body params (RFC 6749 §2.3).

`authorization_headers` is the list of `Authorization` header values (as
returned by `Plug.Conn.get_req_header(conn, "authorization")`). `params` is
the parsed request body. Returns `{:ok, %Result{}}` or
`{:error, %AttestoPhoenix.OAuthError{}}`.

# `authenticate_with_context`

```elixir
@spec authenticate_with_context(
  [String.t()],
  map(),
  AttestoPhoenix.Config.t(),
  AttestoPhoenix.ClientAuthentication.Policy.t()
) ::
  {:ok, AttestoPhoenix.ClientAuthentication.Result.t()}
  | {:error, AttestoPhoenix.OAuthError.t(),
     AttestoPhoenix.ClientAuthentication.ErrorContext.t()}
```

Authenticates the client and preserves client-authentication transport context
on errors.

The successful return matches `authenticate/4`. Error returns add an
`%ErrorContext{}` naming the `Authorization` scheme when the request attempted
header authentication; token-endpoint callers use it to apply RFC 6749 §5.2
401 challenge rules without re-reading the conn.

---

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