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

Configuration for the `attesto_phoenix` authorization-server layer.

This is the single source of truth consumed by every controller and plug in
the library. It reads the host's configuration (from a host-chosen
`otp_app`/config key), validates the required keys, applies neutral defaults,
and derives the `Attesto.Config` the protocol layer needs.

Build one with `new/1` (from a keyword list or map) or `from_otp_app/2` (to
read `Application.get_env/2`). Validation raises `ArgumentError` on a missing
required key so misconfiguration fails fast at boot.

## Keys

### Required

  * `:issuer` - issuer URL (string) used as the JWT `iss`, the discovery
    issuer, and the base for endpoint URLs.
  * `:keystore` - module implementing `Attesto.Keystore` providing the
    signing key and the verification keys published via JWKS. Use a static
    keystore or a host KMS/HSM/Vault-backed implementation; per-key `alg`
    metadata is supported by the core keystore behaviour.
  * `:repo` - `Ecto.Repo` module used by the Ecto-backed code, refresh,
    nonce, and replay stores.
  * `:load_client` - `(client_id -> {:ok, client} | {:error, :not_found} |
    {:error, :revoked})`. Resolves an OAuth client. The host owns the client
    registry and revocation policy.
  * `:verify_client_secret` - `(client, presented_secret -> boolean)`.
    Constant-time client-secret verification (e.g. via
    `Attesto.SecureCompare`). The host owns secret hashing.
  * `:load_principal` - `(subject_id -> {:ok, principal} | {:error,
    :not_found})`. Resolves the subject/principal during protected-resource
    authentication.

