AttestoClient owns protocol mechanics: discovery validation, S256 PKCE, state and nonce correlation, code exchange, ID Token verification, refresh single-flight, revocation, and logout request construction. Your application still owns authorization, durable token persistence, and session policy.
Supervision
The included transaction store and each refresh coordinator process are node-local. Start them under your supervisor:
children = [
{AttestoClient.AuthorizationTransaction.Store.ETS,
name: MyApp.OIDCTransactions, max_entries: 10_000},
{AttestoClient.RefreshCoordinator, name: MyApp.OIDCRefreshes}
]If callbacks can land on different nodes, implement
AttestoClient.AuthorizationTransaction.Store over a shared database or cache.
Its put_new/4 and take/2 operations must be atomic, and take/2 must delete
before returning.
Login starts can allocate store capacity before authentication. Rate-limit the
start endpoint per client and network/user signal, and monitor
:capacity_exceeded rather than treating the configured bound as abuse
protection by itself.
Single-flight refresh protection covers callers sharing one coordinator process. In a cluster, route each token-record key to one coordinator or place a distributed lock/serialization layer around refresh; independent coordinators cannot prevent cross-node reuse of the same refresh token.
Start authorization
store = {AttestoClient.AuthorizationTransaction.Store.ETS, MyApp.OIDCTransactions}
# Generate this once per initiating browser session and retain it only in the
# application's secure, HttpOnly session. Do not put it in the redirect URL.
browser_binding = application_browser_session_binding
{:ok, request} =
AttestoClient.AuthorizationCode.start(store,
issuer: "https://accounts.example.com",
client_id: "my-client",
browser_binding: browser_binding,
redirect_uri: "https://app.example.com/oidc/callback",
scopes: ["openid", "profile", "email"],
id_token_alg: "RS256"
)
# Redirect the user agent to request.url.id_token_alg must be the exact algorithm registered for the client. It is
checked against provider metadata and then used as the sole accepted ID Token
algorithm. RFC 9864 Ed25519 and Ed448 identifiers are supported only with
their matching issuer JWK curve; legacy EdDSA remains compatible with both.
Detached at_hash and c_hash validation uses the verified key to distinguish
Ed25519's SHA-512 profile from Ed448's SHAKE256 profile.
The browser binding is mandatory protocol correlation: a callback created in one browser session cannot establish a login in another. AttestoClient treats it as an opaque value and does not create, retain, or authorize the application session. A fresh random value with at least 256 bits of entropy retained in the server-side session (or protected by the framework's secure session mechanism) is a suitable binding.
HTTPS redirect URIs are required for web clients. HTTP is accepted only for
loopback redirects (127.0.0.0/8, [::1], or localhost) used by native
clients.
Handle the callback
Pass the callback's string-keyed parameter map. State is consumed before the token request, including for provider errors and invalid responses:
{:ok, completed} =
AttestoClient.AuthorizationCode.callback(store, callback_params,
browser_binding: browser_binding_from_secure_session,
client_auth: {:private_key_jwt, client_private_jwk},
timeout: 10_000
)
claims = completed.id_token_claims
tokens = completed.tokensA timeout means the token endpoint outcome is unknown. The code and transaction have already been consumed and must not be retried.
Successful verification establishes protocol facts such as issuer, audience, nonce, signature, and time validity. It does not decide that the subject may access your application. Apply authorization before creating a session.
When several providers share a client, prefer a distinct registered redirect
URI for each provider. This prevents provider mix-up when an older provider
does not return the authorization-response iss parameter.
Refresh rotation
Use a stable, non-secret key for the token record. Concurrent calls using the same key share one request and result:
{:ok, result} =
AttestoClient.Token.refresh(MyApp.OIDCRefreshes, token_record_key, tokens,
token_endpoint: metadata["token_endpoint"],
issuer: metadata["issuer"],
metadata: metadata,
client_id: "my-client",
client_auth: {:private_key_jwt, client_private_jwk},
subject: verified_subject,
id_token_alg: "RS256",
timeout: 10_000
)Compare-and-swap the stored token set with result.tokens against the prior
record version or refresh token, rejecting stale results. The coordinator does
not retain it after returning. If the response contains an ID Token,
result.id_token_claims contains its verified claims; issuer, audience,
subject, algorithm, time, and any at_hash are checked.
Revocation and logout
:ok =
AttestoClient.Token.revoke(tokens.refresh_token,
revocation_endpoint: metadata["revocation_endpoint"],
client_id: "my-client",
client_auth: {:private_key_jwt, client_private_jwk},
token_type_hint: "refresh_token"
)
{:ok, logout_url} =
AttestoClient.Logout.url(
issuer: metadata["issuer"],
metadata: metadata,
id_token_hint: tokens.id_token,
client_id: "my-client",
post_logout_redirect_uri: "https://app.example.com/logged-out",
state: application_generated_logout_state
)The application correlates logout state and decides when to terminate its local session. Revocation and provider logout do not substitute for local policy.