OAuth orchestrator for Sigra authentication.
Coordinates the full OAuth flow: authorization URL generation with HMAC-signed state, callback processing with account routing (register/login/link-confirm), token refresh, and link/unlink operations.
Architecture (D-20)
Sigra.OAuth-- orchestrator (this module)Sigra.OAuth.Callback-- response processing and account routingSigra.OAuth.Strategies.*-- per-provider Assent wrappers
HMAC State (D-16)
Sigra owns the OAuth CSRF state parameter. On authorize, a nonce is
generated and signed via Sigra.Token.generate/4 with purpose
"sigra-oauth-state" and a 15-minute TTL. The callback verifies
this signature before processing the token exchange.
Account Scenarios
The callback processor handles five scenarios:
- Existing identity -- user logged in, identity fields updated (D-31)
- New user -- registered with auto-confirmed email (D-42), remember-me session (D-43)
- Email match -- existing user with same email, link confirmation required (D-01)
- No email -- provider didn't return email, error shown (D-08)
- UID/email conflict -- identity maps to user A, email maps to user B, blocked (D-09)
Summary
Functions
Generates an authorization URL for the given provider.
Retrieves OAuth tokens for an identity, auto-refreshing if expired.
Handles an OAuth callback from the provider.
Links an OAuth provider to an existing authenticated user.
Unlinks an OAuth provider from a user.
Functions
@spec authorize_url(map(), atom(), keyword()) :: {:ok, String.t(), map()} | {:error, atom() | %Sigra.Error.OAuthError{ __exception__: true, error_code: term(), message: term(), provider: term() }}
Generates an authorization URL for the given provider.
Resolves the provider's strategy wrapper, calls its authorize_url/1,
and replaces the state parameter with an HMAC-signed version.
Returns {:ok, url, session_params} where session_params includes
:sigra_state and any PKCE :code_verifier.
Examples
{:ok, url, session_params} = Sigra.OAuth.authorize_url(config, :google)
@spec get_tokens(map(), Sigra.Identity.t()) :: {:ok, map()} | {:error, :token_expired}
Retrieves OAuth tokens for an identity, auto-refreshing if expired.
When the access token is expired and a refresh token exists, calls the
provider's token refresh endpoint, persists the new tokens, and returns
updated tokens. If no refresh token is available or refresh fails,
returns {:error, :token_expired}.
The returned map uses logical key names (:access_token, :refresh_token).
The identity struct fields are named encrypted_* because the generated
Ecto schema uses Cloak for transparent encryption -- when loaded through
Ecto, these fields contain plaintext (decrypted) values.
Examples
{:ok, %{access_token: "plaintext_token"}} = Sigra.OAuth.get_tokens(config, identity)
@spec handle_callback(map(), atom(), map(), map()) :: {:ok, atom(), map(), map()} | {:link_confirmation_required, map()} | {:error, %Sigra.Error.OAuthError{ __exception__: true, error_code: term(), message: term(), provider: term() }}
Handles an OAuth callback from the provider.
Verifies the HMAC-signed state parameter, calls the strategy's
callback/3 to exchange the authorization code for tokens and user info,
then delegates to Sigra.OAuth.Callback.process_callback/4 for account
routing.
Returns
{:ok, :registered, user, session}-- new user created{:ok, :logged_in, user, session}-- existing identity matched{:link_confirmation_required, %{provider: p, email: e, ...}}-- email match{:error, %OAuthError{}}-- state mismatch, no email, provider error, etc.
@spec link_provider(map(), map(), map(), keyword()) :: {:ok, map()} | {:error, :already_linked | :sudo_required}
Links an OAuth provider to an existing authenticated user.
Creates a new identity record for the user. Requires the user to not already have an identity for this provider.
Requires sudo mode (D-05). Pass the current session via opts[:session].
Emits [:sigra, :oauth, :link, :stop] telemetry event (D-61).
The caller (controller/LiveView) is responsible for sending
notification emails on success using the generated email templates.
Options
:session- The current%Sigra.Session{}. Required for sudo check.
Returns
{:ok, identity}on success{:error, :already_linked}if the user already has this provider{:error, :sudo_required}if sudo mode is not active
@spec unlink_provider(map(), map(), atom() | String.t(), keyword()) :: {:ok, :unlinked} | {:error, :last_provider | :not_found | :sudo_required}
Unlinks an OAuth provider from a user.
Blocks if this is the user's last auth method and no password is set (D-03).
Requires sudo mode (D-05). Pass the current session via opts[:session].
Emits [:sigra, :oauth, :unlink, :stop] telemetry event.
The caller (controller/LiveView) is responsible for sending
notification emails on success using the generated email templates (D-07).
Options
:session- The current%Sigra.Session{}. Required for sudo check.
Returns
{:ok, :unlinked}on success{:error, :last_provider}if last auth method and no password{:error, :not_found}if identity not found{:error, :sudo_required}if sudo mode is not active