Atex.OAuth
(atex v0.9.1)
View Source
OAuth 2.0 implementation for AT Protocol authentication.
This module provides utilities for implementing OAuth flows compliant with the AT Protocol specification. It includes support for:
- Pushed Authorization Requests (PAR)
- DPoP (Demonstration of Proof of Possession) tokens
- JWT client assertions
- PKCE (Proof Key for Code Exchange)
- Token refresh
- Handle to PDS resolution
Configuration
See Atex.Config.OAuth module for configuration documentation.
Usage Example
iex> pds = "https://bsky.social"
iex> login_hint = "example.com"
iex> {:ok, authz_server} = Atex.OAuth.get_authorization_server(pds)
iex> {:ok, authz_metadata} = Atex.OAuth.get_authorization_server_metadata(authz_server)
iex> state = Atex.OAuth.create_nonce()
iex> code_verifier = Atex.OAuth.create_nonce()
iex> {:ok, auth_url} = Atex.OAuth.create_authorization_url(
authz_metadata,
state,
code_verifier,
login_hint
)
Summary
Functions
Create an OAuth authorization URL for a PDS.
Get a map containing the client metadata information needed for an authorization server to validate this client.
Returns the composite session key ("<did>:<nonce>") for the currently active
OAuth session on the given conn.
Deletes a session from the store and revokes its tokens.
Fetch the authorization server for a given Personal Data Server (PDS).
Fetch the metadata for an OAuth authorization server.
Retrieves the configured JWT private key for signing client assertions.
Returns all composite session keys stored for this device's conn session.
Revokes the access and refresh tokens with the authorization server.
Switches the active session to the given composite session key.
Exchange an OAuth authorization code for a set of access and refresh tokens.
Types
@type create_authorization_url_option() :: {:key, JOSE.JWK.t()} | {:client_id, String.t()} | {:redirect_uri, String.t()} | {:scopes, String.t()}
@type create_client_metadata_option() :: {:key, JOSE.JWK.t()} | {:client_id, String.t()} | {:redirect_uri, String.t()} | {:extra_redirect_uris, [String.t()]} | {:scopes, String.t()}
@type refresh_token_option() :: {:key, JOSE.JWK.t()} | {:client_id, String.t()} | {:redirect_uri, String.t()} | {:scopes, String.t()}
@type tokens() :: %{ access_token: String.t(), refresh_token: String.t(), did: String.t(), expires_at: NaiveDateTime.t() }
@type validate_authorization_code_option() :: {:key, JOSE.JWK.t()} | {:client_id, String.t()} | {:redirect_uri, String.t()} | {:scopes, String.t()}
Functions
@spec create_authorization_url( authorization_metadata(), String.t(), String.t(), String.t(), [create_authorization_url_option()] ) :: {:ok, String.t()} | {:error, any()}
Create an OAuth authorization URL for a PDS.
Submits a PAR request to the authorization server and constructs the authorization URL with the returned request URI. Supports PKCE, DPoP, and client assertions as required by the AT Protocol.
Parameters
authz_metadata- Authorization server metadata containing endpoints, fetched fromget_authorization_server_metadata/1state- Random token for session validationcode_verifier- PKCE code verifierlogin_hint- User identifier (handle or DID) for pre-filled login
Returns
{:ok, authorization_url}- Successfully created authorization URL{:ok, :invalid_par_response}- Server respondend incorrectly to the request{:error, reason}- Error creating authorization URL
@spec create_client_assertion(JOSE.JWK.t(), String.t(), String.t()) :: String.t()
@spec create_client_metadata([create_client_metadata_option()]) :: map()
Get a map containing the client metadata information needed for an authorization server to validate this client.
@spec create_dpop_token(JOSE.JWK.t(), Req.Request.t(), any(), map()) :: String.t()
@spec current_session_key(Plug.Conn.t()) :: {:ok, String.t()} | :error
Returns the composite session key ("<did>:<nonce>") for the currently active
OAuth session on the given conn.
This is the primary way to identify which session is active for a request. The
returned key can be passed directly to Atex.OAuth.SessionStore.get/1 or used
to construct an Atex.XRPC.OAuthClient.
Returns
{:ok, session_key}- The composite key for the active session:error- No active session found in the conn
Examples
case Atex.OAuth.current_session_key(conn) do
{:ok, key} -> {:ok, client} = Atex.XRPC.OAuthClient.new(key)
:error -> redirect_to_login(conn)
end
@spec delete_session(Atex.OAuth.Session.t() | String.t()) :: :ok | {:error, :not_found | any()}
Deletes a session from the store and revokes its tokens.
This is the primary function for logging out a session. It:
- Fetches the session data from the store if a key is provided
- Revokes the tokens with the authorization server
- Removes the session from the store
Parameters
session_or_key- Either aSession.t()struct or a composite session key string
Returns
:ok- Session deleted and tokens revoked{:error, :not_found}- Session not found in store{:error, reason}- Token revocation or store deletion failed
Examples
# Using a session key
case Atex.OAuth.delete_session("did:plc:abc123:device-nonce") do
:ok -> :logged_out
{:error, :not_found} -> :session_already_gone
end
# Using a session struct
{:ok, session} = Atex.OAuth.SessionStore.get("did:plc:abc123:device-nonce")
:ok = Atex.OAuth.delete_session(session)
Fetch the authorization server for a given Personal Data Server (PDS).
Makes a request to the PDS's .well-known/oauth-protected-resource endpoint
to discover the associated authorization server that should be used for the
OAuth flow. Results are cached for 1 hour to reduce load on third-party PDSs.
Parameters
pds_host- Base URL of the PDS (e.g., "https://bsky.social")fresh- Iftrue, bypasses the cache and fetches fresh data (default:false)
Returns
{:ok, authorization_server}- Successfully discovered authorization server URL{:error, :invalid_metadata}- Server returned invalid metadata{:error, reason}- Error discovering authorization server
@spec get_authorization_server_metadata(String.t(), boolean()) :: {:ok, authorization_metadata()} | {:error, any()}
Fetch the metadata for an OAuth authorization server.
Retrieves the metadata from the authorization server's
.well-known/oauth-authorization-server endpoint, providing endpoint URLs
required for the OAuth flow. Results are cached for 1 hour to reduce load on
third-party PDSs.
Parameters
issuer- Authorization server issuer URLfresh- Iftrue, bypasses the cache and fetches fresh data (default:false)
Returns
{:ok, metadata}- Successfully retrieved authorization server metadata{:error, :invalid_metadata}- Server returned invalid metadata{:error, :invalid_issuer}- Issuer mismatch in metadata{:error, any()}- Other error fetching metadata
@spec get_key() :: JOSE.JWK.t()
Retrieves the configured JWT private key for signing client assertions.
Loads the private key from configuration, decodes the base64-encoded DER data, and creates a JOSE JWK structure with the key ID field set.
Returns
A JOSE.JWK struct containing the private key and key identifier.
Raises
Application.Env.Errorif the private_key or key_id configuration is missing
Examples
key = OAuth.get_key()
key = OAuth.get_key()
@spec list_session_keys(Plug.Conn.t()) :: [String.t()]
Returns all composite session keys stored for this device's conn session.
Each key corresponds to a distinct authenticated account on this device. The list is ordered with the most recently logged-in account first.
Examples
keys = Atex.OAuth.list_session_keys(conn)
# => ["did:plc:abc:nonce1", "did:plc:xyz:nonce2"]
@spec refresh_token( String.t(), JOSE.JWK.t(), String.t(), String.t(), [refresh_token_option()] ) :: {:ok, tokens(), String.t()} | {:error, any()}
@spec request_protected_dpop_resource( Req.Request.t(), String.t(), String.t(), JOSE.JWK.t(), String.t() | nil ) :: {:ok, Req.Response.t(), String.t() | nil} | {:error, any()}
@spec revoke_tokens(Atex.OAuth.Session.t(), authorization_metadata()) :: :ok | {:error, any()}
Revokes the access and refresh tokens with the authorization server.
Sends both tokens to the revocation endpoint as defined in RFC 7009. This invalidates the tokens on the PDS side, preventing further use.
Parameters
session- The session containing tokens to revokeauthz_metadata- Authorization server metadata includingrevocation_endpoint
Returns
:ok- Tokens successfully revoked (or revocation endpoint unreachable){:error, reason}- Revocation failed
@spec send_oauth_dpop_request(Req.Request.t(), JOSE.JWK.t(), String.t() | nil) :: {:ok, map(), String.t()} | {:error, any(), String.t()}
@spec switch_session(Plug.Conn.t(), String.t()) :: {:ok, Plug.Conn.t()} | {:error, :not_found}
Switches the active session to the given composite session key.
Validates that the key is present in the conn's session list and that the corresponding session still exists in the store before updating the conn.
Returns
{:ok, conn}- Active session switched; the returned conn has the updated session and should be used for subsequent operations{:error, :not_found}- The key is not in the session list or the session no longer exists in the store
Examples
case Atex.OAuth.switch_session(conn, "did:plc:xyz:nonce2") do
{:ok, conn} -> send_resp(conn, 200, "Switched accounts")
{:error, :not_found} -> send_resp(conn, 404, "Session not found")
end
@spec validate_authorization_code( authorization_metadata(), JOSE.JWK.t(), String.t(), String.t(), [validate_authorization_code_option()] ) :: {:ok, tokens(), String.t()} | {:error, any()}
Exchange an OAuth authorization code for a set of access and refresh tokens.
Validates the authorization code by submitting it to the token endpoint along with the PKCE code verifier and client assertion. Returns access tokens for making authenticated requests to the relevant user's PDS.
Parameters
authz_metadata- Authorization server metadata containing token endpointdpop_key- JWK for DPoP token generationcode- Authorization code from OAuth callbackcode_verifier- PKCE code verifier from authorization flow
Returns
{:ok, tokens, nonce}- Successfully obtained tokens with returned DPoP nonce{:error, reason}- Error exchanging code for tokens