Packages
attesto_phoenix
2.3.0
2.3.0
2.2.0
2.1.0
2.0.2
2.0.1
2.0.0
1.4.0
1.3.0
1.2.0
1.1.0
1.0.0
0.20.0
0.19.1
0.19.0
0.18.0
0.17.0
0.16.0
0.15.0
0.14.2
0.14.1
0.14.0
0.13.5
0.13.4
0.13.3
0.13.2
0.13.1
0.13.0
0.12.0
0.11.0
0.10.0
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.0
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.23
0.6.22
0.6.21
0.6.20
0.6.19
0.6.18
0.6.17
0.6.16
0.6.15
0.6.14
0.6.13
0.6.12
0.6.11
0.6.10
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
Phoenix/Ecto OAuth 2.0 / OIDC authorization server layer over attesto: authorization, token, PAR, revocation, discovery, JWKS, UserInfo, protected-resource plugs, and Ecto-backed token stores.
Current section
Files
Jump to
Current section
Files
guides/examples.md
# Example configurations
Two minimal `AttestoPhoenix.Config` setups. Both assume the host has wired the
router macro and a pipeline that installs the config on the connection.
## 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.
```elixir
AttestoPhoenix.Config.new(
issuer: "https://auth.example",
keystore: MyApp.Keystore,
repo: MyApp.Repo,
# 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`.
```elixir
AttestoPhoenix.Config.new(
issuer: "https://auth.example",
keystore: MyApp.Keystore,
repo: MyApp.Repo,
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.
```elixir
AttestoPhoenix.Config.new(
issuer: "https://auth.example",
keystore: MyApp.Keystore,
repo: MyApp.Repo,
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. `localhost` is NOT
# acceptable (§8.3); register the literal IP.
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: [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:
```elixir
oauth_path_prefix: "/mcp/oauth"
```
See `guides/consumer_migration.md` for the details.