Main OAuth2 flow orchestrator.
Handles complete OAuth2 authorization code flow with PKCE support, session management, and token exchange with automatic audit logging.
Summary
Functions
Maps an exchange failure to an enumerated audit error code.
Generates an authorization URL from the values bound to a session.
Generates OAuth authorization URL with PKCE support.
Generate authorization URL with optional scopes.
Build OAuth callback URL from connection, handling proxy headers.
Build HTTPS callback URL from connection.
Content Security Policy sent with the popup callback page.
Classify OAuth error types.
Cleans up expired OAuth sessions.
Converts an OAuth2.AccessToken into the string-keyed token response map used
by connection creation.
Creates an authorization session whose scopes, redirect, completion origin, and provider parameters are bound at creation.
Creates a new OAuth session for provider and tenant.
Safely decode OAuth state parameter.
Exchanges an authorization code for the session's provider and returns the raw token response.
Exchanges authorization code for access token.
Generates the popup callback page for a process_popup_callback/1 result.
Gets a valid (non-expired) session by token.
Log OAuth callback errors with audit trail.
Map OAuth error codes to atoms.
Parse OAuth scopes from space- or comma-separated strings, or lists.
Completes a popup OAuth callback by staging the authorization code.
Encodes a term as JSON that is safe to embed inside an HTML <script> element.
Returns true when the session carries staged-authorization bindings (a completion origin or an intent reference).
Types
@type cleanup_result() :: {:ok, non_neg_integer()} | {:error, atom()}
@type connection_result() :: {:ok, %Tango.Schemas.Connection{ __meta__: term(), access_token: term(), account_subject: term(), auth_failures: term(), auto_refresh_enabled: term(), connection_config: term(), expires_at: term(), granted_scopes: term(), id: term(), inserted_at: term(), last_auth_failure: term(), last_refresh_failure: term(), last_used_at: term(), lock_version: term(), metadata: term(), next_refresh_at: term(), oauth_client_id: term(), provider: term(), provider_id: term(), raw_payload: term(), refresh_attempts: term(), refresh_exhausted: term(), refresh_token: term(), status: term(), tenant_id: term(), token_type: term(), updated_at: term() }} | {:error, atom()}
@type session_result() :: {:ok, %Tango.Schemas.OAuthSession{ __meta__: term(), auth_params: term(), code_verifier: term(), completion_origin: term(), exchanged_at: term(), expected_connection_id: term(), expected_subject: term(), expires_at: term(), id: term(), inserted_at: term(), intent_reference: term(), metadata: term(), oauth_client_id: term(), provider: term(), provider_id: term(), redirect_uri: term(), scopes: term(), session_token: term(), state: term(), tenant_id: term(), updated_at: term() }} | {:error, atom()}
Functions
Maps an exchange failure to an enumerated audit error code.
Audit rows store only enumerated categories, never provider payloads; anything
unrecognized becomes :unknown_error.
@spec authorization_url(Tango.Schemas.OAuthSession.t()) :: auth_url_result()
Generates an authorization URL from the values bound to a session.
@spec authorize_url(String.t(), keyword()) :: auth_url_result()
Generates OAuth authorization URL with PKCE support.
Examples
iex> authorize_url("session_token_123", redirect_uri: "https://app.com/callback")
{:ok, "https://github.com/login/oauth/authorize?client_id=..."}
iex> authorize_url("invalid_token", redirect_uri: "https://app.com/callback")
{:error, :session_not_found}
Generate authorization URL with optional scopes.
Uses session/provider defaults when scopes list is empty, otherwise uses explicit scopes provided.
Build OAuth callback URL from connection, handling proxy headers.
Build HTTPS callback URL from connection.
Content Security Policy sent with the popup callback page.
Classify OAuth error types.
@spec cleanup_expired_sessions() :: cleanup_result()
Cleans up expired OAuth sessions.
Should be called periodically to prevent table bloat.
Examples
iex> cleanup_expired_sessions()
{:ok, 5} # 5 sessions cleaned up
Converts an OAuth2.AccessToken into the string-keyed token response map used
by connection creation.
@spec create_authorization_session(String.t(), String.t(), keyword()) :: session_result()
Creates an authorization session whose scopes, redirect, completion origin, and provider parameters are bound at creation.
@spec create_session(String.t(), String.t(), keyword()) :: session_result()
Creates a new OAuth session for provider and tenant.
Generates secure session tokens, PKCE parameters, and CSRF state.
Examples
iex> create_session("github", "user-123")
{:ok, %OAuthSession{}}
iex> create_session("nonexistent", "user-123")
{:error, :provider_not_found}
Safely decode OAuth state parameter.
Exchanges an authorization code for the session's provider and returns the raw token response.
Fails with {:error, :client_changed} when the provider's OAuth client no
longer matches the client recorded on the session. Other failures are reduced
to {:error, atom} or {:error, {:oauth_error, category}}; provider response
bodies are never returned.
@spec exchange_code(String.t(), String.t(), String.t(), keyword()) :: connection_result()
Exchanges authorization code for access token.
Validates session state, exchanges code with provider, and creates connection. SECURITY: Requires tenant_id for multi-tenant isolation.
Examples
iex> exchange_code("state_123", "auth_code_456", "tenant-123", redirect_uri: "https://app.com/callback")
{:ok, %Connection{}}
iex> exchange_code("invalid_state", "code", "tenant-123", redirect_uri: "https://app.com/callback")
{:error, :invalid_state}
iex> exchange_code("state_123", "code", "wrong-tenant", redirect_uri: "https://app.com/callback")
{:error, :tenant_mismatch}
@spec generate_callback_html(callback_result()) :: String.t()
Generates the popup callback page for a process_popup_callback/1 result.
The page posts the payload to the session's exact completion origin and never
falls back to a wildcard target. Payload and origin are encoded with
script_json/1 so they cannot break out of the script element.
@spec get_session(String.t()) :: session_result()
Gets a valid (non-expired) session by token.
Examples
iex> get_session("valid_token")
{:ok, %OAuthSession{}}
iex> get_session("expired_token")
{:error, :session_expired}
Log OAuth callback errors with audit trail.
The audit row stores the mapped error category and a hash of the session state. The provider's raw error code and description are not persisted.
Map OAuth error codes to atoms.
Parse OAuth scopes from space- or comma-separated strings, or lists.
@spec process_popup_callback(map()) :: callback_result()
Completes a popup OAuth callback by staging the authorization code.
Only sessions created with create_authorization_session/3 can complete
here. The code is exchanged into an encrypted, short-lived candidate and the
result carries only the opaque completion handle plus the session's intent
reference. Failures carry a stable category and the intent reference. Neither
shape includes tokens or provider response bodies.
Returns %{payload: payload, origin: origin}, where origin is the session's
validated completion origin. When the callback cannot be tied to a staged
session with an allowed origin, both values are nil and nothing is posted.
Encodes a term as JSON that is safe to embed inside an HTML <script> element.
<, >, &, and the JavaScript line separators are written as unicode
escapes, so no value can close the element or open an HTML comment.
Returns true when the session carries staged-authorization bindings (a completion origin or an intent reference).
Staged sessions complete only through Tango.stage_authorization/4.