AttestoPhoenix.Controller.TokenController (AttestoPhoenix v0.6.1)

Copy Markdown View Source

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 through Attesto.AuthorizationCode.redeem/4.
  • refresh_token (RFC 6749 §6) with rotation and reuse detection (RFC 6749 §10.4, OAuth 2.0 Security BCP), via Attesto.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 the Attesto.Scope algebra.
  • :nonce_store, :cert_der, :dpop_enabled, :mtls_enabled, :dpop_nonce_required - sender-constraint policy and stores.
  • :on_event - the optional audit/telemetry hook (via AttestoPhoenix.Event).
  • :issuer, :audience, :keystore, :access_token_ttl - claim-level policy, supplied to Attesto.Token as an Attesto.Config derived by AttestoPhoenix.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 the Attesto.Token.mint/3 principal.
  • :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 contains offline_access (OIDC Core §11) and a :refresh_store is configured.
  • :code_store / :refresh_store - the Attesto.CodeStore / Attesto.RefreshStore modules backing the stateful grants.

Summary

Functions

Token endpoint action (RFC 6749 §3.2).

Functions

create(conn, params)

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