### Optional callbacks

  * `:authorize_scope` - `(client, requested_scope -> {:ok, granted_scope} |
    {:error, :invalid_scope})`. Validates/narrows requested scope using
    `Attesto.Scope` algebra. Defaults to "subset of `:scopes_supported`".
  * `:on_event` - `(%AttestoPhoenix.Event{} -> any)`. Audit/telemetry hook.
    No-op by default; the library never stores events itself.
  * `:send_error` - `(conn, status, body_map -> conn)`. Optional transport
    hook used by `AttestoPhoenix.OAuthError` to serialize OAuth/OIDC errors
    into the host's API envelope while preserving the RFC status, challenge,
    and cache-control semantics.
  * `:no_store` - `(conn -> conn)`. Optional transport hook used by
    `AttestoPhoenix.OAuthError` to apply no-store headers.
  * `:www_authenticate` - `(conn, challenge_string -> conn)`. Optional
    transport hook used by `AttestoPhoenix.OAuthError` to write the
    `WWW-Authenticate` challenge header.
  * `:resource_metadata` - absolute URL of this resource's protected-resource
    metadata document (RFC 9728). When set, `AttestoPhoenix.Plug.Authenticate`
    advertises it as a `resource_metadata` auth-param on every
    `WWW-Authenticate` challenge it renders (RFC 9728 §5.1), so a client that
    is refused with 401 can discover which authorization server issues tokens
    for this resource. Omitted from the challenge when unset.
  * `:basic_realm` - realm string for token-endpoint Basic auth challenges.
    Default `"OAuth"`.
  * `:htu` - `(conn -> canonical_url_string)`. Overrides how the DPoP `htu`
    is computed behind proxies. Defaults to derivation from `:trusted_proxies`.
  * `:cert_der` - `(conn -> der_binary | nil)`. Extracts the client mTLS
    certificate DER. Required only when `:mtls_enabled`.
  * `:register_client` - `(metadata -> {:ok, client} | {:error, reason})`.
    Persists a dynamically registered client. Required only when
    `:registration_enabled`.
  * `:unregister_client` - `(client -> :ok | {:ok, client} | {:error, reason})`.
    Deletes a dynamically registered client for registration management
    cleanup (RFC 7592). Optional; when unset, DELETE requests to the
    registration management endpoint fail closed.
  * `:client_registration_access_token_hash` - `(client -> String.t() | nil)`.
    Extracts the stored hash of the registration access token issued with a
    dynamic client (RFC 7592). Optional; when unset, DELETE requests fail
    closed.
  * `:introspection_authorize` - `(caller_client_id, response -> boolean)`.
    Authorizes the authenticated introspection caller against the token being
    introspected (RFC 7662 §4 / RFC 9701 §5). Consulted only for an active
    response; returning anything but `true` (or raising) downgrades the
    response to `%{"active" => false}` so a caller not entitled to the token
    learns nothing about it (FAPI: a regular client querying introspection is
    a leakage risk). `response` is the RFC 7662 member map (carrying `aud`,
    `client_id`, `sub`, `scope`, ...), letting the host match the token's
    audience/scope against the calling protected resource. Optional - when
    unset, every authenticated caller may introspect any token (the
    single-trust-domain default).
  * `:principal_kinds` - non-empty list of `Attesto.PrincipalKind` values
    or a zero-arity callback returning that list, passed into the core token
    configuration.
  * `:build_principal` - `(client, subject, scope -> map)`. Builds the
    principal map passed to `Attesto.Token.mint/3`. The returned `:sub` MUST
    be namespaced with the matching `Attesto.PrincipalKind` `sub_prefix`:
    `Attesto.Token` rejects an unprefixed subject at mint time
    (`:invalid_sub`). This matters most for the `client_credentials` grant
    (RFC 6749 §4.4), where the principal subject is the OAuth `client_id` -
    and Dynamic Client Registration (RFC 7591 §3.2.1) issues that id
    *unprefixed* (the host's `:register_client` chooses it; the library
    imposes no namespace). `:build_principal` is the sole seam that applies
    the prefix; the prefix is mint-time defense-in-depth (an issued token's
    `sub` is unambiguous across principal kinds), not a substitute for it.
  * `:build_userinfo_claims` - `(subject, granted_scopes, requested_claims ->
    claims_map)`. Produces the claim values the UserInfo endpoint
    (OpenID Connect Core §5.3) returns for the authenticated subject. The
    host owns the claim source (its user store); the library owns only the
    scope-to-claim shaping (OpenID Connect Core §5.4) and the guarantee that
    `sub` is present (OpenID Connect Core §5.3.2). `granted_scopes` is the
    list of scopes on the access token; `requested_claims` is the per-claim
    request map from the OpenID Connect `claims` parameter (`%{}` when none).
    Required only when the UserInfo endpoint is mounted.
  * `:build_id_token_claims` - `(client, subject, granted_scopes,
    requested_claims -> claims_map)`. Produces the host claims merged into an
    ID Token (OpenID Connect Core §3.1.3.6 / §5.5 `id_token` member). Distinct
    from `:build_userinfo_claims`: it receives the resolved `client`, draws
    from the `claims` parameter's `id_token` member, and MUST NOT carry `sub`
    (the library sets the verified subject; a host-supplied `sub` is rejected
    by `Attesto.IDToken`). Optional - when unset the ID Token carries only the
    protocol claims.
  * `:client_id` - `(client -> String.t())`. Extracts the OAuth client
    identifier from the host's client struct.
  * `:client_jwks` - `(client -> jwks)`. Returns the client's trusted public
    JWK Set for `private_key_jwt` client authentication. Required only for
    clients that authenticate with `private_key_jwt`.
  * `:client_redirect_uris` - `(client -> [String.t()])`. Returns the
    client's registered redirect URIs (RFC 6749 §3.1.2.2). The authorization
    endpoint exact-matches the request `redirect_uri` against this set
    (RFC 6749 §3.1.2.3); a client exposing none rejects every authorization
    request (fail closed).
  * `:authenticate_resource_owner` - `(conn, request, auth_opts ->
    {:authenticated, subject} | {:halt, conn} | {:none} | {:error,
    :login_required | :consent_required | :interaction_required})`.
    Establishes the resource owner for an authorization request (RFC 6749
    §3.1, OIDC Core §3.1.2.3). Returns `{:authenticated, subject}` once a
    resource owner is known (a map carrying at least `:subject`, the OIDC
    `sub`, and optionally `:auth_time`, `:acr`, `:amr`), `{:halt, conn}` to
    take over the connection (e.g. redirect to a host login page that
    re-enters the authorization endpoint), `{:none}` when no subject can be
    established without UI, or an `{:error, _}` classifying why interaction is
    required (OIDC Core §3.1.2.6). `auth_opts` is a map carrying the OIDC Core
    §3.1.2.1 `prompt`/`max_age` directives the host must honour: `:prompt`,
    `:force_reauth` (`prompt=login`), `:interactive` (`false` for
    `prompt=none`, forbidding UI), and `:max_age`. The host owns all login
    UI; the library only invokes this hook. Required only when the
    authorization endpoint is mounted.
  * `:consent` - `(conn, request, subject -> {:consented, subject} |
    {:halt, conn} | {:denied, reason})`. Obtains the resource owner's consent
    for an authorization request (RFC 6749 §4.1.1). Returns
    `{:consented, subject}` to proceed (the returned subject may carry
    consent-derived claims), `{:halt, conn}` to take over the connection (e.g.
    render a consent screen that re-enters the authorization endpoint), or
    `{:denied, reason}` to refuse (reported to the client as `access_denied`,
    RFC 6749 §4.1.2.1). When unset, consent is implicitly granted for the
    authenticated subject.
  * `:client_public?` - `(client -> boolean())`. Returns whether a client
    may authenticate without a secret and rely on PKCE.
  * `:client_requires_mtls?` - `(client -> boolean())`. Returns whether a
    client requires mTLS-bound token issuance.
  * `:client_requires_dpop?` - `(client -> boolean())`. Returns whether a
    client requires DPoP-bound token issuance.
  * `:client_grant_types` - `(client -> [String.t()] | nil)`. Returns the
    grant types registered for this client (RFC 7591 §2). When set, the
    token endpoint rejects a requested grant type not in the returned list.
  * `:issue_refresh_token?` - `(client, granted_scope -> boolean())`.
    Returns whether the authorization-code grant should issue an initial
    refresh token (RFC 6749 §6). When unset, the token controller issues one
    iff the granted scope contains `offline_access` (OIDC Core §11) and a
    `:refresh_store` is configured.
  * `:code_store` - module implementing `Attesto.CodeStore`.
  * `:refresh_store` - module implementing `Attesto.RefreshStore`.
  * `:par_store` - module implementing `AttestoPhoenix.PARStore`. Defaults to
    the single-node `AttestoPhoenix.Store.PAR.ETS`; use
    `AttestoPhoenix.Store.EctoPARStore` for a clustered/load-balanced
    deployment so a `request_uri` resolves on every node (FAPI 2.0 requires
    PAR).
  * `:consent_grant_store` - module implementing
    `AttestoPhoenix.ConsentGrantStore`, the single-use request-bound consent
    primitive (RFC 6749 §4.1.1). The host consent screen mints a grant when
    the resource owner authorizes; the host's `:consent` callback consumes it
    before a code is issued, so one consent click cannot approve a different
    client/redirect/scope/challenge. The library ships the Ecto-backed
    `AttestoPhoenix.Store.EctoConsentGrantStore`; there is no default, because
    the library never renders a consent screen — a host wires this only when
    it adopts the consent primitive. Read it back with
    `consent_grant_store/1`.
  * `:grant_types_supported` - the grant types the server supports. Advertised
    as `grant_types_supported` (RFC 8414 §2), enforced by the token endpoint (a
    `grant_type` outside the set is rejected), and the accepted set for dynamic
    registration. Defaults to every implemented grant; narrow it to disable one
    (e.g. drop token-exchange) everywhere at once. See `grant_types_supported/1`.
  * `:token_endpoint_auth_methods_supported` - client authentication methods
    advertised/accepted by dynamic client registration and by the token/PAR
    endpoints when configured. When unset, all package-supported methods are
    accepted.

