Packages
attesto_phoenix
2.2.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 server requires PKCE for it (§8.1) and refuses any
token-endpoint credential from it (§8.4) — both without further configuration.
Add `native_apps: [loopback_redirect: true]` only if the app cannot use a
private-use URI scheme (`com.example.app:/cb`) and must instead bind an
ephemeral port on the loopback interface (§7.3). That option widens
redirect-URI matching, which the OpenID Connect and FAPI profiles assume is
exact — see the README's certification note before turning it on.
```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,
native_apps: [
loopback_redirect: true,
# Optional §8.12 in-app-webview refusal; a User-Agent heuristic, so it can
# misjudge honest browsers.
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.