A checklist for replacing an existing OAuth/OIDC provider (a hand-rolled
provider or an ex_oauth2_provider-style library) with the attesto /
attesto_phoenix stack.
1. Mount the routes
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:
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)
end2. Configure the host callbacks
Build an AttestoPhoenix.Config. The recommended production shape is the named
behaviours - AttestoPhoenix.ClientStore, AttestoPhoenix.PrincipalStore,
AttestoPhoenix.ScopePolicy, AttestoPhoenix.ConsentPolicy,
AttestoPhoenix.RegistrationStore, AttestoPhoenix.EventSink - wired into the
matching Config keys. See guides/examples.md for minimal configs.
Declare protocol capability explicitly. OIDC hosts should set
openid_provider: true (also the compatibility default); OAuth-only hosts set
it to false and may mount routes with
openid_configuration: false, userinfo: false. Signing keys are required by
both protocols and do not imply OIDC support.
Point the library's global resolver and Ecto-backed stores at the same host:
config :attesto_phoenix, otp_app: :my_app, repo: MyApp.Repo3. Map your existing client store
Your old provider already has a client table. Point :load_client,
:verify_client_secret, :client_auth_method, :client_id, :client_redirect_uris, and
:client_public? at it. You do not need to migrate the rows into a new schema;
you need callbacks that read your existing rows.
:client_auth_method must return the client's trusted registered
token_endpoint_auth_method, including the distinction between Basic and POST.
Confidential authentication fails closed without this callback from 3.4.1;
see the security upgrade guide.
4. Remove the runtime provider, keep historical migrations
This is the step that trips people up. When you delete the old provider dependency:
Remove the runtime code - the old provider's plugs, routes, and any
use OldProvider.Xlines.Keep the historical migrations working. Your repo's migration history still references the old provider's tables and, sometimes, helper modules the old provider exposed at migration time. If you delete the dependency outright,
mix ecto.migrateon a fresh database fails when it reaches those old migrations.Options that keep history runnable:
- Leave the old migrations as-is and keep a thin compatibility shim for any module they reference, OR
- Squash the historical migrations into a single baseline that no longer references the removed dependency (only safe once every environment is past those migrations), OR
- Replace the dependency-specific calls inside the old migration files with the raw SQL they generated, so the migration no longer needs the dependency at all.
Pick one before removing the dep from
mix.exs. Do not remove the dep and discover the break on the next clean deploy.
5. Scope and token claims
Move scope policy into :authorize_scope (or AttestoPhoenix.ScopePolicy) and
the principal/claim shaping into :build_principal /
:build_userinfo_claims. Attesto owns the JWT/JWKS/DPoP mechanics; your host
owns who the subject is and which scopes a client may hold.
With openid_provider: true, config adds openid once to the
authorization-server catalog. Discovery, dynamic-registration validation,
registration defaults, and the default authorization policy then read the same
catalog. This does not update existing client rows or bypass host callbacks;
migrate existing clients that should be allowed to request openid through
your normal client-policy process. Protected-resource and MCP tool catalogs
stay separate and never receive this identity scope automatically. Configure
the bundled RFC 9728 document with protected_resource_scopes_supported. Its
root and optional path-inserted URI describe the same resource and share that
catalog. Multi-resource hosts must publish a separate metadata document and
scope catalog per resource, for example with attesto_mcp's per-resource
router.
6. Verify discovery
Fetch /.well-known/oauth-authorization-server and
/.well-known/openid-configuration and confirm every advertised endpoint URL
points at a route you actually mounted.
Note for a /mcp/oauth (or any non-/oauth) consumer
If you mount the OAuth endpoints somewhere other than /oauth - for example
under /mcp/oauth to avoid colliding with a legacy provider at /oauth - set
the mount once:
oauth_path_prefix: "/mcp/oauth"With that set, the discovery documents and the RFC 7591
registration_client_uri advertise the /mcp/oauth/* paths automatically, and
AttestoPhoenix.Config.to_attesto_config/2 passes the resolved token path into
the core config for you. A consumer that previously hand-passed
token_endpoint_path: "/mcp/oauth/token" into to_attesto_config/2 can drop
that argument once :oauth_path_prefix is set, since the resolver now derives
it. (Explicit per-endpoint overrides such as :token_path still win if you
need a path that does not follow the prefix.)
The bundled attesto_routes/1 macro mounts fixed /oauth/* tails. For this
prefix, mount it with prefix: "/mcp" (or put it in a surrounding /mcp
scope), so the physical routes and advertised URLs agree. An explicit override
such as token_path: "/mcp/oauth/token2" changes metadata only; add a matching
host route when using one. The installer accepts only /oauth or a prefix that
ends in /oauth, using literal slash-separated segments with letters, digits,
_, or -; it rejects other values before changing files.