### Optional values (with defaults)

  * `:audience` - default access-token audience (string or list).
  * `:client_auth_signing_algs` - the JOSE algorithms accepted for
    `private_key_jwt` client-assertion signatures, and the set advertised as
    `token_endpoint_auth_signing_alg_values_supported` in discovery. Defaults
    to `Attesto.SigningAlg.fapi_algs/0` (PS256, ES256, EdDSA). A non-FAPI
    deployment can widen it; verification and the advertised metadata stay in
    lockstep because both read this one value.
  * `:request_object_policy` - an `Attesto.RequestObject.Policy` controlling
    verification of signed authorization request objects (JAR, RFC 9101).
    Defaults to `%Attesto.RequestObject.Policy{}` (generic OpenID Connect §6.1:
    `nbf`/`exp`/`typ` not required). For FAPI 2.0 Message Signing §5.3.1 set
    `Attesto.RequestObject.Policy.fapi_message_signing()`; the policy is then
    enforced both at the PAR endpoint and at `/authorize`.
  * `:scopes_supported` - list of supported scope strings (concrete and
    wildcard) advertised in discovery and used as the default scope catalog.
    For an OpenID Provider the reserved `openid` scope (OpenID Connect Core
    §3.1.2.1) is added to the OpenID Provider Metadata automatically by the
    core builder; it need not be listed here.
  * `:bearer_methods_supported` - the RFC 6750 access-token presentation
    methods the resource server accepts, advertised as
    `bearer_methods_supported` in the RFC 9728 protected-resource metadata
    document (`/.well-known/oauth-protected-resource`). A non-empty list of
    distinct methods, each `"header"` (§2.1) or `"body"` (§2.2) - the methods
    `AttestoPhoenix.Plug.Authenticate` accepts. The §2.3 `"query"` method is
    rejected: the plug never accepts a query-presented token, so advertising it
    would name a method the library cannot honour (and RFC 6750 §2.3 says it
    SHOULD NOT be used). Defaults to `["header"]`; add `"body"` only for a
    resource server that intentionally accepts RFC 6750 §2.2 form-body
    `access_token` credentials and wants to advertise that method.
  * `:authorization_endpoint` - absolute URL of the host-owned authorization
    endpoint (RFC 6749 §3.1 / OpenID Connect Discovery §3). The authorization
    endpoint runs the host's login/consent UI, so the library does not mount
    it; the host supplies the URL where it serves it. Advertised in the
    OpenID Provider Metadata; omitted when unset.
  * `:userinfo_endpoint` - absolute URL of the host-owned UserInfo endpoint
    (OpenID Connect Core §5.3). The host owns the claim source, so the
    library does not mount it; the host supplies the URL. Advertised in the
    OpenID Provider Metadata; omitted when unset.
  * `:claims_supported` - list of claim names the host's UserInfo endpoint
    and ID Tokens can return (OpenID Connect Discovery §3). Advertised in the
    OpenID Provider Metadata; omitted when unset.
  * `:claims_parameter_supported` - whether the provider accepts the OpenID
    Connect `claims` request parameter (OpenID Connect Discovery §3 /
    OpenID Connect Core §5.5). Default `false`: the authorization endpoint
    does not consume a `claims` parameter unless the host wires it, so the
    provider does not claim support for it. Advertised in the OpenID Provider
    Metadata only when set to `true` (the core builder treats absence as
    `false` per OpenID Connect Discovery §3).
  * `:acr_values_supported` - list of Authentication Context Class Reference
    values the provider can satisfy (OpenID Connect Discovery §3 /
    OpenID Connect Core §2). Advertised only when the host configures a
    non-empty list; omitted otherwise.
  * `:ui_locales_supported` - list of BCP47 (RFC 5646) language tags the
    provider's UI supports (OpenID Connect Discovery §3). Advertised only
    when the host configures a non-empty list; omitted otherwise.
  * `:require_nonce` - require the OpenID Connect `nonce` parameter on
    OpenID Connect Authentication Requests (OpenID Connect Core §3.1.2.1).
    Default `false`. When `true`, the authorization endpoint passes
    `require_nonce: true` to `Attesto.AuthorizationRequest.validate/2` for a
    request whose scope contains `openid`, so a missing `nonce` on an OIDC
    request is rejected with a redirectable `invalid_request` error. A
    non-OpenID OAuth 2.0 request is never affected (RFC 6749 keeps the
    authorization code at SHOULD, never requiring a `nonce`). The host sets
    this per its own OpenID Provider policy.
  * `:require_pushed_authorization_requests` - require front-channel
    authorization requests to use a PAR `request_uri` issued by this server
    (RFC 9126). Default `false`.
  * `:authorization_response_iss` - include the RFC 9207 `iss` authorization
    response parameter on success and error redirects. Default `true`
    (authorization-server mix-up defense, mandated by FAPI 2.0); set `false`
    only for a deployment that must omit it.
  * `:require_https` - enforce HTTPS on the endpoints. Default `true`.
  * `:trusted_proxies` - list of trusted proxy CIDRs/IPs controlling whether
    `X-Forwarded-*` headers are honored. Default `[]` (no forwarded trust).
  * `:access_token_ttl` - access-token lifetime, seconds. Default `900`.
  * `:refresh_token_ttl` - refresh-token lifetime, seconds. Default `1_209_600`.
  * `:refresh_token_rotation_grace_seconds` - idempotency window, in
    seconds, during which a just-rotated refresh token can be retried and
    receive the same successor refresh token instead of being treated as a
    reuse attack. Default `60`; set `0` for strict immediate reuse
    revocation. A non-zero window is important for clients that lose the
    first rotation response and retry the previous token (OAuth 2.0 Security
    BCP §4.13; FAPI 2.0 Security Profile §5.3.2.1).
  * `:authorization_code_ttl` - authorization-code lifetime, seconds. Default `60`.
  * `:dpop_enabled` - enable DPoP sender-constraint support. Default `true`.
  * `:dpop_nonce_required` - require server-issued DPoP nonces. Default `false`.
  * `:mtls_enabled` - enable mTLS (RFC 8705) `cnf` binding. Default `false`.
  * `:registration_enabled` - enable `/oauth/register`. Default `false`.
  * `:registration_default_scope` - the scope assigned to a dynamically
    registered client (RFC 7591 §2) when its request omits `scope`, echoed
    back in the §3.2.1 response. `:scopes_supported` assigns the full catalog;
    a list assigns that explicit subset (validated against `:scopes_supported`
    at boot). Default `nil` - a scopeless registration stays scopeless
    (fail-closed). Setting this lets a scopeless DCR client (e.g. an MCP/agent
    client) register with a usable scope without each host reinventing it.
  * `:client_id_metadata` - Client ID Metadata Document support - CIMD
    (`draft-ietf-oauth-client-id-metadata-document-01`, IETF OAuth WG). A
    keyword list configuring whether (and how) the authorization server
    dereferences an HTTPS `client_id` URL to a client metadata document. The
    whole feature is off by default; when `enabled: true`, discovery
    advertises `client_id_metadata_document_supported` and
    `AttestoPhoenix.ClientIdMetadata.Resolver` resolves a CIMD `client_id`
    through the configured fetcher and cache. Read it back with
    `client_id_metadata/1` (the merged, defaulted keyword list) or the
    `client_id_metadata_enabled?/1` predicate. Recognized members, with their
    defaults:

      * `:enabled` - master switch. Default `false`.
      * `:fetcher` - module implementing
        `AttestoPhoenix.ClientIdMetadata.Fetcher` (the SSRF-guarded outbound
        `GET`). Default `AttestoPhoenix.ClientIdMetadata.Fetcher.Req`. A host
        may override with its own HTTP stack or a CIMD proxy service.
      * `:cache` - module implementing
        `AttestoPhoenix.ClientIdMetadata.Cache`. Default
        `AttestoPhoenix.ClientIdMetadata.Cache.Ecto` (cluster-coherent); a
        single-node deployment may select
        `AttestoPhoenix.ClientIdMetadata.Cache.ETS`.
      * `:allow_loopback` - permit loopback addresses (the draft's "AS runs on
        loopback" exception; development only). Default `false`.
      * `:max_document_bytes` - body size cap for the fetched document
        (draft's recommended 5 KB). Default `5_120`.
      * `:request_timeout_ms` - connect and receive timeout for the fetch.
        Default `5_000`.
      * `:cache_ttl_bounds` - `{min_seconds, max_seconds}` the resolver clamps
        the response's `Cache-Control: max-age` / `Expires` freshness to
        (RFC 9111). Default `{60, 86_400}`.
      * `:require_same_origin_redirect_uri` - additionally require the request
        `redirect_uri` to be same-origin with the `client_id` URL, on top of
        the exact-match against the document's `redirect_uris` (draft §2 MAY,
        enforced by default here). Default `true`.
      * `:allowed_hosts` - optional allowlist of hostnames a CIMD `client_id`
        URL may resolve through; `nil` means "any public host" (subject to the
        fetcher's SSRF guard). Default `nil`.
      * `:blocked_hosts` - hostnames a CIMD `client_id` URL must never resolve
        through, checked before any network work. Default `[]`.
  * `:replay_check` - DPoP `jti` replay check (module or `{module, fun}`).
    Defaults to the single-node ETS replay cache.
  * `:nonce_store` - `Attesto.DPoP.NonceStore` implementation. Defaults to
    the single-node ETS nonce store.
  * `:sweep_interval_ms` - interval for `AttestoPhoenix.Store.Sweeper`. The
    sweeper is not started if unset.
  * `:table_prefix` - optional Ecto schema/table prefix for the generated
    tables.

### Endpoint paths advertised in metadata

The discovery documents (RFC 8414 §3, OpenID Connect Discovery §4) and the
RFC 7591 §3.2.1 registration response advertise absolute endpoint URLs built
from the `:issuer` and the request path each endpoint is mounted at. By
default the OAuth endpoints live under `/oauth/*` (the historic surface), but
a host that mounts them elsewhere (for example under `/mcp/oauth/*` to avoid
colliding with a legacy provider) MUST advertise the paths it actually serves
or clients are misdirected. These keys control that, all additive with
defaults that reproduce the historic `/oauth/*` surface exactly:

  * `:oauth_path_prefix` - path segment prepended to every OAuth endpoint
    tail. Default `"/oauth"`, yielding the historic `/oauth/token`,
    `/oauth/par`, etc. A host mounting under `/mcp/oauth` sets
    `oauth_path_prefix: "/mcp/oauth"` to advertise `/mcp/oauth/token` and so
    on. This is the FULL client-visible mount prefix, since the controllers
    cannot see the surrounding Phoenix `scope`. The well-known documents
    (RFC 8615) and the JWKS document stay anchored at the host root and are
    NOT relocated by this prefix.
  * `:authorize_path`, `:token_path`, `:par_path`, `:revocation_path`,
    `:introspection_path`, `:registration_path`, `:userinfo_path` - explicit per-endpoint path
    overrides. When set, the override wins over `:oauth_path_prefix` for that
    one endpoint (the integrator's "explicit endpoint overrides plus sane
    defaults"). Each defaults to `nil`, meaning "derive from
    `:oauth_path_prefix`". An override is an absolute path reference
    (`"/custom/token"`), advertised verbatim merged onto the issuer.

Use the resolver helpers (`token_endpoint_url/1`, `par_endpoint_url/1`,
`revocation_endpoint_url/1`, `registration_endpoint_url/1`,
`userinfo_endpoint_url/1`, `authorize_endpoint_url/1`, `jwks_uri/1`, and the
resolved-path helpers `token_path/1` and friends) rather than re-deriving the
URLs in callers; the router macro derives its mounted-route tails from the
same source so the mounted routes and the advertised routes cannot drift.

## Recommended production callback contracts

The loose `*_client`, `*_principal`, `authorize_scope`, consent, registration,
and event callbacks above are grouped into named behaviours that document the
full contract (with the governing RFC for each callback) and serve as the
recommended production shape: `AttestoPhoenix.ClientStore`,
`AttestoPhoenix.PrincipalStore`, `AttestoPhoenix.ScopePolicy`,
`AttestoPhoenix.ConsentPolicy`, `AttestoPhoenix.RegistrationStore`, and
`AttestoPhoenix.EventSink`. Wiring stays identical: pass an anonymous
function, a `{module, function}` pair, or a `{module, function, extra_args}`
triple per key as documented above. The behaviours are the contract; the
Config keys are how a host installs an implementation.

## Behaviour-module Config keys

Rather than wiring every host callback as an individual flat key, a host may
install one behaviour module per concern and let the library resolve each
callback from it:

  * `:client_store` - a module implementing `AttestoPhoenix.ClientStore`.
  * `:principal_store` - a module implementing `AttestoPhoenix.PrincipalStore`.
  * `:consent_policy` - a module implementing `AttestoPhoenix.ConsentPolicy`.
  * `:scope_policy` - a module implementing `AttestoPhoenix.ScopePolicy`.
  * `:event_sink` - a module implementing `AttestoPhoenix.EventSink`.
  * `:registration` - a module implementing `AttestoPhoenix.RegistrationStore`.
  * `:claims_provider` - a module implementing `AttestoPhoenix.ClaimsProvider`.

Each per-callback value is resolved through the matching resolver fun on this
module (`client_id_fun/1`, `load_principal_fun/1`, `consent_fun/1`, and so on)
with a single precedence: the explicit flat key wins when set; otherwise, when
a behaviour module is installed and exports the corresponding behaviour
callback (after `Code.ensure_loaded/1`), the `{module, function}` pair is used;
otherwise the resolution is `nil` (and the consumer's existing fail-closed
default applies). Flat keys therefore never break: a host that wires the
individual callbacks keeps the exact behaviour it had. `new/1` validates at
boot that any installed behaviour module is loadable and exports the callbacks
it claims, so a typo'd or partial module fails fast rather than silently
resolving to `nil` at request time.

# `callback`

```elixir
@type callback() :: function() | {module(), atom()} | {module(), atom(), [any()]}
```

# `t`

```elixir
@type t() :: %AttestoPhoenix.Config{
  access_token_ttl: pos_integer(),
  acr_values_supported: [String.t()],
  audience: String.t() | [String.t()] | nil,
  authenticate_device_user: term(),
  authenticate_resource_owner: callback() | nil,
  authorization_code_ttl: pos_integer(),
  authorization_endpoint: String.t() | nil,
  authorization_response_iss: boolean(),
  authorize_path: String.t() | nil,
  authorize_scope: callback() | nil,
  basic_realm: String.t(),
  bearer_methods_supported: [String.t()],
  build_id_token_claims: callback() | nil,
  build_principal: callback() | nil,
  build_userinfo_claims: callback() | nil,
  cert_der: callback() | nil,
  claims_parameter_supported: boolean(),
  claims_provider: module() | nil,
  claims_supported: [String.t()],
  client_auth_signing_algs: [String.t()] | nil,
  client_backchannel_logout_session_required: term(),
  client_backchannel_logout_uri: term(),
  client_grant_types: callback() | nil,
  client_id: callback() | nil,
  client_id_metadata: keyword(),
  client_jwks: callback() | nil,
  client_post_logout_redirect_uris: term(),
  client_public?: callback() | nil,
  client_redirect_uris: callback() | nil,
  client_registration_access_token_hash: callback() | nil,
  client_requires_dpop?: callback() | nil,
  client_requires_mtls?: callback() | nil,
  client_store: module() | nil,
  code_store: module() | nil,
  consent: callback() | nil,
  consent_grant_store: module() | nil,
  consent_policy: module() | nil,
  device_authorization: term(),
  device_authorization_path: term(),
  device_code_store: term(),
  device_verification_path: term(),
  dpop_enabled: boolean(),
  dpop_nonce_required: boolean(),
  end_session_path: term(),
  event_sink: module() | nil,
  grant_types_supported: [String.t()] | nil,
  htu: callback() | nil,
  introspection_authorize: callback() | nil,
  introspection_path: String.t() | nil,
  issue_refresh_token?: callback() | nil,
  issuer: String.t(),
  jwt_bearer: keyword(),
  keystore: module(),
  load_client: callback(),
  load_principal: callback(),
  logout: keyword(),
  logout_session_store: term(),
  mtls_enabled: boolean(),
  no_store: callback() | nil,
  nonce_store: module() | nil,
  oauth_path_prefix: String.t(),
  on_event: callback() | nil,
  par_path: String.t() | nil,
  par_store: module() | nil,
  par_ttl: pos_integer(),
  principal_kinds: [Attesto.PrincipalKind.t()] | callback() | nil,
  principal_store: module() | nil,
  refresh_store: module() | nil,
  refresh_token_rotation_grace_seconds: non_neg_integer(),
  refresh_token_ttl: pos_integer(),
  register_client: callback() | nil,
  registration: module() | nil,
  registration_default_scope: [String.t()] | :scopes_supported | nil,
  registration_enabled: boolean(),
  registration_path: String.t() | nil,
  render_device_verification: term(),
  render_logged_out: term(),
  replay_check: callback() | module() | nil,
  repo: module(),
  request_object_policy: Attesto.RequestObject.Policy.t() | nil,
  require_https: boolean(),
  require_nonce: boolean(),
  require_pkce: boolean(),
  require_pushed_authorization_requests: boolean(),
  resolve_jwt_bearer_subject: callback() | nil,
  resource_indicators: keyword(),
  resource_metadata: String.t() | nil,
  revocation_path: String.t() | nil,
  scope_policy: module() | nil,
  scopes_supported: [String.t()],
  send_error: callback() | nil,
  sweep_interval_ms: pos_integer() | nil,
  table_prefix: String.t() | nil,
  terminate_session: term(),
  token_endpoint_auth_methods_supported: [String.t()] | nil,
  token_path: String.t() | nil,
  trusted_proxies: [String.t()],
  ui_locales_supported: [String.t()],
  unregister_client: callback() | nil,
  userinfo_endpoint: String.t() | nil,
  userinfo_path: String.t() | nil,
  verify_client_secret: callback(),
  www_authenticate: callback() | nil
}
```

# `allowed_resources`

```elixir
@spec allowed_resources(t(), term()) :: [String.t()]
```

The set of resource identifiers this authorization server will mint a token
for, for `client` (RFC 8707 §2.2).

Composes the server's own `:audience` (always served), the static
`resource_indicators: [allowed_resources: [...]]` list, and the per-client
`:allowed_resources_for` callback's result. A requested `resource` is honored
only when it appears here; anything else is `invalid_target`.

# `authenticate_resource_owner_fun`

```elixir
@spec authenticate_resource_owner_fun(t()) :: callback() | nil
```

Resolve the `authenticate_resource_owner` callback. See `resolve_callback/2`.

# `authorize_endpoint_url`

```elixir
@spec authorize_endpoint_url(t()) :: String.t()
```

Absolute URL of the authorization endpoint: the issuer merged with
`authorize_path/1`. Advertised in the OpenID Provider Metadata when the host
does not supply a separate `:authorization_endpoint`.

# `authorize_path`

```elixir
@spec authorize_path(t()) :: String.t()
```

The resolved request path of the authorization endpoint: the explicit
`:authorize_path` override when set, otherwise `:oauth_path_prefix` joined
with the conventional `/authorize` tail.

# `authorize_scope_fun`

```elixir
@spec authorize_scope_fun(t()) :: callback() | nil
```

Resolve the `authorize_scope` callback. See `resolve_callback/2`.

# `backchannel_logout_http`

```elixir
@spec backchannel_logout_http(t()) :: module()
```

The module that POSTs a `logout_token` to a Relying Party's `backchannel_logout_uri`.

# `backchannel_logout_session_supported?`

```elixir
@spec backchannel_logout_session_supported?(t()) :: boolean()
```

Returns `true` iff the OP includes `sid` in its logout tokens (advertised as
`backchannel_logout_session_supported`, Back-Channel Logout 1.0 §2.1). attesto
always asserts `sid` when the session supplies one, so this tracks
`backchannel_logout_supported?/1`.

# `backchannel_logout_supported?`

```elixir
@spec backchannel_logout_supported?(t()) :: boolean()
```

Returns `true` iff Back-Channel Logout is supported — logout is enabled AND a
`:logout_session_store` is wired (advertised as `backchannel_logout_supported`,
Back-Channel Logout 1.0 §2.1).

# `build_id_token_claims_fun`

```elixir
@spec build_id_token_claims_fun(t()) :: callback() | nil
```

Resolve the `build_id_token_claims` callback. See `resolve_callback/2`.

# `build_principal_fun`

```elixir
@spec build_principal_fun(t()) :: callback() | nil
```

Resolve the `build_principal` callback. See `resolve_callback/2`.

# `build_userinfo_claims`

```elixir
@spec build_userinfo_claims(t(), String.t(), [String.t()], map()) :: map()
```

Invokes the host's `:build_userinfo_claims` callback for the authenticated
subject and returns the raw claims map it produces.

The callback is applied with `[subject, granted_scopes, requested_claims]`
(see the `:build_userinfo_claims` key documentation). It is the claim source
for the UserInfo endpoint (OpenID Connect Core §5.3); the host owns the claim
values, the controller owns the scope-to-claim shaping. Raises
`ArgumentError` when the host has not configured the callback, so a mounted
UserInfo endpoint cannot silently return an empty document.

# `build_userinfo_claims_fun`

```elixir
@spec build_userinfo_claims_fun(t()) :: callback() | nil
```

Resolve the `build_userinfo_claims` callback. See `resolve_callback/2`.

# `client_backchannel_logout_session_required`

```elixir
@spec client_backchannel_logout_session_required(t(), term()) :: boolean()
```

The Relying Party's `backchannel_logout_session_required` (Back-Channel Logout
1.0 §2.2): whether its logout token MUST carry `sid`. Defaults to `false`.

# `client_backchannel_logout_session_required_fun`

```elixir
@spec client_backchannel_logout_session_required_fun(t()) :: callback() | nil
```

Resolve the `client_backchannel_logout_session_required` callback. See `resolve_callback/2`.

# `client_backchannel_logout_uri`

```elixir
@spec client_backchannel_logout_uri(t(), term()) :: String.t() | nil
```

The Relying Party's registered `backchannel_logout_uri` (Back-Channel Logout
1.0 §2.2), or `nil` when the client is not back-channel-logout capable (so no
logout session is recorded and no token is fanned out to it).

The URI is also fail-closed against server-side request forgery: the OP POSTs
a `logout_token` to it, so a non-`https` URL, one carrying userinfo/a fragment,
or one whose host is a loopback / private / link-local / unique-local literal
(e.g. `127.0.0.1`, `10.x`, `169.254.169.254`, `localhost`) is treated as
absent. A registered URL that resolves to an internal address only via DNS is a
residual risk the host's egress controls own.

# `client_backchannel_logout_uri_fun`

```elixir
@spec client_backchannel_logout_uri_fun(t()) :: callback() | nil
```

Resolve the `client_backchannel_logout_uri` callback. See `resolve_callback/2`.

# `client_grant_types_fun`

```elixir
@spec client_grant_types_fun(t()) :: callback() | nil
```

Resolve the `client_grant_types` callback. See `resolve_callback/2`.

# `client_id_fun`

```elixir
@spec client_id_fun(t()) :: callback() | nil
```

Resolve the `client_id` callback. See `resolve_callback/2`.

# `client_id_metadata`

```elixir
@spec client_id_metadata(t()) :: keyword()
```

Returns the merged, defaulted Client ID Metadata Document (CIMD) options.

This is the host's `:client_id_metadata` keyword list merged over the library
defaults (`draft-ietf-oauth-client-id-metadata-document-01` §9), so every
recognized member (`:enabled`, `:fetcher`, `:cache`, `:allow_loopback`,
`:max_document_bytes`, `:request_timeout_ms`, `:cache_ttl_bounds`,
`:require_same_origin_redirect_uri`, `:allowed_hosts`, `:blocked_hosts`) is
always present. `AttestoPhoenix.ClientIdMetadata.Resolver` and the discovery
wiring read the feature's configuration through this helper rather than
reaching into the struct field directly.

# `client_id_metadata_enabled?`

```elixir
@spec client_id_metadata_enabled?(t()) :: boolean()
```

Returns `true` iff Client ID Metadata Document support is enabled.

The feature is off unless the host sets `client_id_metadata: [enabled: true]`.
Discovery advertises `client_id_metadata_document_supported` and the
authorization endpoint resolves a CIMD `client_id` URL only when this is
`true`.

# `client_jwks_fun`

```elixir
@spec client_jwks_fun(t()) :: callback() | nil
```

Resolve the `client_jwks` callback. See `resolve_callback/2`.

# `client_post_logout_redirect_uris`

```elixir
@spec client_post_logout_redirect_uris(t(), term()) :: [String.t()]
```

The Relying Party's registered `post_logout_redirect_uris` (RP-Initiated
Logout 1.0 §2): the `:client_post_logout_redirect_uris` callback's result, or
`[]` when the host wires none (so an unvalidatable `post_logout_redirect_uri`
is always refused).

