ProtoRune. Atproto. OAuth
(proto_rune v0.5.1)
Copy Markdown
AT Protocol OAuth 2.0 authorization flow.
Implements the authorization code flow with PAR (Pushed Authorization
Requests), PKCE and DPoP as described in
https://atproto.com/specs/oauth, as an alternative to app password
authentication (ProtoRune.login/3).
Only public clients are supported: the client authenticates with a
client_id URL pointing to its client metadata document, and every
authorization server request carries a DPoP proof signed with an ES256
key generated locally (see ProtoRune.Atproto.OAuth.DPoP). No external
JWT dependency is required.
Flow overview
alias ProtoRune.Atproto.OAuth
alias ProtoRune.Atproto.OAuth.Client
{:ok, client} =
Client.new(
client_id: "https://myapp.example.com/oauth/client-metadata.json",
redirect_uri: "https://myapp.example.com/oauth/callback"
)
# 1. Send the user to the authorization server
{:ok, url, pending} = OAuth.authorization_url(client, "alice.bsky.social")
# redirect the user to `url`, persist `pending` (e.g. in the session)
# 2. Handle the callback with the received query params
{:ok, session} = OAuth.exchange_code(client, pending, callback_params)
# 3. Refresh when the access token expires
{:ok, fresh_session} = OAuth.refresh(client, session)
# 4. Revoke the refresh token on logout
{:ok, :revoked} = OAuth.revoke(session, client_id: client.client_id)The pending map holds the flow state (PKCE verifier, state, DPoP
nonce) and must be kept between steps 1 and 2. It contains the DPoP
private key, so persist it securely.
Scope of this implementation
Issued access tokens are DPoP-bound. OAuth sessions implement the
ProtoRune.Session behaviour, so they can be passed to any XRPC or DSL
function; the session attaches the required DPoP proof to each request.
For long-running applications, ProtoRune.Atproto.OAuth.SessionManager
keeps a session fresh automatically. Confidential clients
(private_key_jwt) and client metadata document serving are out of
scope.
Summary
Functions
Starts the authorization flow and returns the URL to send the user to.
Exchanges the authorization code received on the callback for tokens.
Refreshes an OAuth session using its refresh token.
Revokes the session's refresh token at the authorization server.
Types
@type error() :: {:error, term()}
@type pending() :: %{ did: String.t(), handle: String.t() | nil, service_url: String.t(), issuer: String.t(), state: String.t(), code_verifier: String.t(), dpop_key: ProtoRune.Atproto.OAuth.DPoP.private_key(), dpop_jwk: ProtoRune.Atproto.OAuth.DPoP.jwk(), dpop_nonce: String.t() | nil, token_endpoint: String.t() }
Flow state produced by authorization_url/3 and consumed by
exchange_code/3.
Functions
@spec authorization_url(ProtoRune.Atproto.OAuth.Client.t(), String.t(), keyword()) :: {:ok, String.t(), pending()} | error()
Starts the authorization flow and returns the URL to send the user to.
Resolves the account's PDS from its handle or DID, discovers the authorization server through the OAuth metadata documents and pushes the authorization request (PAR) with PKCE and DPoP.
Returns {:ok, url, pending}. Redirect the user to url and keep
pending until the callback is exchanged with exchange_code/3.
Options
:state- Explicit state value (a random one is generated by default).:dpop_nonce- DPoP nonce from a previous interaction with the authorization server.
@spec exchange_code(ProtoRune.Atproto.OAuth.Client.t(), pending(), map()) :: {:ok, ProtoRune.Atproto.OAuth.Session.t()} | error()
Exchanges the authorization code received on the callback for tokens.
params is the query string params map of the callback request (string
or atom keys). The state param must match the one in pending and,
when present, iss must match the discovered authorization server.
Returns an OAuth Session with DPoP-bound tokens.
@spec refresh(ProtoRune.Atproto.OAuth.Client.t(), ProtoRune.Atproto.OAuth.Session.t()) :: {:ok, ProtoRune.Atproto.OAuth.Session.t()} | error()
Refreshes an OAuth session using its refresh token.
Follows refresh token rotation: when the server issues a new refresh token it replaces the old one, otherwise the previous one is kept.
@spec revoke( ProtoRune.Atproto.OAuth.Session.t(), keyword() ) :: {:ok, :revoked} | error()
Revokes the session's refresh token at the authorization server.
Discovers the revocation_endpoint from the authorization server
metadata (RFC 7009) of the session's issuer and posts a public-client
revocation request carrying a DPoP proof, bound to the session's access
token via the ath claim like the token endpoint calls.
Per RFC 7009 the server answers 200 even for unknown or already invalid
tokens, so {:ok, :revoked} means the token is gone, not that it was
still valid.
Options
:client_id- Required. Theclient_idthe session was issued to, as configured on theClientthat ran the flow.
Returns {:error, :revocation_not_supported} when the authorization
server declares no revocation endpoint; no revocation request is made
in that case.