AshAuthentication. Oauth2Server. Authorize
(ash_authentication_oauth2_server v0.1.0)
Copy Markdown
View Source
Protocol-pure logic for the /oauth/authorize endpoint.
Controllers in ash_authentication_phoenix are thin wrappers around
validate_request/2, consented?/4, grant_consent!/4, and
issue_code!/3. None of these functions touch Plug.Conn.
Summary
Types
The validated authorize-request payload. The struct is intentionally small — enough to render a consent screen and ultimately mint an authorization code.
Functions
Has the user already consented to this client at a scope that covers the currently-requested scope?
Record (or refresh) a consent row for (user, client) at the given scope.
Mint a new short-lived authorization code bound to the user, client, scope, PKCE challenge, and resource URI.
Validate an inbound authorize request.
Types
@type validated() :: %{ client: Ash.Resource.record(), redirect_uri: String.t(), code_challenge: String.t(), scope: String.t(), state: String.t(), resource: String.t() }
The validated authorize-request payload. The struct is intentionally small — enough to render a consent screen and ultimately mint an authorization code.
Functions
@spec consented?( server :: module(), user :: Ash.Resource.record(), client :: Ash.Resource.record(), requested_scope :: String.t() ) :: boolean()
Has the user already consented to this client at a scope that covers the currently-requested scope?
Returns true ONLY when prior consent exists AND its scope is a superset of
requested_scope. This prevents silent privilege expansion when a client
later asks for more scopes than the user originally agreed to.
@spec grant_consent!( server :: module(), user :: Ash.Resource.record(), client :: Ash.Resource.record(), scope :: String.t() ) :: Ash.Resource.record()
Record (or refresh) a consent row for (user, client) at the given scope.
@spec issue_code!( server :: module(), user :: Ash.Resource.record(), validated :: validated() ) :: Ash.Resource.record()
Mint a new short-lived authorization code bound to the user, client, scope, PKCE challenge, and resource URI.
@spec validate_request(server :: module(), params :: map()) :: {:ok, validated()} | {:error, :bad_redirect_uri} | {:error, String.t(), String.t()}
Validate an inbound authorize request.
Returns:
{:ok, validated}— request is structurally sound and the client + redirect_uri are known.{:error, :bad_redirect_uri}— redirect_uri is missing or doesn't match a registered URI; per RFC 6749 §4.1.2.1 the controller MUST NOT redirect.{:error, error_code, description}— any other validation error. Controllers redirect these errors back toredirect_uri.
A note on the state parameter
Clients MUST set state to a cryptographically random, unguessable
value (RFC 6749 §10.12 / RFC 9700 §4.7). The server echoes it back via
the redirect so the client can correlate the response with its
pending request — and verify the response didn't come from a CSRF or
injection attack.
This means clients should NOT use state as a stash for
application-level data like a "return-to" URL or routing hints. That
pattern is unsafe — the value travels through the user-agent and
query string and is reflected back by the server, so any data put in
it can be observed, replayed, or tampered with. Stash that data
server-side (keyed by a fresh state), or encode it in a signed
cookie.
We don't enforce a shape or entropy minimum here, but anything other
than a random per-request value defeats the purpose of state.