# `client_post_logout_redirect_uris_fun`

```elixir
@spec client_post_logout_redirect_uris_fun(t()) :: callback() | nil
```

Resolve the `client_post_logout_redirect_uris` callback. See `resolve_callback/2`.

# `client_public_fun`

```elixir
@spec client_public_fun(t()) :: callback() | nil
```

Resolve the `client_public?` callback. See `resolve_callback/2`.

# `client_redirect_uris_fun`

```elixir
@spec client_redirect_uris_fun(t()) :: callback() | nil
```

Resolve the `client_redirect_uris` callback. See `resolve_callback/2`.

# `client_registration_access_token_hash_fun`

```elixir
@spec client_registration_access_token_hash_fun(t()) :: callback() | nil
```

Resolve the `client_registration_access_token_hash` callback. See `resolve_callback/2`.

# `client_requires_dpop_fun`

```elixir
@spec client_requires_dpop_fun(t()) :: callback() | nil
```

Resolve the `client_requires_dpop?` callback. See `resolve_callback/2`.

# `client_requires_mtls_fun`

```elixir
@spec client_requires_mtls_fun(t()) :: callback() | nil
```

Resolve the `client_requires_mtls?` callback. See `resolve_callback/2`.

# `client_store_load`

```elixir
@spec client_store_load(t(), String.t()) :: term()
```

