Three minimal AttestoPhoenix.Config setups. They use the host wiring below:
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)
endBecause these examples select the bundled Ecto refresh store and retain the default 60-second retry grace, they also load one stable encryption secret in runtime configuration:
# config/runtime.exs
config :attesto_phoenix,
refresh_successor_secret:
System.fetch_env!("ATTESTO_REFRESH_SUCCESSOR_SECRET")The value must be at least 32 bytes and identical on every node and across deployments. A single-node development setup may instead use an explicit development-only value. Production should fail at startup when the environment variable is absent.
Because these examples use the bundled Ecto stores with positive retry grace, supervise the sweeper after the repository:
# lib/my_app/application.ex
children = [
MyApp.Repo,
{AttestoPhoenix.Store.Sweeper,
config: AttestoPhoenix.Config.from_otp_app(:my_app)}
]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.
Confidential client (server-side app, client secret)
A confidential client authenticates with a secret at the token endpoint (RFC 6749 §2.3.1). This config issues access and refresh tokens for the authorization-code grant and serves discovery.
AttestoPhoenix.Config.new(
issuer: "https://auth.example",
audience: "https://api.example",
keystore: MyApp.Keystore,
repo: MyApp.Repo,
principal_kinds: &MyApp.AuthZ.principal_kinds/0,
# Client registry (AttestoPhoenix.ClientStore).
load_client: &MyApp.AuthZ.load_client/1,
verify_client_secret: &MyApp.AuthZ.verify_client_secret/2,
client_id: &MyApp.AuthZ.client_id/1,
client_redirect_uris: &MyApp.AuthZ.client_redirect_uris/1,
client_public?: fn _client -> false end,
# Subject (AttestoPhoenix.PrincipalStore).
load_principal: &MyApp.AuthZ.load_principal/1,
build_principal: &MyApp.AuthZ.build_principal/3,
# Login + consent (AttestoPhoenix.ConsentPolicy).
authenticate_resource_owner: &MyApp.AuthZ.authenticate_resource_owner/3,
consent: &MyApp.AuthZ.consent/3,
# Scope policy (AttestoPhoenix.ScopePolicy); omit to default to
# "subset of :scopes_supported".
authorize_scope: &MyApp.AuthZ.authorize_scope/2,
scopes_supported: ["openid", "profile", "email"],
# Shared production token stores.
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
)Public PKCE client (native / SPA, no secret)
A public client holds no secret and proves possession of the authorization
code with PKCE (RFC 7636). It authenticates at the token endpoint with
none.
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.
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
)Installed native app (RFC 8252)
A native app is a public PKCE client that additionally runs on the end user's
own device, which RFC 8252 (BCP 212) treats as its own case. Mark it with
:client_native? and the whole profile applies to it: PKCE is required (§8.1),
no token-endpoint credential is accepted (§8.4), and its loopback redirect URI
matches on any port (§7.3, a MUST). No further configuration.
A deployment that must forbid the §7.3 relaxation server-wide — one certifying
against a profile that mandates exact redirect-URI matching — sets
native_apps: [loopback_redirect: false]. Such a deployment usually has no
native clients in the first place, in which case nothing is needed at all.
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,
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
)Mounting somewhere other than /oauth
Both configs above advertise the historic /oauth/* endpoints. To advertise a
different mount (for example /mcp/oauth), add a single key:
oauth_path_prefix: "/mcp/oauth"When using the installer, pass the same full client-visible prefix:
mix attesto_phoenix.install --oauth-path-prefix /mcp/oauth
This generates attesto_routes(prefix: "/mcp"), whose fixed /oauth/* tails
match the advertised /mcp/oauth/* paths. The installer rejects prefixes such
as /auth that would not match those tails. A different suffix or an explicit
per-endpoint override requires a host-mounted route with the exact advertised
path; configure that manually rather than relying on the generated mount.
See guides/consumer_migration.md for the details.