### Install and generate migration Source: https://github.com/xukullc/attesto_phoenix/blob/main/README.md Run the installer and migration generator for a new Phoenix app. The installer is idempotent and writes stub callback modules. The migration generator requires the --repo flag. After running these commands, run ecto.migrate to apply the migration. ```bash mix deps.get mix attesto_phoenix.install mix attesto_phoenix.gen.migration --repo MyApp.Repo mix ecto.migrate ``` -------------------------------- ### Configure installed native app (RFC 8252) with AttestoPhoenix.Config.new Source: https://github.com/xukullc/attesto_phoenix/blob/main/guides/examples.md Configure an installed native app (RFC 8252) as a public PKCE client. The client_native? callback marks the client, and the native_apps options optionally disable loopback localhost matching and reject embedded user agents; these are server-wide flags, not per-client properties. ```elixir AttestoPhoenix.Config.new( issuer: "https://auth.example", audience: "https://api.example", keystore: MyApp.Keystore, repo: MyApp.Repo, principal_kinds: &MyApp.AuthZ.principal_kinds/0, load_client: &MyApp.AuthZ.load_client/1, verify_client_secret: fn _client, _secret -> false end, client_id: &MyApp.AuthZ.client_id/1, # Registered as `http://127.0.0.1:0/cb` (and/or `http://[::1]:0/cb`) - the # port is ignored under §7.3, so any placeholder will do. Prefer the literal # IP; clients fixed to a localhost callback require the explicit option below. client_redirect_uris: &MyApp.AuthZ.client_redirect_uris/1, client_public?: fn _client -> true end, client_native?: &MyApp.AuthZ.client_native?/1, # Optional §8.12 in-app-webview refusal; a User-Agent heuristic, so it can # misjudge honest browsers. Unlike the rest of the profile this is a # server-wide posture, so it is a flag rather than a per-client property. native_apps: [ loopback_include_localhost: false, reject_embedded_user_agents: false ], load_principal: &MyApp.AuthZ.load_principal/1, build_principal: &MyApp.AuthZ.build_principal/3, authenticate_resource_owner: &MyApp.AuthZ.authenticate_resource_owner/3, openid_provider: true, scopes_supported: ["openid", "profile"], code_store: AttestoPhoenix.Store.EctoCodeStore, refresh_store: AttestoPhoenix.Store.EctoRefreshStore, replay_check: &AttestoPhoenix.Store.EctoReplayCheck.check_and_record/2, nonce_store: AttestoPhoenix.Store.EctoNonceStore, sweep_interval_ms: 60_000 ) ``` -------------------------------- ### Host wiring for AttestoPhoenix.Config Source: https://github.com/xukullc/attesto_phoenix/blob/main/guides/examples.md Host wiring for the example configurations. Defines the OTP app and repo, a pipeline that plugs AttestoPhoenix.Plug.PutConfig, and a scope that mounts attesto_routes with that pipeline. ```elixir config :attesto_phoenix, otp_app: :my_app, repo: MyApp.Repo pipeline :attesto_phoenix_config do plug AttestoPhoenix.Plug.PutConfig, otp_app: :my_app end scope "/" do attesto_routes(pipeline: :attesto_phoenix_config) end ``` -------------------------------- ### Development setup and test commands Source: https://github.com/xukullc/attesto_phoenix/blob/main/README.md Run these commands in order to set up and validate the project. `mix deps.get` fetches dependencies, `mix precommit` runs pre-commit checks, and `mix test --include ecto` runs the test suite including Ecto tests, which requires a Postgres database. ```bash mix deps.get mix precommit mix test --include ecto # requires Postgres ``` -------------------------------- ### Install and generate migration with schema prefix Source: https://github.com/xukullc/attesto_phoenix/blob/main/README.md Pass the same validated schema prefix to both the installer and the migration generator for a fresh installation in a non-default PostgreSQL schema. The schema prefix must be validated and consistent between the two commands. ```bash mix attesto_phoenix.install --schema-prefix oauth mix attesto_phoenix.gen.migration --repo MyApp.Repo --schema-prefix oauth ``` -------------------------------- ### Generate dev HTTPS certificate with mix attesto_phoenix.gen.dev_https Source: https://github.com/xukullc/attesto_phoenix/blob/main/guides/local_https.md Generates a locally-trusted HTTPS certificate for development. The task checks that mkcert is installed, creates priv/cert/, runs mkcert -install (idempotent), writes priv/cert/localhost.pem and priv/cert/localhost-key.pem for localhost 127.0.0.1 ::1, and ensures priv/cert/ is git-ignored. ```sh mix attesto_phoenix.gen.dev_https ``` -------------------------------- ### Install mkcert with brew install mkcert nss Source: https://github.com/xukullc/attesto_phoenix/blob/main/guides/local_https.md Installs mkcert on macOS via Homebrew, including nss for Firefox trust. For Linux or Windows, follow the installation link provided in the comment. ```sh brew install mkcert nss # macOS; nss adds Firefox trust # Linux / Windows: https://github.com/FiloSottile/mkcert#installation ``` -------------------------------- ### Generate migration with --schema-prefix Source: https://github.com/xukullc/attesto_phoenix/blob/main/guides/upgrade_3_0_schema_prefix.md Generate tables with the schema option for future fresh databases. The installer accepts the same --schema-prefix option; do not pass --table-prefix and do not use a fresh create-table migration as a substitute for the cutover. ```bash mix attesto_phoenix.gen.migration --repo MyApp.Repo --schema-prefix oauth ``` -------------------------------- ### Configure custom OAuth mount path Source: https://github.com/xukullc/attesto_phoenix/blob/main/guides/examples.md To advertise a different mount (e.g., /mcp/oauth) instead of the historic /oauth/* endpoints, add the oauth_path_prefix key to your config. Then run the installer with the same full client-visible prefix; it generates attestо_routes(prefix: "/mcp") and rejects prefixes like /auth that would not match the fixed /oauth/* tails. ```elixir oauth_path_prefix: "/mcp/oauth" ``` ```bash mix attesto_phoenix.install --oauth-path-prefix /mcp/oauth ``` -------------------------------- ### Get exact row counts for candidate relations Source: https://github.com/xukullc/attesto_phoenix/blob/main/guides/upgrade_3_0_schema_prefix.md Example queries to obtain exact row counts for each candidate relation of a logical table. Run a reviewed query for each relation; do not use reltuples as the cutover count. Save exact results, including zeroes, in rehearsal and production records. ```sql SELECT count(*) FROM public.attesto_refresh_tokens; SELECT count(*) FROM public.oauth_attesto_refresh_tokens; SELECT count(*) FROM oauth.attesto_refresh_tokens; ``` -------------------------------- ### Add attesto_phoenix with Igniter installer Source: https://github.com/xukullc/attesto_phoenix/blob/main/README.md Adds attesto_phoenix and the optional Igniter installer as a dev-only dependency. The igniter package is not a runtime dependency and is only needed when running the installer. ```elixir def deps do [ {:attesto_phoenix, "~> 3.2"}, {:igniter, "~> 0.6", only: [:dev], runtime: false} ] end ``` -------------------------------- ### Mount Attesto routes with router macro and pipeline Source: https://github.com/xukullc/attesto_phoenix/blob/main/guides/consumer_migration.md Add the router macro and mount the endpoints. The well-known documents are always at the host root (RFC 8615); the OAuth endpoints live under your chosen prefix. This example uses a pipeline that plugs AttestoPhoenix.Plug.PutConfig with otp_app: :my_app and mounts routes with registration enabled. ```elixir 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, registration: true) end ``` -------------------------------- ### Install with custom OAuth path prefix Source: https://github.com/xukullc/attesto_phoenix/blob/main/README.md Use --oauth-path-prefix to place OAuth endpoints under a custom path instead of /oauth. The prefix must be /oauth or end in /oauth with slash-separated literal segments containing only letters, digits, underscore, or hyphen. An arbitrary suffix is rejected before any files change. ```bash mix attesto_phoenix.install --oauth-path-prefix /mcp/oauth ``` -------------------------------- ### Supervise AttestoPhoenix.Store.Sweeper Source: https://github.com/xukullc/attesto_phoenix/blob/main/guides/examples.md Supervises the sweeper after the repository when using the bundled Ecto stores with positive retry grace. The sweep_interval_ms setting controls the interval but does not start this process. The installer adds the child automatically; manually wired hosts must add it themselves. ```elixir # lib/my_app/application.ex children = [ MyApp.Repo, {AttestoPhoenix.Store.Sweeper, config: AttestoPhoenix.Config.from_otp_app(:my_app)} ] ``` -------------------------------- ### Generate 3.0 and 3.1 upgrade migrations Source: https://github.com/xukullc/attesto_phoenix/blob/main/README.md Generates the 3.0 and 3.1 upgrade migrations for existing 2.x databases, in that order. Follow the stopped-cutover guide. The generator picks up schema_prefix from config automatically; pass --schema-prefix to override. ```bash mix attesto_phoenix.gen.migration --upgrade 3.0 --repo MyApp.Repo ``` ```bash mix attesto_phoenix.gen.migration --upgrade 3.1 --repo MyApp.Repo ``` -------------------------------- ### Verify https endpoint with curl Source: https://github.com/xukullc/attesto_phoenix/blob/main/guides/local_https.md Start the Phoenix server and verify the OAuth authorization server metadata endpoint over https. No -k flag is needed because the certificate is trusted. ```sh mix phx.server curl https://localhost:4443/.well-known/oauth-authorization-server ``` -------------------------------- ### Add attesto_phoenix dependency Source: https://github.com/xukullc/attesto_phoenix/blob/main/README.md Adds the attesto_phoenix package to a Mix project's dependencies. Use this when you only need the Phoenix integration and not the optional Igniter installer. ```elixir def deps do [ {:attesto_phoenix, "~> 3.2"} ] end ``` -------------------------------- ### Set sweeper interval Source: https://github.com/xukullc/attesto_phoenix/blob/main/guides/replay_nonce_production.md Sets the sweeper interval in AttestoPhoenix.Config. The Igniter installer adds the sweeper process after the host repo; a manual configuration must supervise it explicitly. The packaged sweeper is the stable public integration point; its internal cleanup callback is not a host API. ```elixir sweep_interval_ms: 60_000 ``` -------------------------------- ### Configure schema_prefix in Elixir config Source: https://github.com/xukullc/attesto_phoenix/blob/main/guides/upgrade_3_0_schema_prefix.md Configure the 3.0 schema_prefix key after removing all old :table_prefix settings. The value "oauth" is an example; nil uses the Ecto/repo connection default/search_path (often public). ```elixir config :my_app, AttestoPhoenix.Config, schema_prefix: "oauth" # nil uses the Ecto/repo connection default/search_path (often public) ``` -------------------------------- ### Configure private context and completion callbacks Source: https://github.com/xukullc/attesto_phoenix/blob/main/README.md Configure both options together in config.exs. The private context callback returns trusted state; the completion callback receives the authorized client_id, subject, and family_id. The return value must be a portable JSON object of at most 4 KiB. Omitting the completion callback while setting the context is refused at boot. ```elixir config :my_app, AttestoPhoenix.Config, authorization_code_private_context: &MyApp.Policy.private_context/1, authorization_code_completion: &MyApp.Policy.complete/2 ``` -------------------------------- ### Configure native_apps options Source: https://github.com/xukullc/attesto_phoenix/blob/main/README.md Enables server-wide opt-in options for native apps: loopback_include_localhost and reject_embedded_user_agents. Both default to false. loopback_include_localhost allows varying port for localhost redirects; reject_embedded_user_agents refuses authorization requests from embedded user agents. ```elixir config :my_app, AttestoPhoenix.Config, native_apps: [ loopback_include_localhost: true, reject_embedded_user_agents: true ] ``` -------------------------------- ### Configure public PKCE client with AttestoPhoenix.Config.new Source: https://github.com/xukullc/attesto_phoenix/blob/main/guides/examples.md Configure a public PKCE client (native/SPA) with no client secret. The verify_client_secret callback always returns false, so any presented secret fails closed; require_pkce defaults to true, enforcing PKCE for the code grant. ```elixir AttestoPhoenix.Config.new( issuer: "https://auth.example", audience: "https://api.example", keystore: MyApp.Keystore, repo: MyApp.Repo, principal_kinds: &MyApp.AuthZ.principal_kinds/0, load_client: &MyApp.AuthZ.load_client/1, # A public client presents no secret; verification always fails closed if a # secret is somehow presented. verify_client_secret: fn _client, _secret -> false end, client_id: &MyApp.AuthZ.client_id/1, client_redirect_uris: &MyApp.AuthZ.client_redirect_uris/1, client_public?: fn _client -> true end, load_principal: &MyApp.AuthZ.load_principal/1, build_principal: &MyApp.AuthZ.build_principal/3, authenticate_resource_owner: &MyApp.AuthZ.authenticate_resource_owner/3, # require_pkce defaults to true; PKCE is enforced for the code grant. openid_provider: true, scopes_supported: ["openid", "profile"], code_store: AttestoPhoenix.Store.EctoCodeStore, refresh_store: AttestoPhoenix.Store.EctoRefreshStore, replay_check: &AttestoPhoenix.Store.EctoReplayCheck.check_and_record/2, nonce_store: AttestoPhoenix.Store.EctoNonceStore, sweep_interval_ms: 60_000 ) ``` -------------------------------- ### Configure global resolver and Ecto-backed stores Source: https://github.com/xukullc/attesto_phoenix/blob/main/guides/consumer_migration.md Point the library's global resolver and Ecto-backed stores at the same host. This configuration sets the otp_app and repo for the attestо_phoenix application. ```elixir config :attesto_phoenix, otp_app: :my_app, repo: MyApp.Repo ``` -------------------------------- ### AttestoPhoenix.Config configuration in config/config.exs Source: https://github.com/xukullc/attesto_phoenix/blob/main/README.md Central configuration for AttestoPhoenix, pointing controllers and Ecto-backed stores at the host application. Required settings include issuer, audience, keystore, repo, and principal_kinds; optional policy and deployment settings are also shown. The comment notes that the terminator must overwrite, never append/forward, the client-cert header, and that the callback is invoked only from trusted_proxies. ```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_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]) ] ``` -------------------------------- ### Configure https listener with AttestoPhoenix.DevTLS.https_opts/1 Source: https://github.com/xukullc/attesto_phoenix/blob/main/guides/local_https.md In config/dev.exs, hand the endpoint's https listener to AttestoPhoenix.DevTLS.https_opts/1. The function returns the full https keyword with port, cipher_suite: :strong, resolved certfile/keyfile, and a raised max_header_length. It resolves priv/cert/localhost.pem and priv/cert/localhost-key.pem, and raises if the certificate is missing. ```elixir config :my_app, MyAppWeb.Endpoint, https: AttestoPhoenix.DevTLS.https_opts(port: 4443) ``` -------------------------------- ### Enable CIMD for native clients Source: https://github.com/xukullc/attesto_phoenix/blob/main/README.md Configure AttestoPhoenix.Config to enable client_id_metadata with allowed_hosts and set native_apps loopback_include_localhost. Requires optional Req dependency for the default CIMD fetcher. Keep allowed_hosts narrow; localhost option only affects redirect matching. ```elixir config :my_app, AttestoPhoenix.Config, client_id_metadata: [ enabled: true, allowed_hosts: ["client.example"] ], native_apps: [loopback_include_localhost: true] ``` -------------------------------- ### Generate fresh database migration Source: https://github.com/xukullc/attesto_phoenix/blob/main/README.md Generates the complete schema migration for a fresh database. This command creates the operational tables backing the attesto store behaviours. It does not own a clients table; that remains behind your :load_client callback. ```bash mix attesto_phoenix.gen.migration --repo MyApp.Repo ``` -------------------------------- ### Configure Ecto replay check Source: https://github.com/xukullc/attesto_phoenix/blob/main/guides/replay_nonce_production.md Sets :replay_check to the bundled Ecto-backed implementation. The callback records a fixed-length, client-scoped digest for each signed CIBA authentication-request jti. When upgrading from 2.14.0, drain old nodes and wait until every signed request JWT has expired (conservatively 61 minutes) before starting 2.14.1. ```elixir replay_check: &AttestoPhoenix.Store.EctoReplayCheck.check_and_record/2 ``` -------------------------------- ### Configure AttestoPhoenix.Plug.Authenticate with credential_from_conn Source: https://github.com/xukullc/attesto_phoenix/blob/main/README.md Configures the AttestoPhoenix.Plug.Authenticate plug with a custom credential extraction function. The function must return {:ok, :bearer, token}, {:ok, :dpop, token}, or :missing. Attesto still verifies the token through the same JWT/DPoP/mTLS path; the cookie format and CSRF policy remain host concerns. ```elixir plug AttestoPhoenix.Plug.Authenticate, credential_from_conn: &MyAppWeb.Auth.access_token_from_cookie/1 ``` -------------------------------- ### Generate 3.0 and 3.1 upgrade migrations (default schema) Source: https://github.com/xukullc/attesto_phoenix/blob/main/README.md Run these commands while all token writers are stopped. The 3.0 migration adds the unique (family_id, generation) index, creates the durable attesto_refresh_family_revocations table, and backfills revoked families. The 3.1 migration promotes the historical attesto_authorization_codes(code_hash) unique index to the primary key. Remove every legacy :table_prefix setting before running; the generator refuses that retired key even with an explicit --schema-prefix. ```bash # 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 ``` -------------------------------- ### Sweeper child specification in application children list Source: https://github.com/xukullc/attesto_phoenix/blob/main/README.md Shows the child specification for the AttestoPhoenix sweeper, typically added by `mix attesto_phoenix.install`. Supervise one sweeper per independent `{repo, schema_prefix}` pair; never share across schemas or start duplicates. Use `AttestoPhoenix.Store.Sweeper.verify_running!/1` in a readiness check. ```elixir # lib/my_app/application.ex children = [ MyApp.Repo, {AttestoPhoenix.Store.Sweeper, config: AttestoPhoenix.Config.from_otp_app(:my_app)} ] ``` -------------------------------- ### attesto_routes/1 with all feature flags Source: https://github.com/xukullc/attesto_phoenix/blob/main/README.md Call this macro in a Phoenix router to mount the OID4VC issuer, verifier, status list, and federation endpoints. Each flag is optional and only mounts the corresponding routes when set to true. All paths derive from configurable Config tails, so nothing is hardcoded. ```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 ) ``` -------------------------------- ### Build validated AttestoPhoenix config from OTP app Source: https://github.com/xukullc/attesto_phoenix/blob/main/README.md Builds the validated `%AttestoPhoenix.Config{}` struct from the host OTP application environment. Required keys are validated at build time so misconfiguration fails fast. The resulting config must be placed in `conn.private[:attesto_phoenix_config]` by a plug such as `AttestoPhoenix.Plug.PutConfig`. ```elixir config = AttestoPhoenix.Config.from_otp_app(:my_app) ``` -------------------------------- ### Generate 3.1 upgrade migration with attestophoenix.gen.migration Source: https://github.com/xukullc/attesto_phoenix/blob/main/guides/upgrade_3_0_schema_prefix.md Generate the 3.1 upgrade migration to promote the authorization-code index to a primary key. It accepts only the exact historical generated unique index or the exact already-promoted primary key; malformed or ambiguous layouts fail closed for manual review. Apply to the subscriber first, then the publisher, because logical replication does not copy DDL. ```bash mix attesto_phoenix.gen.migration --upgrade 3.1 --repo MyApp.Repo # Dedicated PostgreSQL schema: mix attesto_phoenix.gen.migration --upgrade 3.1 --repo MyApp.Repo --schema-prefix oauth ``` -------------------------------- ### Run Ecto migrations Source: https://github.com/xukullc/attesto_phoenix/blob/main/README.md Runs the generated migrations. The 3.1 migration promotes the historical unique index to primary key in place and requires a stopped cutover with a short lock_timeout. ```bash mix ecto.migrate ``` -------------------------------- ### MyApp.Policy callback module with complete/2 and private_context/1 Source: https://github.com/xukullc/attesto_phoenix/blob/main/README.md Defines the callback module used by the authorization_code_completion hook. private_context/1 returns a map with a security epoch. complete/2 runs the continuation inside a Repo transaction, revalidates the subject's authorization, rolls back on failure, and unwraps the transaction result to match the continuation's exact return shape. ```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 ``` -------------------------------- ### Generate 3.0 and 3.1 upgrade migrations (dedicated PostgreSQL schema) Source: https://github.com/xukullc/attesto_phoenix/blob/main/README.md Use these commands when the deployment uses a dedicated PostgreSQL schema prefix. They generate the same 3.0 and 3.1 migration files as the default case but target the specified schema. The 3.0 file adds the unique (family_id, generation) index, creates the durable attesto_refresh_family_revocations table, and backfills revoked families. The 3.1 file promotes the historical attesto_authorization_codes(code_hash) unique index to the primary key. ```bash # 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 ``` -------------------------------- ### Configure Ecto nonce store Source: https://github.com/xukullc/attesto_phoenix/blob/main/guides/replay_nonce_production.md Sets :nonce_store to the bundled Ecto-backed implementation. A capable store exports issue/1 and valid?/1, or the config-aware issue/2 and valid?/2 callbacks used by the bundled Ecto implementation. Setting dpop_nonce_required: true without a capable store is rejected when AttestoPhoenix.Config is built. ```elixir nonce_store: AttestoPhoenix.Store.EctoNonceStore ``` -------------------------------- ### Configure :www_authenticate to write challenge header Source: https://github.com/xukullc/attesto_phoenix/blob/main/guides/error_envelope.md Use this hook to write the WWW-Authenticate header on 401 responses. The library computes the exact challenge string (scheme plus error, error_description, scope, and DPoP algs auth-params); this hook only writes it. Write the challenge verbatim to avoid dropping required auth-params. ```elixir www_authenticate: fn conn, challenge -> Plug.Conn.put_resp_header(conn, "www-authenticate", challenge) end ``` -------------------------------- ### Configure client_native? callback Source: https://github.com/xukullc/attesto_phoenix/blob/main/README.md Sets the client_native? callback in the AttestoPhoenix config. The callback takes a client and returns a boolean; default is false. When true, the client is treated as a native app under RFC 8252. ```elixir config :my_app, AttestoPhoenix.Config, client_native?: {MyApp.OAuth, :client_native?} # (client -> boolean), default false ``` -------------------------------- ### Configure issuer and audience for https origin Source: https://github.com/xukullc/attesto_phoenix/blob/main/guides/local_https.md Set the AttestoPhoenix.Config issuer and audience to the mkcert https origin so discovery, DPoP htu, and resource identifiers match what a client discovers. ```elixir config :my_app, AttestoPhoenix.Config, issuer: "https://localhost:4443", audience: "https://localhost:4443/mcp" ``` -------------------------------- ### Generate 3.0 upgrade migration with mix attesto_phoenix.gen.migration Source: https://github.com/xukullc/attesto_phoenix/blob/main/CHANGELOG.md Generate the required Ecto migration for upgrading to attesto_phoenix 3.0. The first command is for hosts using the default schema; the second passes an explicit schema prefix. The generated DDL creates a unique index on attestato_refresh_tokens(family_id, generation) with a 5s lock timeout, creates the durable tombstone table, and backfills revoked families. ```bash mix attesto_phoenix.gen.migration --upgrade 3.0 ``` ```bash mix attesto_phoenix.gen.migration --upgrade 3.0 --schema-prefix oauth ``` -------------------------------- ### Generate 3.0 upgrade migration with attestophoenix.gen.migration Source: https://github.com/xukullc/attesto_phoenix/blob/main/guides/upgrade_3_0_schema_prefix.md Generate the 3.0 upgrade migration with attestophoenix 3.2 or later. Remove every legacy `:table_prefix` setting from the Mix environment first; the generator rejects that retired setting even with an explicit `--schema-prefix`. Without `--schema-prefix`, the migration defers to the Ecto migrator/repo default (normally public). ```bash # 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 # Force public when the repo has a different migration default: mix attesto_phoenix.gen.migration --upgrade 3.0 --repo MyApp.Repo --schema-prefix public # When using a dedicated PostgreSQL schema: mix attesto_phoenix.gen.migration --upgrade 3.0 --repo MyApp.Repo --schema-prefix oauth ``` -------------------------------- ### Mount AttestoPhoenix routes with custom pipeline Source: https://github.com/xukullc/attesto_phoenix/blob/main/README.md Use the attesto_routes macro inside a Phoenix scope to mount OAuth/OIDC endpoints. The pipeline plugs in AttestoPhoenix.Plug.PutConfig with the host's OTP app. The :prefix option sets the path before the fixed /oauth/* tails. ```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 ``` -------------------------------- ### Protect API routes with authentication and scope plugs Source: https://github.com/xukullc/attesto_phoenix/blob/main/README.md Defines two pipelines: :api_protected runs AttestoPhoenix.Plug.Authenticate, and :reports_read runs AttestoPhoenix.Plug.RequireScopes with the scope "read:reports". The /api scope pipes through both, and the /reports scope requires the reports_read pipeline before the index action. ```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 ``` -------------------------------- ### Inventory candidate relations with SQL query Source: https://github.com/xukullc/attesto_phoenix/blob/main/guides/upgrade_3_0_schema_prefix.md Resolves candidate relations for each canonical table name across three locations: public canonical, public literal-prefixed (using 'oauth_' as the prefix), and schema-canonical (using 'oauth' as the schema). Replace 'oauth_' and 'oauth' with reviewed identifiers before running. This query only resolves names; it does not create, rename, move, or delete anything. ```sql WITH expected(name) AS (VALUES ('attesto_authorization_codes'), ('attesto_refresh_tokens'), ('attesto_device_codes'), ('attesto_ciba_requests'), ('attesto_logout_sessions'), ('dpop_nonces'), ('dpop_replays'), ('attesto_pushed_authorization_requests'), ('attesto_client_id_metadata'), ('attesto_consent_grants'), ('attesto_refresh_family_revocations') ) SELECT name AS canonical_name, to_regclass(format('public.%I', name)) AS public_canonical, to_regclass(format('public.%I', 'oauth_' || name)) AS public_literal_prefixed, to_regclass(format('%I.%I', 'oauth', name)) AS schema_canonical FROM expected ORDER BY name; ``` -------------------------------- ### Generate upgrade migration with mix attesto_phoenix.gen.migration Source: https://github.com/xukullc/attesto_phoenix/blob/main/CHANGELOG.md Generates the forward migration that promotes the existing unique index on attestto_authorization_codes.code_hash to the table's primary key. Run it after upgrading the package to 3.2.0 or later; the generated migration validates the historical index, uses a 5-second lock_timeout, and handles replica identity restoration in down/0. ```bash mix attesto_phoenix.gen.migration --upgrade 3.1 --repo MyApp.Repo ``` -------------------------------- ### Configure custom OAuth path prefix with oauth_path_prefix Source: https://github.com/xukullc/attesto_phoenix/blob/main/guides/consumer_migration.md Set oauth_path_prefix to mount OAuth endpoints under a non-/oauth path, e.g., /mcp/oauth. Discovery documents and registration_client_uri automatically advertise the prefixed paths, and to_attesto_config/2 derives the token path, so you can drop an explicit token_endpoint_path argument. ```elixir oauth_path_prefix: "/mcp/oauth" ``` -------------------------------- ### Configure static resource metadata URL Source: https://github.com/xukullc/attesto_phoenix/blob/main/README.md Sets the :resource_metadata config option to a static RFC 9728 resource_metadata pointer. This is used when a single protected resource serves the metadata document. ```elixir resource_metadata: "https://api.example/.well-known/oauth-protected-resource", resource_metadata_resolver: {MyAppWeb.ResourceMetadata, :for_request} ``` -------------------------------- ### Wire Ecto stores for clustering Source: https://github.com/xukullc/attesto_phoenix/blob/main/README.md Use this configuration block to make the OAuth stores clusterable by replacing the default in-memory implementations with Postgres-backed Ecto stores. It is the default when using `mix attesto_phoenix.install`. The Ecto refresh store also requires the stable `ATTESTO_REFRESH_SUCCESSOR_SECRET` runtime setting whenever `refresh_token_rotation_grace_seconds` is greater than zero. ```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 ``` -------------------------------- ### resolve_jwt_bearer_subject/1 callback implementation Source: https://github.com/xukullc/attesto_phoenix/blob/main/guides/identity_assertion_grant.md Implement this callback on your AttestoPhoenix.PrincipalStore module to map the validated claims to a local subject. The callback receives claims after sender binding, resource, scope, and host policy pass, and must return {:ok, subject_string} or {:error, :no_local_account}. Because the jti replay claim is checked after the callback, side effects must be idempotent. ```elixir def resolve_jwt_bearer_subject(claims) do # `claims["sub"]` is unique when scoped with `claims["iss"]`; `claims["email"]` # is often also present. Map to YOUR account model however you choose. case MyApp.Accounts.fetch_by_external_id(claims["iss"], claims["sub"]) do {:ok, user} -> {:ok, "user:#{user.id}"} # the subject the token is minted for :error -> {:error, :no_local_account} # a deny becomes invalid_grant end end ``` -------------------------------- ### Enable and configure the jwt_bearer grant in config.exs Source: https://github.com/xukullc/attesto_phoenix/blob/main/guides/identity_assertion_grant.md Enable the JWT bearer grant by setting enabled: true under :jwt_bearer. Provide a map of trusted issuers, each with either static JWKS or a jwks_uri. Optionally set assertion_max_lifetime_seconds to cap exp - iat (default 300). The resolve_jwt_bearer_subject callback is required for subject resolution. If grant_types_supported is explicitly configured, add the URN urn:ietf:params:oauth:grant-type:jwt-bearer yourself. ```elixir config :my_app, AttestoPhoenix.Config, # ... issuer, keystore, repo, the usual callbacks ... jwt_bearer: [ enabled: true, issuers: %{ # A trusted enterprise IdP with STATIC keys: "https://idp.example.com" => [ jwks: %{"keys" => [%{"kty" => "RSA", "kid" => "...", "n" => "...", "e" => "AQAB"}]}, allowed_algs: ["RS256", "ES256"] # optional; defaults to all supported ], # ...or one whose keys are fetched (and cached) from its JWKS URI: "https://idp.other.com" => [ jwks_uri: "https://idp.other.com/.well-known/jwks.json" ] }, assertion_max_lifetime_seconds: 300 # optional ceiling on exp - iat ], resolve_jwt_bearer_subject: &MyApp.AuthZ.resolve_jwt_bearer_subject/1 ``` -------------------------------- ### Remount custom UserInfo controller with discovery Source: https://github.com/xukullc/attesto_phoenix/blob/main/README.md Disable the bundled userinfo route and mount a custom controller at the canonical path. The userinfo_endpoint config advertises the custom endpoint in discovery metadata. ```elixir scope "/" do pipe_through :attesto_phoenix_config attesto_routes(userinfo: false) get "/oauth/userinfo", MyAppWeb.UserInfoController, :show end ``` ```elixir config :my_app, AttestoPhoenix.Config, userinfo_endpoint: "https://issuer.example/oauth/userinfo" ``` -------------------------------- ### Resolve resource metadata per request path Source: https://github.com/xukullc/attesto_phoenix/blob/main/README.md Implements the for_request/1 callback for MyAppWeb.ResourceMetadata. It returns a different resource_metadata URL based on the request_path, and nil for other paths. The resolver is authoritative and does not fall back to the static URL when it returns nil. ```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 ```