Resolve and load the host's client by `client_id` (RFC 6749 §2.2).

A required callback (`:load_client` / `AttestoPhoenix.ClientStore`); this
helper invokes the resolved callback so consumers do not re-derive it.

# `client_store_verify_secret`

```elixir
@spec client_store_verify_secret(t(), term(), String.t()) :: boolean()
```

Resolve and run the host's constant-time client-secret verification
(RFC 6749 §2.3.1) for `client`/`presented_secret`.

# `consent_fun`

```elixir
@spec consent_fun(t()) :: callback() | nil
```

Resolve the `consent` callback. See `resolve_callback/2`.

# `consent_grant_store`

```elixir
@spec consent_grant_store(t()) :: module() | nil
```

Returns the configured single-use consent-grant store module, or `nil`.

The store implements `AttestoPhoenix.ConsentGrantStore` (the RFC 6749 §4.1.1
request-bound consent primitive). There is no default: the library renders no
consent screen, so a host wires `:consent_grant_store` only when it adopts the
primitive (typically `AttestoPhoenix.Store.EctoConsentGrantStore`). A host's
consent UI and its `:consent` callback read it through this helper.

# `device_authorization`

```elixir
@spec device_authorization(t()) :: keyword()
```

The merged, defaulted RFC 8628 device-authorization options.

