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'sclient_idpush requests in its name. Whenfalse, a bodyclient_idwithout a secret is rejected withinvalid_client"client authentication required".:assertion_audiences- the acceptableaudvalues for aprivate_key_jwtassertion (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{}}. It reads 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/2call against:unknown_clientruns 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).
Summary
Functions
Authenticate the client from the request's Authorization header values and
body params (RFC 6749 §2.3).
Functions
@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{}}.