### Run the installer and generate migration Source: https://hexdocs.pm/attesto_phoenix Commands to install attesto_phoenix and generate the database migration. ```shell mix deps.get mix attesto_phoenix.install mix attesto_phoenix.gen.migration --repo MyApp.Repo mix ecto.migrate ``` -------------------------------- ### Install with custom schema prefix and generate migration Source: https://hexdocs.pm/attesto_phoenix Installs attesto_phoenix and generates a migration with a custom PostgreSQL schema prefix. ```shell mix attesto_phoenix.install --schema-prefix oauth mix attesto_phoenix.gen.migration --repo MyApp.Repo --schema-prefix oauth ``` -------------------------------- ### Install with custom OAuth path prefix Source: https://hexdocs.pm/attesto_phoenix Installs attesto_phoenix with a custom OAuth path prefix. ```shell mix attesto_phoenix.install --oauth-path-prefix /mcp/oauth ``` -------------------------------- ### Configure client_native? callback Source: https://hexdocs.pm/attesto_phoenix Sets the `:client_native?` callback in the `AttestoPhoenix.Config` configuration. The callback returns a boolean indicating whether a client is an installed app; default is `false`. When `true`, the client gets the RFC 8252 profile. ```elixir config :my_app, AttestoPhoenix.Config, client_native?: {MyApp.OAuth, :client_native?} # (client -> boolean), default false ``` -------------------------------- ### Add attesto_phoenix with optional Igniter installer Source: https://hexdocs.pm/attesto_phoenix Adds attesto_phoenix and the optional Igniter installer as a dev-only dependency. ```elixir def deps do [ {:attesto_phoenix, "~> 3.4"}, {:igniter, "~> 0.6", only: [:dev], runtime: false} ] end ``` -------------------------------- ### Generate 3.0, 3.1, and 3.4 upgrade migrations Source: https://hexdocs.pm/attesto_phoenix Use these commands in order to upgrade an existing 2.x database to 3.x. Do not use the fresh migration command for existing databases. Follow the stopped-cutover guide before running these. ```shell mix attesto_phoenix.gen.migration --upgrade 3.0 --repo MyApp.Repo mix attesto_phoenix.gen.migration --upgrade 3.1 --repo MyApp.Repo mix attesto_phoenix.gen.migration --upgrade 3.4 --repo MyApp.Repo ``` -------------------------------- ### Configure authorization_code_private_context and completion callback Source: https://hexdocs.pm/attesto_phoenix Configuration example for attaching a private context function and a completion callback in the AttestoPhoenix.Config. The private context is set at the authorization endpoint and read back at completion. The completion callback must return an error if the host transaction rolls back. ```elixir config :my_app, AttestoPhoenix.Config, authorization_code_private_context: &MyApp.Policy.private_context/1, authorization_code_completion: &MyApp.Policy.complete/2 ``` -------------------------------- ### GET /.well-known/openid-federation Source: https://hexdocs.pm/attesto_phoenix Serves the signed OpenID Federation Entity Configuration (application/entity-statement+jwt) built from :federation_authority_hints and :federation_entity_metadata. ```APIDOC ## GET /.well-known/openid-federation ### Description Serves the signed OpenID Federation Entity Configuration (application/entity-statement+jwt) built from :federation_authority_hints and :federation_entity_metadata. ### Method GET ### Endpoint /.well-known/openid-federation ### Response #### Success Response (200) - **body** (string) - The signed entity configuration JWT. #### Response Example { "entity_configuration": "eyJhbGciOiJFUzI1NiJ9..." } ``` -------------------------------- ### GET /oauth/credential_offer/:id Source: https://hexdocs.pm/attesto_phoenix Retrieves a credential offer by reference (the credential_offer_uri that a wallet dereferences). ```APIDOC ## GET /oauth/credential_offer/:id ### Description Retrieves a credential offer by reference (the credential_offer_uri that a wallet dereferences). ### Method GET ### Endpoint /oauth/credential_offer/:id ### Parameters #### Path Parameters - **id** (string) - Required - The identifier of the credential offer. ### Response #### Success Response (200) - **body** (object) - The credential offer object. #### Response Example { "credential_issuer": "https://example.com", "credential_configurations": ["example_credential"], "grants": { "urn:ietf:params:oauth:grant-type:pre-authorized_code": { "pre-authorized_code": "abc123" } } } ``` -------------------------------- ### GET /oauth/device_verification Source: https://hexdocs.pm/attesto_phoenix Serves the verification page where the user enters the user code from the device authorization step. ```APIDOC ## GET /oauth/device_verification ### Description Serves the verification page where the user enters the user code from the device authorization step. ### Method GET ### Endpoint /oauth/device_verification ### Parameters #### Query Parameters - **user_code** (string) - Optional - The user code to pre-fill. ### Response #### Success Response (200) - **HTML page** - The verification page. #### Response Example (HTML content) ``` -------------------------------- ### GET /oauth/presentation_request/:id Source: https://hexdocs.pm/attesto_phoenix Serves the signed request object for OID4VP presentation. The response is of content type application/oauth-authz-req+jwt. ```APIDOC ## GET /oauth/presentation_request/:id ### Description Serves the signed request object for OID4VP presentation. The response is of content type application/oauth-authz-req+jwt. ### Method GET ### Endpoint /oauth/presentation_request/:id ### Parameters #### Path Parameters - **id** (string) - Required - The identifier of the presentation request. ### Response #### Success Response (200) - **body** (string) - The signed request object as a JWT. #### Response Example { "request": "eyJhbGciOiJFUzI1NiJ9..." } ``` -------------------------------- ### POST /oauth/bc-authorize Source: https://hexdocs.pm/attesto_phoenix Starts a CIBA (Client-Initiated Backchannel Authentication) flow for decoupled authentication. The user approves the request out of band, and the client collects tokens at the token endpoint. ```APIDOC ## POST /oauth/bc-authorize ### Description Starts a CIBA (Client-Initiated Backchannel Authentication) flow for decoupled authentication. The user approves the request out of band, and the client collects tokens at the token endpoint. ### Method POST ### Endpoint /oauth/bc-authorize ### Parameters #### Request Body - **grant_type** (string) - Required - Must be `urn:openid:params:grant-type:ciba`. - **client_id** (string) - Required - The client identifier. - **client_secret** (string) - Required - The client secret. - **login_hint** (string) - Required - Identifier for the user to authenticate. - **scope** (string) - Optional - Space-delimited list of requested scopes. - **request** (string) - Optional - Signed authentication request (JWT) following the FAPI-CIBA profile. ### Response #### Success Response (200) - **auth_req_id** (string) - The authentication request identifier. - **expires_in** (integer) - Lifetime of the auth_req_id in seconds. - **interval** (integer) - Minimum polling interval in seconds. #### Response Example { "auth_req_id": "...", "expires_in": 120, "interval": 5 } ``` -------------------------------- ### GET /oauth/statuslist/:id Source: https://hexdocs.pm/attesto_phoenix Serves a signed statuslist+jwt built from the :status_list_store. This is the revocation target referenced by issued credentials. ```APIDOC ## GET /oauth/statuslist/:id ### Description Serves a signed statuslist+jwt built from the :status_list_store. This is the revocation target referenced by issued credentials. ### Method GET ### Endpoint /oauth/statuslist/:id ### Parameters #### Path Parameters - **id** (string) - Required - The identifier of the status list. ### Response #### Success Response (200) - **body** (string) - The signed status list JWT. #### Response Example { "status_list": "eyJhbGciOiJFUzI1NiJ9..." } ``` -------------------------------- ### GET /.well-known/openid-credential-issuer Source: https://hexdocs.pm/attesto_phoenix Returns the Credential Issuer Metadata for the OID4VCI issuer role. This endpoint is mounted when the credential_issuance feature flag is enabled. ```APIDOC ## GET /.well-known/openid-credential-issuer ### Description Returns the Credential Issuer Metadata for the OID4VCI issuer role. ### Method GET ### Endpoint /.well-known/openid-credential-issuer ### Response #### Success Response (200) - **body** (object) - Credential Issuer Metadata as defined by OID4VCI. #### Response Example { "credential_issuer": "https://example.com", "credential_endpoint": "https://example.com/oauth/credential", "credential_configurations_supported": {} } ``` -------------------------------- ### GET /oauth/check_session Source: https://hexdocs.pm/attesto_phoenix Serves the check_session_iframe for OpenID Connect Session Management, allowing RPs to detect changes to the OP login session. ```APIDOC ## GET /oauth/check_session ### Description Serves the check_session_iframe for OpenID Connect Session Management, allowing RPs to detect changes to the OP login session. ### Method GET ### Endpoint /oauth/check_session ### Parameters None ### Response #### Success Response (200) - **HTML page** - The check_session_iframe content. #### Response Example (HTML content) ``` -------------------------------- ### Development commands: mix deps.get, mix precommit, mix test Source: https://hexdocs.pm/attesto_phoenix Run these commands in order to set up and test the project. The test command requires Postgres. ```shell mix deps.get mix precommit mix test --include ecto # requires Postgres ``` -------------------------------- ### Configure native_apps options Source: https://hexdocs.pm/attesto_phoenix Sets the server-wide `native_apps` options in the `AttestoPhoenix.Config` configuration. `loopback_include_localhost: true` accepts a varying port for `http://localhost/...` redirects; `reject_embedded_user_agents: true` refuses authorization requests from in-app webviews. Both are opt-in and default to `false`. ```elixir config :my_app, AttestoPhoenix.Config, native_apps: [ loopback_include_localhost: true, reject_embedded_user_agents: true ] ``` -------------------------------- ### Configure AttestoPhoenix in config/config.exs Source: https://hexdocs.pm/attesto_phoenix Sets required and optional configuration for AttestoPhoenix in the host application's config.exs. Required keys include issuer, audience, keystore, repo, and principal_kinds. Optional keys cover policy modules, token TTLs, DPoP, mTLS, and resource indicators. The otp_app and repo keys at the top point controllers and Ecto-backed stores at the host application. ```elixir # config/config.exs # Points controllers and Ecto-backed stores at the host application. config :attesto_phoenix, otp_app: :my_app, repo: MyApp.Repo config :my_app, AttestoPhoenix.Config, # --- required --- issuer: "https://auth.example.com", audience: "https://api.example.com", keystore: MyApp.Keystore, # implements Attesto.Keystore repo: MyApp.Repo, # Ecto.Repo for the token stores principal_kinds: {MyApp.OAuth.PrincipalStore, :principal_kinds}, # host policy modules (preferred install surface) client_store: MyApp.OAuth.ClientStore, principal_store: MyApp.OAuth.PrincipalStore, scope_policy: MyApp.OAuth.ScopePolicy, consent_policy: MyApp.OAuth.ConsentPolicy, claims_provider: MyApp.OIDC.ClaimsProvider, event_sink: MyApp.OAuth.Events, # --- optional policy --- openid_provider: true, scopes_supported: ["profile", "email", "read:*", "write:*"], protected_resource_scopes_supported: ["read:*", "write:*"], send_error: &MyApp.OAuthErrors.render/3, # (conn, status, body_map -> conn), optional custom OAuth error envelope client_auth_signing_algs: Attesto.SigningAlg.fapi_algs(), client_auth_enforce_fapi_alg_policy: true, request_object_policy: Attesto.RequestObject.Policy.generic(), # --- optional deployment + features --- # Incoming request transport gate only; issuer and advertised endpoint URLs # remain HTTPS-only regardless of this value. require_https: true, trusted_proxies: ["10.0.0.0/8"], # honor X-Forwarded-* only from these access_token_ttl: 900, refresh_token_ttl: 1_209_600, refresh_token_max_lifetime: nil, # optional fixed family limit, seconds refresh_token_expiration_metadata: false, # experimental refresh-expiration draft03 refresh_token_rotation_grace_seconds: 60, schema_prefix: nil, # Ecto/repo connection default/search_path (often `public`) sweep_interval_ms: 60_000, authorization_code_ttl: 60, authorization_grant_id_claim: "https://api.example.com/claims/oauth_grant_id", dpop_enabled: true, dpop_nonce_required: false, mtls_enabled: false, # RFC 8705 certificate-bound tokens # The terminator must overwrite, never append/forward, the client-cert header. # This callback is invoked only from trusted_proxies: forwarded_cert_der: &MyApp.TLS.forwarded_client_cert_der/1, client_certificate_chain_validated?: &MyApp.TLS.chain_validated?/2, token_endpoint_auth_methods_supported: ["private_key_jwt", "tls_client_auth"], client_mtls_metadata: &MyApp.OAuth.Clients.mtls_metadata/1, mtls_endpoint_aliases: %{ "token_endpoint" => "https://mtls.auth.example.com/oauth/token" }, registration_enabled: false, # if true, also set registration callbacks # RFC 8707 resource indicators (optional; see below) resource_indicators: [ allowed_resources: ["https://api.example.com/a", "https://api.example.com/b"], allowed_resources_for: {MyApp.OAuth, :resources_for} # optional per-client (client -> [uri]) ] ``` -------------------------------- ### Generate complete migration for fresh database Source: https://hexdocs.pm/attesto_phoenix Use this command to generate the complete schema migration for a fresh database. It creates the operational tables backing the attesto store behaviours. The command uses the --repo flag to specify the Ecto repository. ```shell mix attesto_phoenix.gen.migration --repo MyApp.Repo ``` -------------------------------- ### Generate upgrade migrations for 3.0 and 3.1 Source: https://hexdocs.pm/attesto_phoenix Run these Mix commands to generate the upgrade migrations. The first pair uses the configured schema prefix (or the Ecto default, normally public). The second pair uses a dedicated PostgreSQL schema via --schema-prefix oauth. Remove any legacy :table_prefix setting before running; the generator refuses that key. ```shell # Use the configured schema_prefix; without one, the migration defers to the # Ecto migrator/repo default (normally public). Keep that aligned with the # runtime connection search path: mix attesto_phoenix.gen.migration --upgrade 3.0 --repo MyApp.Repo mix attesto_phoenix.gen.migration --upgrade 3.1 --repo MyApp.Repo # Dedicated PostgreSQL schema: mix attesto_phoenix.gen.migration --upgrade 3.0 --repo MyApp.Repo --schema-prefix oauth mix attesto_phoenix.gen.migration --upgrade 3.1 --repo MyApp.Repo --schema-prefix oauth ``` -------------------------------- ### Implement authorization_code_completion callback Source: https://hexdocs.pm/attesto_phoenix Defines the MyApp.Policy module with private_context/1 and complete/2. The complete/2 callback wraps the continuation in a database transaction, revalidates authorization, and rolls back on failure. It must return the continuation's result unchanged. ```elixir defmodule MyApp.Policy do def private_context(context) do %{"security_epoch" => epoch_for(context.subject)} end def complete(context, continuation) do MyApp.Repo.transaction(fn -> # Lock and revalidate the subject's authorization under the same # transaction the token issuance will commit in. case still_authorized?(context.subject, context.private_context) do true -> case continuation.() do {:ok, _response, _events} = ok -> ok {:error, _oauth_error} = error -> MyApp.Repo.rollback(error) end false -> error = AttestoPhoenix.OAuthError.new( :invalid_grant, "subject authorization is no longer valid" ) MyApp.Repo.rollback({:error, error}) end end) |> case do {:ok, result} -> result {:error, {:error, _} = failure} -> failure {:error, reason} -> {:error, reason} end end end ``` -------------------------------- ### Configure dev endpoint with AttestoPhoenix.DevTLS.https_opts Source: https://hexdocs.pm/attesto_phoenix Add this line to config/dev.exs to serve the endpoint over HTTPS using the generated certificate. The function raises if the certificate is missing and never falls back to http. ```elixir config :my_app, MyAppWeb.Endpoint, https: AttestoPhoenix.DevTLS.https_opts(port: 4443) ``` -------------------------------- ### Run Ecto migrations Source: https://hexdocs.pm/attesto_phoenix Run this command to apply the generated migrations to the database. It is used after generating the migration files. ```shell mix ecto.migrate ``` -------------------------------- ### Configure static resource metadata and resolver Source: https://hexdocs.pm/attesto_phoenix Shows configuration for static `resource_metadata` and a resolver function. The resolver is authoritative when present and does not fall back to the static URL when it returns `nil`. The resolver runs once per protected-resource request and must be fast and total. ```elixir resource_metadata: "https://api.example/.well-known/oauth-protected-resource", resource_metadata_resolver: {MyAppWeb.ResourceMetadata, :for_request} ``` ```elixir def for_request(%Plug.Conn{request_path: "/alpha"}) do "https://api.example/.well-known/oauth-protected-resource/alpha" end def for_request(%Plug.Conn{request_path: "/beta"}) do "https://api.example/.well-known/oauth-protected-resource/beta" end def for_request(_conn), do: nil ``` -------------------------------- ### Generate local HTTPS certificate with mix attesto_phoenix.gen.dev_https Source: https://hexdocs.pm/attesto_phoenix Run this mix task once to generate a locally-trusted mkcert certificate for development. Required because attestto requires an https issuer and there is no disable-https switch. ```shell mix attesto_phoenix.gen.dev_https ``` -------------------------------- ### Configure build_refresh_principal callback Source: https://hexdocs.pm/attesto_phoenix Set the :build_refresh_principal option in the AttestoPhoenix.Config to a callback that checks the principal and session state during refresh. The callback receives the client and a map with refresh context fields; return the principal map to allow refresh or {:error, :invalid_grant} to deny and revoke the family. ```elixir config :my_app, AttestoPhoenix.Config, build_refresh_principal: &MyApp.OAuth.PrincipalStore.build_refresh_principal/2 ``` -------------------------------- ### Mount Attesto routes with router macro Source: https://hexdocs.pm/attesto_phoenix Defines a router module that uses AttestoPhoenix.Router and mounts routes under a scope with a custom pipeline that sets the OTP app. ```elixir defmodule MyAppWeb.Router do use MyAppWeb, :router use AttestoPhoenix.Router pipeline :attesto_phoenix_config do plug AttestoPhoenix.Plug.PutConfig, otp_app: :my_app end scope "/" do attesto_routes(pipeline: :attesto_phoenix_config) end end ``` -------------------------------- ### Configure protected pipelines and scopes Source: https://hexdocs.pm/attesto_phoenix Defines pipelines for authentication and scope authorization, then applies them to a scope. `AttestoPhoenix.Plug.Authenticate` verifies the Bearer JWT, enforces DPoP and mTLS binding when enabled, and resolves the subject via `:load_principal`. `AttestoPhoenix.Plug.RequireScopes` enforces route-level scope authorization using `Attesto.Scope` grant-form algebra. ```elixir pipeline :api_protected do plug AttestoPhoenix.Plug.Authenticate end pipeline :reports_read do plug AttestoPhoenix.Plug.RequireScopes, "read:reports" end scope "/api", MyAppWeb do pipe_through [:api, :api_protected] scope "/reports" do pipe_through :reports_read get "/", ReportController, :index end end ``` -------------------------------- ### Enable CIMD for native clients in config Source: https://hexdocs.pm/attesto_phoenix Configuration to enable Client ID Metadata (CIMD) for native clients, with allowed hosts and loopback include localhost option. The default CIMD fetcher requires the optional Req dependency. Keep allowed_hosts narrow when clients are known in advance. The localhost option only affects redirect matching, not outbound fetch guard. ```elixir config :my_app, AttestoPhoenix.Config, client_id_metadata: [ enabled: true, allowed_hosts: ["client.example"] ], native_apps: [loopback_include_localhost: true] ``` -------------------------------- ### Configure Plug.Parsers with Attesto body reader Source: https://hexdocs.pm/attesto_phoenix Configures the endpoint's Plug.Parsers to use AttestoPhoenix.DuplicateParameterGuard as the body reader to detect repeated parameters before dispatch. ```elixir plug Plug.Parsers, parsers: [:urlencoded, :multipart, :json], pass: ["*/*"], json_decoder: Phoenix.json_library(), body_reader: {AttestoPhoenix.DuplicateParameterGuard, :read_body, []} ``` -------------------------------- ### Configure route pipelines for interactive routes Source: https://hexdocs.pm/attesto_phoenix Classifies generated routes into interactive and protocol pipelines, allowing host session support for interactive routes while keeping protocol routes separate. ```elixir attesto_routes( pipeline: :oauth_common, route_pipelines: [ interactive: [:oauth_interactive, :oauth_common] ], registration: true ) ``` -------------------------------- ### Disable OIDC routes and set openid_provider false Source: https://hexdocs.pm/attesto_phoenix Shows how to omit userinfo and openid_configuration routes and set openid_provider to false in runtime config for an OAuth-only server. ```elixir attesto_routes( userinfo: false, openid_configuration: false ) ``` ```elixir config :my_app, AttestoPhoenix.Config, openid_provider: false, scopes_supported: ["api.read"] ``` -------------------------------- ### Build validated AttestoPhoenix.Config struct Source: https://hexdocs.pm/attesto_phoenix Constructs the validated configuration struct from the OTP application environment. Required keys are validated at build time to fail fast on misconfiguration. ```elixir config = AttestoPhoenix.Config.from_otp_app(:my_app) ``` -------------------------------- ### Configure Ecto stores for clustering Source: https://hexdocs.pm/attesto_phoenix Default store configuration from `mix attesto_phoenix.install` for clustered deployments. All mutable OAuth stores use Postgres-backed Ecto implementations to avoid per-node state. Requires the stable `ATTESTO_REFRESH_SUCCESSOR_SECRET` runtime setting when `refresh_token_rotation_grace_seconds` is greater than zero (the default). For FAPI 2.0 with `require_pushed_authorization_requests: true`, the PAR store must implement atomic `take/1`; configuration rejects fetch-only stores. ```elixir code_store: AttestoPhoenix.Store.EctoCodeStore, refresh_store: AttestoPhoenix.Store.EctoRefreshStore, nonce_store: AttestoPhoenix.Store.EctoNonceStore, replay_check: {AttestoPhoenix.Store.EctoReplayCheck, :check_and_record}, par_store: AttestoPhoenix.Store.EctoPARStore ``` -------------------------------- ### Remount UserInfo route with userinfo: false Source: https://hexdocs.pm/attesto_phoenix Shows a host remounting its own UserInfo controller at the canonical path while disabling the bundled route. The scope pipes through :attesto_phoenix_config and uses attesto_routes(userinfo: false). ```elixir scope "/" do pipe_through :attesto_phoenix_config attesto_routes(userinfo: false) get "/oauth/userinfo", MyAppWeb.UserInfoController, :show end ``` -------------------------------- ### Add attesto_phoenix dependency Source: https://hexdocs.pm/attesto_phoenix Adds the attesto_phoenix package to your Mix dependencies. ```elixir def deps do [ {:attesto_phoenix, "~> 3.4"} ] end ``` -------------------------------- ### Supervise AttestoPhoenix.Store.Sweeper after repo Source: https://hexdocs.pm/attesto_phoenix Manual wiring for the sweeper in an application's supervision tree. Must be placed after the repo. The sweeper removes expired rows and redacts encrypted successors after the retry window. Use one sweeper per {repo, schema_prefix} pair. ```elixir # lib/my_app/application.ex children = [ MyApp.Repo, {AttestoPhoenix.Store.Sweeper, config: AttestoPhoenix.Config.from_otp_app(:my_app)} ] ``` -------------------------------- ### POST /oauth/device_authorization Source: https://hexdocs.pm/attesto_phoenix Initiates the Device Authorization Grant flow for input-constrained devices. Returns a device code and a user code that the user enters on a separate device. ```APIDOC ## POST /oauth/device_authorization ### Description Initiates the Device Authorization Grant flow for input-constrained devices. Returns a device code and a user code that the user enters on a separate device. ### Method POST ### Endpoint /oauth/device_authorization ### Parameters #### Request Body - **client_id** (string) - Required - The client identifier. - **scope** (string) - Optional - Space-delimited list of requested scopes. ### Response #### Success Response (200) - **device_code** (string) - The device verification code. - **user_code** (string) - The short human-typable code. - **verification_uri** (string) - The URL where the user enters the user code. - **expires_in** (integer) - Lifetime of the device_code in seconds. - **interval** (integer) - Minimum polling interval in seconds. #### Response Example { "device_code": "...", "user_code": "...", "verification_uri": "https://example.com/oauth/device_verification", "expires_in": 1800, "interval": 5 } ``` -------------------------------- ### AttestoPhoenix.Plug.Authenticate Source: https://hexdocs.pm/attesto_phoenix Verifies the Bearer JWT, enforces DPoP and mTLS binding when enabled, resolves the subject via :load_principal, and emits auth events. It assigns attest_claims, attest_principal, and attest_context to the connection. ```APIDOC ## AttestoPhoenix.Plug.Authenticate ### Description Verifies the Bearer JWT, enforces DPoP and mTLS binding when enabled, resolves the subject via `:load_principal`, and emits neutral `:auth_succeeded` / `:auth_denied` events through `:on_event`. When `conn.private[:attesto_phoenix_config]` is present, that validated request config is authoritative; plug `:config`/`:otp_app` options are used only when the request-private value is absent. A malformed request-private value fails closed rather than falling back to global application configuration. Mount `AttestoPhoenix.Plug.PutConfig` in the pipeline when the plug should use a request-specific profile. ### Usage ```elixir pipeline :api_protected do plug AttestoPhoenix.Plug.Authenticate end ``` ### Assigns - `conn.assigns.attesto_claims` - the verified JWT claims - `conn.assigns.attesto_principal` - the host principal returned by `:load_principal` - `conn.assigns.attesto_context` - a neutral `%{subject, client_id, scope, claims, cnf, principal}` map ### Options - `:config` - configuration map, used only when `conn.private[:attesto_phoenix_config]` is absent - `:otp_app` - OTP application for configuration, used only when `conn.private[:attesto_phoenix_config]` is absent - `:credential_from_conn` - function to extract a generic credential from the connection (e.g., from a cookie) - `:resource_metadata` - static RFC 9728 resource metadata pointer - `:resource_metadata_resolver` - MFA or function to select resource metadata per request ### Bearer Credentials Bearer credentials default to the `Authorization` header only, matching `bearer_methods_supported: ["header"]` in protected-resource metadata. Configure `bearer_methods_supported: ["header", "body"]` only for resource servers that intentionally accept RFC 6750 form-body `access_token` credentials. ### Resource Metadata Resolver The resolver is authoritative when present; it does not fall back to the static URL when it returns `nil`. An invalid runtime return is safely omitted rather than turned into a challenge or a request-time exception. A static Config value is validated by `AttestoPhoenix.Config.new/1`; a non-`nil` per-plug value is validated when `AttestoPhoenix.Plug.Authenticate` is initialized (at compile time under Phoenix's default Plug initialization mode). Explicit per-plug `nil` remains a valid, authoritative omission. Function callbacks must accept one argument, and MFA tuples must export the effective arity (the request plus any extra arguments, which are appended after it). The explicit per-plug option wins on core verification, TLS, revocation, and principal failures and skips the resolver. ### Resolver Example ```elixir def for_request(%Plug.Conn{request_path: "/alpha"}) do "https://api.example/.well-known/oauth-protected-resource/alpha" end def for_request(%Plug.Conn{request_path: "/beta"}) do "https://api.example/.well-known/oauth-protected-resource/beta" end def for_request(_conn), do: nil ``` ### Security Notes The resolver is trusted configuration. Return pinned or allowlisted HTTPS URLs; do not construct a metadata authority from untrusted Host, forwarded, query, or arbitrary header values. The returned URL is never fetched or used as a redirect, and it is validated with the same HTTPS/host/no-fragment rules as the static value before it can enter a quoted challenge. The resolver runs once per protected-resource request — including requests that authenticate successfully, since the pointer must be selected before verification renders any challenge — so keep it fast and total. Resolver exceptions are not rescued: a callback that raises propagates the exception and fails the request (successful ones included) instead of rendering an authentication challenge. ### Example with Cookie Credential Extraction ```elixir plug AttestoPhoenix.Plug.Authenticate, credential_from_conn: &MyAppWeb.Auth.access_token_from_cookie/1 ``` ``` -------------------------------- ### authorization_code_completion Source: https://hexdocs.pm/attesto_phoenix Configuration hook that wraps the tail of an authorization-code redemption in a synchronous callback, allowing the host to serialize token issuance with its own database transaction. ```APIDOC ## authorization_code_completion ### Description Wrap the whole tail of an authorization-code redemption — principal construction, host ID-token claim construction, access- and ID-token minting, optional logout-session recording, access-token `jti` recording, optional generation-0 refresh issuance, and code finalization — in one synchronous callback, so a host can serialize it with its own database transaction. This is an **initial authorization-code completion hook only**. Scope policy and resource-indicator resolution have already completed before it runs. Refresh, device-code, CIBA, pre-authorized-code, JWT-bearer, client-credentials, and token-exchange grants all bypass it. It is not a per-request reauthorization hook for a resource server or MCP server; enforce request-time policy where the access token is used. ### Configuration ``` config :my_app, AttestoPhoenix.Config, authorization_code_completion: &MyApp.Policy.complete/2 ``` ### Callback Signature `complete(context, continuation)` ### Parameters - **context** (map) - Required - Contains only the authenticated `:client_id`, the grant `:subject`, its `:family_id`, and the host's `:private_context`. Never contains the authorization code or a minted token secret. - **continuation** (function) - Required - A closure that completes the token issuance. It must be invoked exactly once within the callback. The callback must return the first invocation's result unchanged. ### Behavior - The callback runs in the same process as the continuation. - The continuation is bound to the callback's process and permits exactly one invocation; a second, cross-process, or escaped call is refused before anything is minted or persisted. - The callback must return the first invocation's result unchanged. - A callback that never invokes the continuation cannot pass off a fabricated `{:ok, response, events}` as a token set. - The `{:ok, _}` wrapper `Repo.transaction/1` puts around a committed return is unwrapped rather than refused, because that commit already carried the mint, refresh insert, and finalization. - A success wrapper around a failed continuation result is also unwrapped so the wire error is preserved, but it emits a static warning: unless the callback rolled the transaction back, earlier continuation writes may have committed. - If the continuation raises, throws, or exits before returning and the callback catches that termination, AttestoPhoenix accepts an OAuth error response but emits a static warning that partial writes may have occurred. A success returned in place of the missing continuation result is refused. - Only that single transaction wrapper is accepted. Nested transactions produce additional `{:ok, ...}` layers, and `Ecto.Multi` returns a result map; both are refused unless the callback unwraps them and returns the continuation's exact result. ### Transaction Boundaries - Rollback covers only stores that use the **same Ecto Repo and enclosing transaction** as the callback. Set the `:repo` key of the request-local `AttestoPhoenix.Config` to the Repo used by the callback. The package-level `config :attesto_phoenix, :repo` setting is only a legacy fallback when no `:otp_app` pointer exists. A custom store that uses another Repo, opens and commits its own transaction, or writes to an external service is outside this rollback boundary; its writes will not be undone with the host transaction. ### Security Notes - The continuation closure captures completion state, including the plaintext authorization code. Trusted host code MUST NOT inspect or dump the function's environment. - The callback is trusted host code running in the same process. The private process entry is a correctness guard against accidental interference (including broad mailbox receives) and contract mistakes, not a sandbox against a hostile callback that deliberately inspects or mutates its own process dictionary. ### Example ```elixir defmodule MyApp.Policy do def private_context(context) do %{"security_epoch" => epoch_for(context.subject)} end def complete(context, continuation) do MyApp.Repo.transaction(fn -> # Lock and revalidate the subject's authorization under the same # transaction the token issuance will commit in. case still_authorized?(context.subject, context.private_context) do true -> case continuation.() do {:ok, _response, _events} = ok -> ok {:error, _oauth_error} = error -> MyApp.Repo.rollback(error) end false -> error = AttestoPhoenix.OAuthError.new( :invalid_grant, "subject authorization is no longer valid" ) MyApp.Repo.rollback({:error, error}) end end) |> case do {:ok, result} -> result {:error, {:error, _} = failure} -> failure {:error, reason} -> {:error, reason} end end end ``` ``` -------------------------------- ### attesto_routes configuration for OID4VC features Source: https://hexdocs.pm/attesto_phoenix Configures the attest_phoenix routes for OID4VCI issuer, OID4VP verifier, status list, and federation. Each flag mounts the corresponding endpoints only when set to true. ```elixir attesto_routes( credential_issuance: true, # OID4VCI issuer presentation: true, # OID4VP verifier status_list: true, # Token Status List revocation federation: true # OpenID Federation entity configuration ) ``` -------------------------------- ### POST /oauth/presentation_response Source: https://hexdocs.pm/attesto_phoenix The direct_post (and encrypted direct_post.jwt) response endpoint for OID4VP. Verifies the vp_token against the session's bound nonce/audience (single-use, uniform error). Supports SD-JWT VC and mdoc presentations. ```APIDOC ## POST /oauth/presentation_response ### Description The direct_post (and encrypted direct_post.jwt) response endpoint for OID4VP. Verifies the vp_token against the session's bound nonce/audience (single-use, uniform error). Supports SD-JWT VC and mdoc presentations. ### Method POST ### Endpoint /oauth/presentation_response ### Parameters #### Request Body - **vp_token** (string) - Required - The presentation token (SD-JWT VC or mdoc). - **presentation_submission** (object) - Required - The presentation submission object. - **state** (string) - Required - The state identifier for the session. ### Request Example { "vp_token": "eyJhbGciOiJFUzI1NiJ9...", "presentation_submission": { "id": "ps_123", "definition_id": "def_123", "descriptor_map": [] }, "state": "state_123" } ### Response #### Success Response (200) - **status** (string) - The status of the verification (e.g., `ok`). #### Response Example { "status": "ok" } ``` -------------------------------- ### POST /oauth/nonce Source: https://hexdocs.pm/attesto_phoenix Public endpoint that returns a c_nonce for the OID4VCI issuer. This is used by wallets to obtain a nonce for proof of possession. ```APIDOC ## POST /oauth/nonce ### Description Public endpoint that returns a c_nonce for the OID4VCI issuer. ### Method POST ### Endpoint /oauth/nonce ### Response #### Success Response (200) - **c_nonce** (string) - The nonce value. - **c_nonce_expires_in** (integer) - Expiration time in seconds. #### Response Example { "c_nonce": "abc123", "c_nonce_expires_in": 300 } ``` -------------------------------- ### POST /oauth/credential Source: https://hexdocs.pm/attesto_phoenix Access-token protected endpoint that issues a credential (SD-JWT VC, ISO mdoc, or jwt_vc_json) bound to the holder key. Supports batch proofs and uses the host's :build_credential callback. ```APIDOC ## POST /oauth/credential ### Description Access-token protected endpoint that issues a credential (SD-JWT VC, ISO mdoc, or jwt_vc_json) bound to the holder key. Supports batch proofs and uses the host's :build_credential callback. ### Method POST ### Endpoint /oauth/credential ### Parameters #### Request Body - **format** (string) - Required - The credential format: `sd_jwt_vc`, `mso_mdoc`, or `jwt_vc_json`. - **credential_identifier** (string) - Optional - Identifier of the credential configuration. - **proof** (object) - Required - The proof of possession, containing `proof_type` and `jwt`. - **proofs** (array) - Optional - Batch of proofs, each with `proof_type` and `jwt`. ### Request Example { "format": "sd_jwt_vc", "credential_identifier": "example_credential", "proof": { "proof_type": "jwt", "jwt": "eyJhbGciOiJFUzI1NiJ9..." } } ### Response #### Success Response (200) - **credential** (string) - The issued credential (JWT or CWT). - **c_nonce** (string) - New c_nonce for subsequent requests. - **c_nonce_expires_in** (integer) - Expiration time in seconds. #### Response Example { "credential": "eyJhbGciOiJFUzI1NiJ9...", "c_nonce": "new_nonce", "c_nonce_expires_in": 300 } ``` -------------------------------- ### AttestoPhoenix.Plug.RequireScopes Source: https://hexdocs.pm/attesto_phoenix Enforces route-level scope authorization using Attesto.Scope grant-form algebra. It accepts either a single scope string or a list of required scopes. ```APIDOC ## AttestoPhoenix.Plug.RequireScopes ### Description Enforces route-level scope authorization using `Attesto.Scope` grant-form algebra. It accepts either a single scope string or a list of required scopes. ### Usage ```elixir pipeline :reports_read do plug AttestoPhoenix.Plug.RequireScopes, "read:reports" end scope "/api", MyAppWeb do pipe_through [:api, :api_protected] scope "/reports" do pipe_through :reports_read get "/", ReportController, :index end end ``` ### Options - `:scopes` - a single scope string or a list of required scopes (passed as the second argument to `plug`) ### Behavior When `:resource_metadata` is set on the config, a 401 challenge carries that static RFC 9728 `resource_metadata` pointer, preserving the single-resource default. A host serving several protected resources can instead select the correct pointer per request, or return `nil` when that surface has no applicable metadata declaration. ``` -------------------------------- ### Configure userinfo_endpoint as explicit HTTPS URL Source: https://hexdocs.pm/attesto_phoenix Configures the userinfo_endpoint to an explicit HTTPS URL, which is authoritative and always advertised. This is the supported form when a host replaces the bundled UserInfo controller. ```elixir config :my_app, AttestoPhoenix.Config, userinfo_endpoint: "https://issuer.example/oauth/userinfo" ``` -------------------------------- ### Extract credentials from cookie Source: https://hexdocs.pm/attesto_phoenix Passes a custom credential extractor to `AttestoPhoenix.Plug.Authenticate` for first-party web flows, keeping cookie semantics in the app. ```elixir plug AttestoPhoenix.Plug.Authenticate, credential_from_conn: &MyAppWeb.Auth.access_token_from_cookie/1 ``` -------------------------------- ### Configure authorization_grant_id_claim in AttestoPhoenix.Config Source: https://hexdocs.pm/attesto_phoenix Sets the :authorization_grant_id_claim to a private claim name under a namespace you control. The feature is disabled by default; when enabled, Attesto Phoenix generates a 128-bit authorization family ID and signs it into access tokens. Use a private claim name, not OIDC sid. ```elixir config :my_app, AttestoPhoenix.Config, authorization_grant_id_claim: "https://api.example.com/claims/oauth_grant_id" ``` -------------------------------- ### Configure authorization_code_completion hook Source: https://hexdocs.pm/attesto_phoenix Sets the authorization_code_completion option in the host application's config to point to a callback module and function. The callback is invoked during authorization-code redemption after scope and resource-indicator resolution. ```elixir config :my_app, AttestoPhoenix.Config, authorization_code_completion: &MyApp.Policy.complete/2 ``` -------------------------------- ### POST /oauth/deferred_credential Source: https://hexdocs.pm/attesto_phoenix Token-protected endpoint for deferred issuance (OID4VCI §9). Driven by the :build_deferred_credential callback, which returns issuance_pending until the credential is ready. ```APIDOC ## POST /oauth/deferred_credential ### Description Token-protected endpoint for deferred issuance (OID4VCI §9). Driven by the :build_deferred_credential callback, which returns issuance_pending until the credential is ready. ### Method POST ### Endpoint /oauth/deferred_credential ### Parameters #### Request Body - **transaction_id** (string) - Required - The transaction ID from the initial credential request. ### Request Example { "transaction_id": "txn_123" } ### Response #### Success Response (200) - **credential** (string) - The issued credential when ready. - **c_nonce** (string) - New c_nonce for subsequent requests. - **c_nonce_expires_in** (integer) - Expiration time in seconds. #### Response Example { "credential": "eyJhbGciOiJFUzI1NiJ9...", "c_nonce": "new_nonce", "c_nonce_expires_in": 300 } ``` -------------------------------- ### Load refresh successor secret in config/runtime.exs Source: https://hexdocs.pm/attesto_phoenix Loads the refresh_successor_secret from the ATTESTO_REFRESH_SUCCESSOR_SECRET environment variable at runtime. This secret is used by the bundled Ecto refresh store to encrypt the retry record for a non-zero refresh-rotation grace period. ```elixir # config/runtime.exs config :attesto_phoenix, refresh_successor_secret: System.fetch_env!("ATTESTO_REFRESH_SUCCESSOR_SECRET") ``` -------------------------------- ### Configure client_assertion_audiences to accept only issuer Source: https://hexdocs.pm/attesto_phoenix Sets the accepted private_key_jwt audiences to only the issuer, aligning with the upcoming RFC 10070 default. Use after updating clients to trusted issuer audiences. ```elixir client_assertion_audiences: fn config -> [config.issuer] end ```