# `device_authorization_enabled?`

```elixir
@spec device_authorization_enabled?(t()) :: boolean()
```

Returns `true` iff the RFC 8628 device authorization grant is enabled
(`device_authorization: [enabled: true]`). When enabled, `device_code` is
added to `grant_types_supported/1` and the `device_authorization_endpoint` is
advertised.

# `device_authorization_endpoint_url`

```elixir
@spec device_authorization_endpoint_url(t()) :: String.t()
```

Absolute URL of the device-authorization endpoint (advertised as `device_authorization_endpoint`, RFC 8628 §4).

# `device_authorization_path`

```elixir
@spec device_authorization_path(t()) :: String.t()
```

The resolved request path of the device-authorization endpoint (RFC 8628).

# `device_code_store`

```elixir
@spec device_code_store(t()) :: module() | nil
```

The configured `Attesto.DeviceCodeStore` module, or `nil`.

# `device_verification_endpoint_url`

```elixir
@spec device_verification_endpoint_url(t()) :: String.t()
```

Absolute URL of the device-verification page.

# `device_verification_path`

```elixir
@spec device_verification_path(t()) :: String.t()
```

The resolved request path of the device-verification page (RFC 8628 §3.3).

# `device_verification_uri`

```elixir
@spec device_verification_uri(t()) :: String.t()
```

