AshAuthentication. Oauth2Server. Token
(ash_authentication_oauth2_server v0.1.0)
Copy Markdown
View Source
Protocol-pure logic for the /oauth/token endpoint.
Supports two grant types:
authorization_code— with PKCE verification, redirect/resource binding checks, and one-shot consumption of the code.refresh_token— with rotation and reuse detection per OAuth 2.1 §4.3.1. A second use of an already-rotated refresh token revokes the entire descendant chain.
All functions return tagged tuples; controllers translate them to HTTP.
Summary
Types
Result of a successful grant — the bundle returned to the client.
Functions
Exchange an authorization code (with PKCE verifier) for an access + refresh
token pair. Consumes the code atomically; a second call with the same code
returns {:error, :reuse}.
Exchange a refresh token for a new access + refresh pair. Implements
rotation + reuse detection (OAuth 2.1 §4.3.1): a second use of an
already-rotated refresh token returns {:error, :reuse} and revokes the
descendant chain.
Revoke a token per RFC 7009. Always returns :ok regardless of whether the
token existed, was already revoked, or belonged to a different client — the
RFC requires the endpoint not to leak token state.
Types
@type token_response() :: %{ access_token: String.t(), token_type: String.t(), expires_in: pos_integer(), refresh_token: String.t(), scope: String.t() }
Result of a successful grant — the bundle returned to the client.
Functions
@spec exchange_authorization_code(server :: module(), params :: map()) :: {:ok, token_response()} | {:error, atom()}
Exchange an authorization code (with PKCE verifier) for an access + refresh
token pair. Consumes the code atomically; a second call with the same code
returns {:error, :reuse}.
@spec exchange_refresh_token(server :: module(), params :: map()) :: {:ok, token_response()} | {:error, atom()}
Exchange a refresh token for a new access + refresh pair. Implements
rotation + reuse detection (OAuth 2.1 §4.3.1): a second use of an
already-rotated refresh token returns {:error, :reuse} and revokes the
descendant chain.
The rotation is atomic at the data-layer level — every "is this
refresh usable" check lives in the :rotate action's filter, so
validate + rotate is one query in the happy path. On a 0-row result
(race lost, invalid token, expired, etc.) we do a follow-up read to
distinguish :reuse from the other failure modes.
Revoke a token per RFC 7009. Always returns :ok regardless of whether the
token existed, was already revoked, or belonged to a different client — the
RFC requires the endpoint not to leak token state.
Only refresh tokens are revocable here: access tokens are stateless JWTs. When a refresh token is revoked, the entire descendant chain (rotated-to successors) is also revoked, so a refresh that has been rotated through cannot resurrect the session.
The params map mirrors what RFC 7009 §2.1 sends to the endpoint:
"token"(required) — the raw token string the client wishes to revoke."client_id"(required) — the public client identifier."token_type_hint"(optional) —"refresh_token"or"access_token". Treated as a hint only; access-token revocation is a silent no-op.