Tango.Auth (tango v0.3.0)

Copy Markdown View Source

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

auth_url_result()

@type auth_url_result() :: {:ok, String.t()} | {:error, atom()}

callback_result()

@type callback_result() :: %{payload: map() | nil, origin: String.t() | nil}

cleanup_result()

@type cleanup_result() :: {:ok, non_neg_integer()} | {:error, atom()}

connection_result()

@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()}

session_result()

@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

audit_error_code(reason)

Maps an exchange failure to an enumerated audit error code.

Audit rows store only enumerated categories, never provider payloads; anything unrecognized becomes :unknown_error.

authorization_url(session)

@spec authorization_url(Tango.Schemas.OAuthSession.t()) :: auth_url_result()

Generates an authorization URL from the values bound to a session.

authorize_url(session_token, opts \\ [])

@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}

authorize_url_with_scopes(session_token, redirect_uri, scopes)

Generate authorization URL with optional scopes.

Uses session/provider defaults when scopes list is empty, otherwise uses explicit scopes provided.

build_callback_url(conn)

Build OAuth callback URL from connection, handling proxy headers.

build_https_callback_url(conn)

Build HTTPS callback URL from connection.

callback_content_security_policy()

Content Security Policy sent with the popup callback page.

classify_oauth_error(arg1)

Classify OAuth error types.

cleanup_expired_sessions()

@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

convert_token_to_response(token)

Converts an OAuth2.AccessToken into the string-keyed token response map used by connection creation.

create_authorization_session(provider_name, tenant_id, opts)

@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.

create_session(provider_name, tenant_id, opts \\ [])

@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}

decode_state_safely(state)

Safely decode OAuth state parameter.

decode_state_with_tenant(encoded_state)

exchange_authorization_code(session, authorization_code, redirect_uri)

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.

exchange_code(state, authorization_code, tenant_id, opts)

@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}

generate_callback_html(map)

@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.

get_session(session_token)

@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_error(state, error, error_description)

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_code(arg1)

Map OAuth error codes to atoms.

parse_scopes(scopes)

Parse OAuth scopes from space- or comma-separated strings, or lists.

process_popup_callback(params)

@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.

script_json(term)

@spec script_json(term()) :: String.t()

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.

staged_session?(o_auth_session)

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.