The RFC 8628 §3.2 verification URI shown to the user: the configured
`device_authorization: [verification_uri: ...]` override, otherwise the
issuer-derived device-verification endpoint URL.

# `end_session_endpoint_url`

```elixir
@spec end_session_endpoint_url(t()) :: String.t()
```

Absolute URL of the end-session endpoint (advertised as `end_session_endpoint`, RP-Initiated Logout 1.0 §2).

# `end_session_path`

```elixir
@spec end_session_path(t()) :: String.t()
```

The resolved request path of the end-session endpoint (RP-Initiated Logout 1.0).

# `from_otp_app`

```elixir
@spec from_otp_app(atom(), atom()) :: t()
```

Reads the config for `otp_app` under `key` (default `AttestoPhoenix`) from the
application environment and builds a validated config.

# `grant_types_supported`

```elixir
@spec grant_types_supported(t()) :: [String.t()]
```

The grant types the authorization server supports.

Advertised as `grant_types_supported` (RFC 8414 §2) by both discovery documents
and enforced by the token endpoint — a `grant_type` outside this set is rejected
as `unsupported_grant_type` before dispatch. Defaults to every grant the token
endpoint implements (["authorization_code", "refresh_token", "client_credentials", "urn:ietf:params:oauth:grant-type:token-exchange"]); configure
`:grant_types_supported` to narrow it, e.g. drop
`urn:ietf:params:oauth:grant-type:token-exchange` to disable token exchange
across discovery, the token endpoint, and dynamic registration at once.

# `introspection_endpoint_url`

```elixir
@spec introspection_endpoint_url(t()) :: String.t()
```

Absolute URL of the token introspection endpoint (RFC 7662): the issuer merged
with `introspection_path/1`. Advertised as `introspection_endpoint`.

# `introspection_path`

```elixir
@spec introspection_path(t()) :: String.t()
```

The resolved request path of the token introspection endpoint (RFC 7662). See
`authorize_path/1`.

# `jwks_uri`

```elixir
@spec jwks_uri(t()) :: String.t()
```

Absolute URL of the JWK Set document (RFC 7517 §5; the `jwks_uri` per
RFC 8414 §2). The JWKS document is anchored at the host root under RFC 8615,
so it is NOT relocated by `:oauth_path_prefix`.

# `jwt_bearer`

```elixir
@spec jwt_bearer(t()) :: keyword()
```

Returns the merged, defaulted Identity Assertion JWT Authorization Grant
(ID-JAG / `jwt-bearer`) options.

This is the host's `:jwt_bearer` keyword merged over the library defaults
(`draft-ietf-oauth-identity-assertion-authz-grant-04`), so every recognized
member (`:enabled`, `:issuers`, `:assertion_max_lifetime_seconds`,
`:jwks_resolver`, `:jwks_fetcher`, `:jwks_cache`, `:jwks_cache_ttl_bounds`,
`:fetch_opts`) is always present.
`AttestoPhoenix.AuthorizationServer.JwtBearer` reads the feature's
configuration through this helper.

# `jwt_bearer_enabled?`

```elixir
@spec jwt_bearer_enabled?(t()) :: boolean()
```

Returns `true` iff the Identity Assertion JWT Authorization Grant (ID-JAG /
`jwt-bearer`) is enabled.

The feature is off unless the host sets `jwt_bearer: [enabled: true, ...]`.
When enabled, `urn:ietf:params:oauth:grant-type:jwt-bearer` is added to
`grant_types_supported/1` (so both discovery and the token endpoint honour it).

# `load_client_fun`

```elixir
@spec load_client_fun(t()) :: callback() | nil
```

Resolve the `load_client` callback. See `resolve_callback/2`.

# `load_principal_fun`

```elixir
@spec load_principal_fun(t()) :: callback() | nil
```

Resolve the `load_principal` callback. See `resolve_callback/2`.

# `logout`

```elixir
@spec logout(t()) :: keyword()
```

The merged, defaulted OpenID Connect logout (RP-Initiated + Back-Channel) options.

# `logout_enabled?`

```elixir
@spec logout_enabled?(t()) :: boolean()
```

