# `Sigra.OAuth`
[🔗](https://github.com/sztheory/sigra/blob/v1.20.0/lib/sigra/oauth.ex#L1)

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 routing
- `Sigra.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:

1. **Existing identity** -- user logged in, identity fields updated (D-31)
2. **New user** -- registered with auto-confirmed email (D-42), remember-me session (D-43)
3. **Email match** -- existing user with same email, link confirmation required (D-01)
4. **No email** -- provider didn't return email, error shown (D-08)
5. **UID/email conflict** -- identity maps to user A, email maps to user B, blocked (D-09)

# `authorize_url`
*since 0.5.0* 

```elixir
@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)

# `get_tokens`
*since 0.5.0* 

```elixir
@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)

# `handle_callback`
*since 0.5.0* 

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

# `link_provider`
*since 0.5.0* 

```elixir
@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

# `unlink_provider`
*since 0.5.0* 

```elixir
@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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
