Migrating to Attesto from Boruta or a custom OAuth provider

Copy Markdown View Source

A checklist for replacing an existing OAuth/OIDC provider (Boruta, 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

scope "/" do
  attesto_routes(registration: true)
end

2. 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.

3. Map your existing client store

Your old provider already has a client table. Point :load_client, :verify_client_secret, :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.

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.X lines.

  • 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.migrate on 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.

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