Packages
attesto_phoenix
2.0.2
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
lib/attesto_phoenix/client_store.ex
defmodule AttestoPhoenix.ClientStore do
@moduledoc """
The host-owned OAuth client registry contract (RFC 6749 §2 / §3.1.2).
The library never owns the client registry: it resolves a client from its
identifier, verifies its secret in constant time, and reads the per-client
attributes the authorization, token, PAR, and revocation endpoints need. A
host implements this behaviour and wires each callback into
`AttestoPhoenix.Config` as an anonymous function, a `{module, function}`
pair, or a `{module, function, extra_args}` triple. This module is the
contract those Config keys install; it is the recommended production shape
but the wiring is unchanged from passing the callbacks individually.
Each `@callback` corresponds to the identically named `AttestoPhoenix.Config`
key:
* `load_client/1` (`:load_client`, required)
* `verify_client_secret/2` (`:verify_client_secret`, required)
* `client_id/1` (`:client_id`)
* `client_jwks/1` (`:client_jwks`)
* `client_redirect_uris/1` (`:client_redirect_uris`)
* `client_public?/1` (`:client_public?`)
* `client_requires_mtls?/1` (`:client_requires_mtls?`)
* `client_requires_dpop?/1` (`:client_requires_dpop?`)
* `client_grant_types/1` (`:client_grant_types`)
The `client` term is opaque to the library: whatever
`load_client/1` returns is threaded back into the other callbacks unchanged.
"""
@typedoc "The host's opaque client representation (e.g. an Ecto struct)."
@type client :: term()
@doc """
Resolve an OAuth client by its identifier (RFC 6749 §2.2).
Returns `{:ok, client}` for a usable client, `{:error, :not_found}` when no
such client exists, or `{:error, :revoked}` when the client is known but has
been revoked. The host owns the registry and the revocation policy.
"""
@callback load_client(client_id :: String.t()) ::
{:ok, client()} | {:error, :not_found} | {:error, :revoked}
@doc """
Constant-time verification of a presented client secret (RFC 6749 §2.3.1).
Returns `true` iff `presented_secret` matches the client's stored secret.
The host owns secret hashing; use `Attesto.SecureCompare` to avoid timing
leaks.
"""
@callback verify_client_secret(client(), presented_secret :: String.t()) :: boolean()
@doc """
The client's OAuth identifier (RFC 6749 §2.2), extracted from the host's
client representation.
"""
@callback client_id(client()) :: String.t()
@doc """
The client's trusted public JWK Set for `private_key_jwt` client
authentication (RFC 7523 / OpenID Connect Core §9). Returns `nil` for a
client that does not authenticate with a signed assertion.
"""
@callback client_jwks(client()) :: map() | nil
@doc """
The client's registered redirect URIs (RFC 6749 §3.1.2.2). The authorization
endpoint exact-matches the request `redirect_uri` against this set
(RFC 6749 §3.1.2.3); a client exposing none rejects every authorization
request (fail closed).
"""
@callback client_redirect_uris(client()) :: [String.t()]
@doc """
Whether the client may authenticate without a secret and rely on PKCE
(RFC 6749 §2.1 / RFC 7636).
"""
@callback client_public?(client()) :: boolean()
@doc """
Whether the client requires mTLS-bound token issuance (RFC 8705).
"""
@callback client_requires_mtls?(client()) :: boolean()
@doc """
Whether the client requires DPoP-bound token issuance (RFC 9449).
"""
@callback client_requires_dpop?(client()) :: boolean()
@doc """
The grant types registered for this client (RFC 7591 §2).
When the host exposes this callback, the token endpoint rejects a requested
`grant_type` not in the returned list before dispatching to the grant
implementation. Return `nil` only when the host has no per-client grant
registry and wants the package's legacy configured-supported-grants behavior.
"""
@callback client_grant_types(client()) :: [String.t()] | nil
@doc """
The client's registered `post_logout_redirect_uris` (OpenID Connect
RP-Initiated Logout 1.0 §2). The end-session endpoint exact-matches the
request `post_logout_redirect_uri` against this set; a client exposing none
has no validated return URI (fail closed — the OP renders its own page).
"""
@callback client_post_logout_redirect_uris(client()) :: [String.t()]
@doc """
The client's registered `backchannel_logout_uri` (OpenID Connect Back-Channel
Logout 1.0 §2.2), or `nil` when the client is not back-channel-logout capable.
When present, the OP records a logout session at ID-Token mint and POSTs a
`logout_token` here when the session ends.
"""
@callback client_backchannel_logout_uri(client()) :: String.t() | nil
@doc """
Whether this client's `logout_token` MUST carry a `sid` claim
(`backchannel_logout_session_required`, Back-Channel Logout 1.0 §2.2).
Defaults to `false` when the callback is not exposed.
"""
@callback client_backchannel_logout_session_required(client()) :: boolean()
@doc """
The client's registered `frontchannel_logout_uri` (OpenID Connect
Front-Channel Logout 1.0 §2), or `nil` when the client is not
front-channel-logout capable. When present, the OP records a logout session
at ID-Token mint and renders the URI in an iframe on the end-session logout
page when the session ends.
"""
@callback client_frontchannel_logout_uri(client()) :: String.t() | nil
@doc """
Whether this client's rendered `frontchannel_logout_uri` must carry `iss` and
`sid` query parameters (`frontchannel_logout_session_required`, Front-Channel
Logout 1.0 §2). Defaults to `false` when the callback is not exposed.
"""
@callback client_frontchannel_logout_session_required(client()) :: boolean()
@doc """
The client's registered OpenID Connect CIBA metadata (CIBA Core §4), as a map
with `:token_delivery_mode` (`:poll` | `:ping` | `:push`),
`:client_notification_endpoint` (the ping-mode
`backchannel_client_notification_endpoint`), `:request_signing_alg` (the
registered `backchannel_authentication_request_signing_alg`), and
`:user_code_parameter` (the `backchannel_user_code_parameter` boolean). A
client not registered for CIBA returns `%{}` (or `nil`), which the backchannel
authentication endpoint treats as `unauthorized_client`.
"""
@callback client_ciba_registration(client()) :: map() | nil
@optional_callbacks client_id: 1,
client_jwks: 1,
client_redirect_uris: 1,
client_public?: 1,
client_requires_mtls?: 1,
client_requires_dpop?: 1,
client_grant_types: 1,
client_post_logout_redirect_uris: 1,
client_backchannel_logout_uri: 1,
client_backchannel_logout_session_required: 1,
client_frontchannel_logout_uri: 1,
client_frontchannel_logout_session_required: 1,
client_ciba_registration: 1
end