Returns `true` iff OpenID Connect logout is enabled (`logout: [enabled: true]`).
When enabled, the `end_session_endpoint` is advertised and (with a
`:logout_session_store` wired) Back-Channel Logout is supported. The host MUST
ALSO pass `logout: true` to `attesto_routes/1` to mount the endpoint.

# `logout_session_store`

```elixir
@spec logout_session_store(t()) :: module() | nil
```

The configured `Attesto.LogoutSessionStore` module (Back-Channel Logout), or `nil`.

# `logout_session_ttl_seconds`

```elixir
@spec logout_session_ttl_seconds(t()) :: pos_integer()
```

How long a recorded back-channel-logout session lives before it is swept, in seconds.

# `new`

```elixir
@spec new(keyword() | map()) :: t()
```

Builds and validates a config from a keyword list or map.

Raises `ArgumentError` if a required key is missing or if a dependent key is
absent for an enabled feature (e.g. `:register_client` when
`:registration_enabled`, or `:cert_der` when `:mtls_enabled`).

# `on_event_fun`

```elixir
@spec on_event_fun(t()) :: callback() | nil
```

Resolve the `on_event` callback. See `resolve_callback/2`.

# `par_endpoint_url`

```elixir
@spec par_endpoint_url(t()) :: String.t()
```

Absolute URL of the pushed-authorization-request endpoint: the issuer merged
with `par_path/1`. Advertised as `pushed_authorization_request_endpoint`
(RFC 9126 §5).

# `par_path`

```elixir
@spec par_path(t()) :: String.t()
```

The resolved request path of the pushed-authorization-request endpoint
(RFC 9126). See `authorize_path/1`.

# `register_client_fun`

```elixir
@spec register_client_fun(t()) :: callback() | nil
```

Resolve the `register_client` callback. See `resolve_callback/2`.

# `registration_client_uri`

```elixir
@spec registration_client_uri(t(), String.t()) :: String.t()
```

Absolute URL of an individual registered client's RFC 7592 management
endpoint: the registration endpoint URL with the URL-encoded `client_id`
appended. Returned as `registration_client_uri` in the RFC 7591 §3.2.1
client information response.

# `registration_default_scope`

```elixir
@spec registration_default_scope(t()) :: [String.t()] | nil
```

The scope a dynamically registered client is assigned when its registration
request omits `scope` (RFC 7591 §2: the authorization server MAY register a
default scope). Resolves the `:registration_default_scope` setting to a
concrete list:

  * `:scopes_supported` — every scope in `scopes_supported`.
  * a list of scope strings — that explicit default (the registration layer
    still rejects any member outside `scopes_supported`).
  * `nil` (the default) — no defaulting; a scopeless registration stays
    scopeless (fail-closed).

Returns the resolved list, or `nil` when no default applies.

# `registration_endpoint_url`

```elixir
@spec registration_endpoint_url(t()) :: String.t()
```

Absolute URL of the dynamic client registration endpoint: the issuer merged
with `registration_path/1`. Advertised as `registration_endpoint` (RFC 7591
§3) only when registration is enabled.

# `registration_path`

```elixir
@spec registration_path(t()) :: String.t()
```

The resolved request path of the dynamic client registration endpoint
(RFC 7591). See `authorize_path/1`.

# `resolve_callback`

```elixir
@spec resolve_callback(t(), atom()) :: callback() | nil
```

Resolve a configured callback by its flat `key`.

Precedence (see the "Behaviour-module Config keys" section): the explicit
flat key wins when set; otherwise the installed behaviour module wins when it
exports the corresponding behaviour callback; otherwise `nil`. The result is a
value an `AttestoPhoenix.Callback.invoke/2,3` caller can run (an anonymous
function, a `{module, function}` pair, a `{module, function, extra_args}`
triple), or `nil`.

# `resolve_jwt_bearer_subject_fun`

```elixir
@spec resolve_jwt_bearer_subject_fun(t()) :: callback() | nil
```

Resolve the `resolve_jwt_bearer_subject` callback. See `resolve_callback/2`.

# `resource_indicators`

```elixir
@spec resource_indicators(t()) :: keyword()
```

Returns the merged, defaulted RFC 8707 Resource Indicators options
(`:allowed_resources`, `:allowed_resources_for`).

# `revocation_endpoint_url`

```elixir
@spec revocation_endpoint_url(t()) :: String.t()
```

Absolute URL of the revocation endpoint: the issuer merged with
`revocation_path/1`. Advertised as `revocation_endpoint` (RFC 8414 §2,
RFC 7009).

# `revocation_path`

```elixir
@spec revocation_path(t()) :: String.t()
```

The resolved request path of the revocation endpoint (RFC 7009). See
`authorize_path/1`.

# `to_attesto_config`

```elixir
@spec to_attesto_config(
  t(),
  keyword()
) :: Attesto.Config.t()
```

Derives the `Attesto.Config` consumed by the protocol layer from this config.

The protocol layer owns only the claim-level policy (`:issuer`, `:audience`,
`:keystore`, the principal kinds, and the default access-token lifetime). The
refresh/code TTLs and the DPoP/mTLS feature toggles are read directly from
this struct by the controllers and plugs, so they are not duplicated into the
`Attesto.Config`.

Pass `principal_kinds:` (a non-empty list of `Attesto.PrincipalKind`) and any
other `Attesto.Config.new/1` option as `extra` to complete the protocol
config; they are merged over the values derived here.

# `token_endpoint_url`

```elixir
@spec token_endpoint_url(t()) :: String.t()
```

Absolute URL of the token endpoint: the issuer merged with `token_path/1`.
Advertised as `token_endpoint` (RFC 8414 §2).

# `token_path`

```elixir
@spec token_path(t()) :: String.t()
```

The resolved request path of the token endpoint. See `authorize_path/1`.

# `unregister_client_fun`

```elixir
@spec unregister_client_fun(t()) :: callback() | nil
```

Resolve the `unregister_client` callback. See `resolve_callback/2`.

# `userinfo_endpoint_url`

```elixir
@spec userinfo_endpoint_url(t()) :: String.t()
```

Absolute URL of the UserInfo endpoint: the issuer merged with
`userinfo_path/1`. Used when the host does not supply a separate
`:userinfo_endpoint`.

# `userinfo_path`

```elixir
@spec userinfo_path(t()) :: String.t()
```

The resolved request path of the UserInfo endpoint (OpenID Connect Core
§5.3). See `authorize_path/1`.

# `verify_client_secret_fun`

```elixir
@spec verify_client_secret_fun(t()) :: callback() | nil
```

Resolve the `verify_client_secret` callback. See `resolve_callback/2`.

---

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