OAuth 2.0 token endpoint (RFC 6749 §3.2).
Handles POST /oauth/token. This module owns the HTTP and protocol-framing
concerns only; every cryptographic and grant-state decision is delegated to
the Attesto core, and every policy decision is delegated to a callback on
AttestoPhoenix.Config. It carries no business-domain logic.
Grant types
authorization_code(RFC 6749 §4.1.3) with mandatory PKCE (RFC 7636), redeemed throughAttesto.AuthorizationCode.redeem/4.refresh_token(RFC 6749 §6) with rotation and reuse detection (RFC 6749 §10.4, OAuth 2.0 Security BCP), viaAttesto.RefreshToken.rotate/3.client_credentials(RFC 6749 §4.4).- OAuth token exchange (RFC 8693) for downscoping a verified subject access token.
Client authentication
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). Confidential clients must authenticate; a client
identified without a secret/assertion is admitted only when the host's
:client_public? callback marks it public, in which case it relies on PKCE
(RFC 7636) instead. Lookup, secret verification, and client public keys are
supplied by the host's :load_client, :verify_client_secret, and
:client_jwks callbacks on AttestoPhoenix.Config.
Revocation is carried by :load_client itself: it returns
{:error, :revoked} (or {:error, :not_found}) for a client that must not
authenticate, and both the confidential and public paths fail closed on any
non-{:ok, _} result. There is no separate revocation predicate - the
single lookup is the revocation gate, so a revoked client is rejected on
every grant.
Sender-constrained tokens
Access tokens are signed JWTs minted by Attesto.Token. When the
request carries a DPoP proof (RFC 9449) the access token is bound to the
proof's JWK thumbprint (cnf.jkt); when the client is configured for
mutual TLS (RFC 8705) it is bound to the certificate thumbprint
(cnf.x5t#S256). DPoP takes precedence when both are presentable
(RFC 9449 §5). When a fresh DPoP nonce is required (RFC 9449 §8/§9), one is
issued through the configured nonce store and returned in a DPoP-Nonce
response header alongside a use_dpop_nonce error.
Responses
Success renders the RFC 6749 §5.1 body; failure renders the RFC 6749 §5.2 error body. Both carry no-store cache headers (RFC 7234 §5.2) so credentials are never cached by an intermediary.
Configuration contract
All host policy is resolved through AttestoPhoenix.Config; nothing is
hardcoded here. This controller reads (see AttestoPhoenix.Config for the
authoritative definitions and defaults):
:load_client,:verify_client_secret- client lookup and constant-time secret check.:authorize_scope- scope resolution through theAttesto.Scopealgebra.:nonce_store,:cert_der,:dpop_enabled,:mtls_enabled,:dpop_nonce_required- sender-constraint policy and stores.:on_event- the optional audit/telemetry hook (viaAttestoPhoenix.Event).:issuer,:audience,:keystore,:access_token_ttl- claim-level policy, supplied toAttesto.Tokenas anAttesto.Configderived byAttestoPhoenix.Config.to_attesto_config/2.
Further callbacks are read from the configuration so the host owns the
client- and grant-shaped pieces this library cannot know generically. They
are read defensively and fail closed when unset: a missing
:client_public? treats every client as confidential (so no secretless
authentication), a missing :client_requires_mtls? treats no client as
mTLS-required, and a missing :build_principal yields a fail-closed
invalid_request rather than a crash.
:client_public?-(client -> boolean)public/confidential discriminator (RFC 6749 §2.1). A client that is not public MUST present a secret.:client_requires_mtls?-(client -> boolean)certificate-binding requirement (RFC 8705). A client that requires mTLS and calls without a certificate is rejected, not downgraded to Bearer.:client_id-(client -> String.t())the client's identifier (RFC 6749 §2.2).:build_principal-(client, subject, scope -> Attesto principal map)assembling theAttesto.Token.mint/3principal.:issue_refresh_token?-(client, granted_scope -> boolean)gate on issuing an initial refresh token from the authorization-code grant (RFC 6749 §6). When unset, an initial refresh token is issued iff the granted scope containsoffline_access(OIDC Core §11) and a:refresh_storeis configured.:code_store/:refresh_store- theAttesto.CodeStore/Attesto.RefreshStoremodules backing the stateful grants.
Summary
Functions
Token endpoint action (RFC 6749 §3.2).
Functions
@spec create(Plug.Conn.t(), map()) :: Plug.Conn.t()
Token endpoint action (RFC 6749 §3.2).
Authenticates the client, dispatches on grant_type, and renders either the
RFC 6749 §5.1 success body or an RFC 6749 §5.2 error. Every response carries
no-store cache headers (RFC 7234 §5.2).