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

Core authentication orchestrator.

All security-critical auth operations live here. The generated
`MyApp.Auth` context delegates to these functions for registration,
authentication, session management, and magic link flows.

## Usage

    Sigra.Auth.register(MyApp.Repo, attrs, changeset_fn: &MyApp.User.registration_changeset/1)
    Sigra.Auth.authenticate(MyApp.Repo, params, user_schema: MyApp.User)

## Security Properties

- User enumeration prevention: generic error messages, constant-time operations
- Hash upgrade: transparent bcrypt-to-Argon2id migration on successful login
- Failed attempt tracking: incremented on wrong password, reset on success
- Magic link: single-use, 10-minute TTL, rate limited
- Telemetry: all operations emit structured events

# `authenticate`
*since 0.2.0* 

```elixir
@spec authenticate(module() | Sigra.Config.t(), map(), keyword()) ::
  {:ok, struct()}
  | {:ok, struct(), map()}
  | {:error, :invalid_credentials | :unconfirmed | :account_locked}
```

Authenticates a user by email and password.

Normalizes the email, looks up the user, verifies the password with
automatic hash upgrade support (bcrypt -> Argon2id), and tracks failed
login attempts.

Returns `{:ok, user}` on success with failed attempts reset to 0,
`{:error, :invalid_credentials}` on failure, or `{:error, :unconfirmed}`
when confirmation is required and the user has not confirmed their email.

## Options

- `:user_schema` - Required. The Ecto schema module for users.
- `:require_confirmation` - Whether to check `confirmed_at`. Default: `false`.

## Telemetry

- `[:sigra, :auth, :login, :start | :stop | :exception]` span
- `[:sigra, :auth, :hash_upgraded]` event when hash is upgraded

# `cancel_deletion`
*since 0.8.0* 

```elixir
@spec cancel_deletion(Sigra.Config.t(), struct(), keyword()) ::
  {:ok, struct()} | {:error, term()}
```

Cancel scheduled account deletion.

Clears deletion timestamps and reactivates the account. The user
must re-authenticate (all sessions were revoked on scheduling).

# `cancel_email_change`
*since 0.8.0* 

```elixir
@spec cancel_email_change(Sigra.Config.t(), struct(), keyword()) ::
  {:ok, struct()} | {:error, term()}
```

Cancel a pending email change.

Clears the `pending_email` field and deletes the email change token.

# `change_password`
*since 0.8.0* 

```elixir
@spec change_password(Sigra.Config.t(), struct(), String.t(), map(), keyword()) ::
  {:ok, struct()} | {:error, term()}
```

Change password with current password verification.

Verifies the current password, updates to the new password, and
invalidates all sessions except the current one.

# `complete_mfa_verification`
*since 0.6.0* 

```elixir
@spec complete_mfa_verification(
  Sigra.Config.t(),
  struct(),
  Sigra.Session.t(),
  keyword()
) ::
  {:ok, map()} | {:error, term()}
```

Completes MFA verification by rotating the session token and upgrading
the session type from :mfa_pending to :standard or :remember_me.

Called after successful TOTP or backup code verification. The old
mfa_pending session token is invalidated and a new token is issued,
preventing session fixation attacks.

## Options

  * `:remember_me` - If `true`, upgrades to `:remember_me` session. Default: `false`.
  * `:trust_browser` - If `true`, includes trust_browser flag in result for
    "trust this browser" cookie. Default: `false`.

## Returns

  * `{:ok, %{session: session, trust_browser: boolean}}` on success
  * `{:error, reason}` if session creation fails

# `confirm_email_change`
*since 0.8.0* 

```elixir
@spec confirm_email_change(Sigra.Config.t(), String.t(), keyword()) ::
  {:ok, struct()} | {:error, term()}
```

Confirm an email change via token.

Verifies the HMAC-signed token, updates the user's email, and
invalidates all existing sessions (forcing re-authentication).

# `confirm_sudo`
*since 0.4.0* 

```elixir
@spec confirm_sudo(Sigra.Config.t(), binary(), keyword()) ::
  :ok | {:error, :not_found}
```

Confirm sudo mode by updating sudo_at timestamp.

Emits a `[:sigra, :session, :sudo]` telemetry span.

# `confirm_user`
*since 0.3.0* 

```elixir
@spec confirm_user(module(), String.t(), keyword()) ::
  {:ok, struct()}
  | {:error, :token_expired | :token_invalid | :already_confirmed}
```

Confirms a user by HMAC-signed link token.

Verifies the HMAC signature, decodes the raw token, hashes it,
looks up in DB. On success, sets confirmed_at and deletes all
confirm/confirm_code tokens for the user in a single transaction.

Returns `{:ok, user}`, `{:error, :token_expired}`, `{:error, :token_invalid}`,
or `{:error, :already_confirmed}`.

## Options

- `:secret_key_base` - Required. The host app's secret key base.
- `:user_token_schema` - Required. The Ecto schema module for user tokens.
- `:user_schema` - Required. The Ecto schema module for users.
- `:confirmation_ttl` - Token TTL in seconds. Default: `172800` (48 hours).

# `create_api_token`
*since 0.7.0* 

```elixir
@spec create_api_token(Sigra.Config.t(), struct(), map()) ::
  {:ok, String.t(), struct()} | {:error, term()}
```

Creates an API token for the user.

Returns `{:ok, raw_key, token}` on success. Sends a notification email
on successful creation if the email module is configured (D-62).

## Parameters

- `config` - A `%Sigra.Config{}` struct
- `user` - The user struct (must have an `:id` field)
- `attrs` - A map with `:name`, `:scopes`, and optional `:expires_at`

# `create_session`
*since 0.4.0* 

```elixir
@spec create_session(Sigra.Config.t(), struct(), map(), keyword()) ::
  {:ok, Sigra.Session.t()} | {:error, term()}
```

Create a new session for the user with connection metadata.

Creates a session via the configured SessionStore and emits a
`[:sigra, :session, :create]` telemetry span.

## Phase 14: organization selector (D-12, D-26, ORG-SCOPE-06)

When `config.organizations_module` is set, the function runs the
`Sigra.Organizations.select_active_organization/3` selector once per
login and writes the result (or `nil`) into the newly-created session
row via `SessionStore.update_active_organization/3`. The selector
call is wrapped in a `try/rescue` block and MUST NOT fail the login
— selector failures fall back to `active_organization_id: nil` and
the user sees the picker on their next `RequireMembership` hit
(T-14-13 mitigation).

## Options

- `:session_store` - Override the session store from config.
- `:previous_active_organization_id` - Resume pointer passed to
  `Sigra.Organizations.select_active_organization/3`. When a user
  with 2+ memberships logs in, the selector uses this to pick up
  where the user left off (D-12).

# `delete_all_sessions`
*since 0.4.0* 

```elixir
@spec delete_all_sessions(Sigra.Config.t(), term(), keyword()) ::
  {non_neg_integer(), nil}
```

Delete all sessions for a user. Broadcasts PubSub disconnect per D-16.

Returns `{count, nil}` where count is the number of deleted sessions.

## Options

- `:except_token` - Hashed token to exclude (current session).
- `:pubsub` - Phoenix.PubSub module name for LiveView disconnect broadcasts.

# `delete_session`
*since 0.4.0* 

```elixir
@spec delete_session(Sigra.Config.t(), binary(), keyword()) :: :ok
```

Delete a specific session by its hashed token.

Emits a `[:sigra, :session, :delete]` telemetry span.

# `execute_deletion`
*since 0.8.0* 

```elixir
@spec execute_deletion(Sigra.Config.t(), struct(), keyword()) ::
  {:ok, atom()} | {:error, term()}
```

Execute account deletion (called by Oban worker).

Applies the configured deletion strategy (soft_delete, hard_delete,
or anonymize) to finalize the account removal.

# `generate_confirmation_token`
*since 0.3.0* 

```elixir
@spec generate_confirmation_token(module(), struct(), keyword()) ::
  {String.t(), String.t(), struct(), struct()}
```

Generates a confirmation link token AND a 6-digit code for the given user.

Returns `{encoded_token, code, link_token_struct, code_token_struct}`.
The encoded_token is HMAC-signed for URL use. The code is a random 6-digit
numeric string. Both are stored as SHA-256 hashes in the DB.

Per D-01: link-first, code as fallback. Both generated together.

## Options

- `:secret_key_base` - Required. The host app's secret key base.
- `:user_token_schema` - Required. The Ecto schema module for user tokens.

# `generate_jwt_tokens`
*since 0.7.0* 

```elixir
@spec generate_jwt_tokens(Sigra.Config.t(), struct(), [String.t()]) ::
  {:ok, map()} | {:error, term()}
```

Generates JWT access + refresh tokens for a user.

# `link_provider`
*since 0.5.0* 

```elixir
@spec link_provider(Sigra.Config.t() | map(), map(), map(), keyword()) ::
  {:ok, map()} | {:error, term()}
```

Links an OAuth provider to an existing authenticated user.

Requires sudo mode active. Creates identity record, sends notification email.

Returns `{:ok, identity}` or `{:error, reason}`.

# `list_api_scopes`
*since 0.7.0* 

```elixir
@spec list_api_scopes(Sigra.Config.t()) :: [String.t()]
```

Returns all registered API token scopes.

# `list_api_tokens`
*since 0.7.0* 

```elixir
@spec list_api_tokens(Sigra.Config.t(), term(), keyword()) ::
  {[struct()], String.t() | nil}
```

Lists active API tokens for a user with cursor pagination.

# `list_sessions`
*since 0.4.0* 

```elixir
@spec list_sessions(Sigra.Config.t(), term(), keyword()) :: [Sigra.Session.t()]
```

List all active sessions for a user.

Excludes `:mfa_pending` sessions from the listing since they represent
incomplete authentication attempts, not active sessions (D-29).

# `login_oauth`
*since 0.5.0* 

```elixir
@spec login_oauth(Sigra.Config.t() | map(), atom(), map(), map()) ::
  {:ok, :logged_in, map(), map()}
  | {:link_confirmation_required, map()}
  | {:error, term()}
```

Logs in an existing user via OAuth identity match.

Looks up identity by (provider, provider_uid), updates identity fields,
creates session with auth_method: :oauth metadata.

Delegates to `Sigra.OAuth.Callback.process_callback/4`.

# `normalize_email`
*since 0.10.0* 

```elixir
@spec normalize_email(String.t() | nil) :: String.t() | nil
```

Normalizes an email address for storage and lookup.

Applies `String.trim/1` (leading/trailing whitespace) then
`String.downcase/1`. Sigra stores emails in a `citext` column so
case-insensitive matching is already enforced at the database level, but
callers doing in-memory comparisons (login form pre-flight, fixture
setup, audit queries) should normalize first.

Returns a string on any binary input; passes through `nil` untouched so
callers can pipe through without nil-guarding.

## Scope (RFC 5321 §2.3.4 caveat)

This function does NOT strip interior whitespace from the local-part.
Quoted local-parts with embedded whitespace (extremely rare, technically
permitted by RFC 5321 §2.3.4) pass through unchanged. Callers MUST also
run `valid_email?/1` — which rejects interior whitespace via regex —
before persisting. Passing an unvalidated normalized value straight to a
`citext` column is a caller error.

## Examples

    iex> Sigra.Auth.normalize_email("Alice@Example.COM")
    "alice@example.com"

    iex> Sigra.Auth.normalize_email("  bob@example.com  ")
    "bob@example.com"

    iex> Sigra.Auth.normalize_email("")
    ""

    iex> Sigra.Auth.normalize_email(nil)
    nil

    # Interior whitespace is retained — run valid_email?/1 to reject.
    iex> Sigra.Auth.normalize_email("Alice @example.com")
    "alice @example.com"

    iex> Sigra.Auth.valid_email?("alice @example.com")
    false

# `refresh_jwt`
*since 0.7.0* 

```elixir
@spec refresh_jwt(Sigra.Config.t(), String.t()) ::
  {:ok, map()}
  | {:error,
     :invalid_token | :token_expired | :reuse_detected | :jwt_refresh_aborted}
```

Refreshes JWT tokens using a refresh token.

When **`:audit_schema`** is set, persistence and JWT audit share one
transaction; failures there return **`{:error, :jwt_refresh_aborted}`** (see
**`Sigra.JWT.refresh/3`**).

# `register`
*since 0.2.0* 

```elixir
@spec register(module(), map(), keyword()) ::
  {:ok, struct()} | {:error, Ecto.Changeset.t()} | {:error, :email_taken}
```

Registers a user.

Takes a repo module, attributes map, and options. The `:changeset_fn`
option must be a function that takes attrs and returns an `Ecto.Changeset`.

Returns `{:ok, user}` on success, `{:error, changeset}` for validation
errors, or `{:error, :email_taken}` when the email uniqueness constraint
is violated (enumeration-safe -- callers should show a generic message).

## Options

- `:changeset_fn` - Required. Function `(attrs -> Ecto.Changeset.t())`.

## Telemetry

Emits `[:sigra, :auth, :register, :start | :stop | :exception]` span.
On success, metadata includes `%{user_id: id}`.

# `register_oauth`
*since 0.5.0* 

```elixir
@spec register_oauth(Sigra.Config.t() | map(), atom(), map(), map()) ::
  {:ok, :registered, map(), map()} | {:error, term()}
```

Registers a new user via OAuth provider callback data.

Creates user + identity in a transaction. Sets confirmed_at if provider
email is trusted. Creates a session with auth_method: :oauth metadata.

Delegates to `Sigra.OAuth.Callback.process_callback/4` for the full
account routing logic (register/login/link-confirm).

# `register_user_multi`
*since 0.4.0* 

```elixir
@spec register_user_multi(
  map(),
  keyword()
) :: Ecto.Multi.t()
```

Pure `Ecto.Multi` builder for user registration.

Returns a multi with a `:user` step that inserts the user via the
configured `:changeset_fn`. When `:audit_schema` is set (see
`audit_opts_from_keyword/1`), appends `auth.register.success` via
`Audit.log_multi_safe/3` in the same Multi. Makes ZERO Repo calls during
construction — composable via `Ecto.Multi.append/2`. Intended for use by
`Sigra.Organizations.Invitations.accept_with_signup/3` (Phase 17 D-07)
to atomically compose signup + confirm + membership + accept.

## Options

  * `:changeset_fn` — REQUIRED. 1-arity function producing a user
    changeset from `attrs`.

## Example

    Ecto.Multi.new()
    |> Ecto.Multi.append(
         Sigra.Auth.register_user_multi(attrs, changeset_fn: &User.registration_changeset/1)
       )
    |> MyApp.Repo.transact()

# `request_email_change`
*since 0.8.0* 

```elixir
@spec request_email_change(Sigra.Config.t(), struct(), String.t(), keyword()) ::
  {:ok, struct(), String.t()} | {:error, term()}
```

Request an email change for the user.

Generates a verification token for the new email address and returns
it for delivery. The email is not changed until confirmed via
`confirm_email_change/3`.

# `request_magic_link`
*since 0.2.0* 

```elixir
@spec request_magic_link(module(), String.t(), keyword()) ::
  {:ok, {String.t(), String.t()}} | {:ok, :sent} | {:error, :rate_limited}
```

Requests a magic link for the given email.

If the user exists and is not rate-limited, generates a token and returns
`{:ok, {raw_token, url}}`. If the user does not exist, returns `{:ok, :sent}`
to prevent email enumeration. If rate-limited, returns `{:error, :rate_limited}`.

## Options

- `:user_schema` - Required. The Ecto schema module for users.
- `:user_token_schema` - Required. The Ecto schema module for user tokens (e.g., `MyApp.Accounts.UserToken`).
- `:url_fun` - Required. Function `(token -> url_string)`.
- `:rate_limiter` - Module implementing `Sigra.RateLimiter`. Default: `nil` (no rate limiting).
- `:max_requests` - Max magic link requests per window. Default: `3`.
- `:window_ms` - Rate limit window in milliseconds. Default: `900_000` (15 min).

# `request_password_reset`
*since 0.3.0* 

```elixir
@spec request_password_reset(module(), String.t(), keyword()) ::
  {:ok, {String.t(), String.t()}} | {:ok, :sent} | {:error, :rate_limited}
```

Requests a password reset for the given email.

Enumeration-safe: always returns `{:ok, :sent}` for non-existent emails
with a dummy hash operation to match timing (per D-38).

## Options

- `:user_schema` - Required. The Ecto schema module for users.
- `:user_token_schema` - Required. The Ecto schema module for user tokens (e.g., `MyApp.Accounts.UserToken`).
- `:secret_key_base` - Required. The host app's secret key base.
- `:url_fun` - Required. Function `(token -> url_string)`.
- `:rate_limiter` - Module implementing `Sigra.RateLimiter`. Default: `nil`.
- `:max_requests` - Max reset requests per window. Default: `3`.
- `:window_ms` - Rate limit window in milliseconds. Default: `900_000` (15 min).

# `reset_password`
*since 0.3.0* 

```elixir
@spec reset_password(module(), String.t(), map(), keyword()) ::
  {:ok, struct()}
  | {:error, :token_expired | :token_invalid | Ecto.Changeset.t()}
```

Resets a user's password using a valid reset token.

Verifies HMAC signature, looks up token in DB, changes password,
and invalidates ALL tokens (including sessions) in a single transaction.
Per D-29: auto-login after reset (caller creates new session).

## Options

- `:secret_key_base` - Required. The host app's secret key base.
- `:user_token_schema` - Required. The Ecto schema module for user tokens.
- `:user_schema` - Required. The Ecto schema module for users.
- `:changeset_fn` - Required. Function `(user, attrs -> Ecto.Changeset.t())`.
- `:reset_ttl` - Token TTL in seconds. Default: `3600` (1 hour).

# `revoke_all_api_tokens`
*since 0.7.0* 

```elixir
@spec revoke_all_api_tokens(
  Sigra.Config.t(),
  struct()
) :: {:ok, non_neg_integer()}
```

Revokes all active API tokens for a user.

# `revoke_api_token`
*since 0.7.0* 

```elixir
@spec revoke_api_token(Sigra.Config.t(), term()) ::
  {:ok, struct()} | {:error, :not_found}
```

Revokes an API token by ID.

# `revoke_jwt_refresh`
*since 0.7.0* 

```elixir
@spec revoke_jwt_refresh(Sigra.Config.t(), String.t()) ::
  :ok | {:error, :invalid_token}
```

Revokes a JWT refresh token.

# `revoke_session`
*since 0.4.0* 

```elixir
@spec revoke_session(Sigra.Config.t(), binary(), keyword()) :: :ok
```

Revoke a specific session by hashed_token.

Delegates to `delete_session/3`.

# `schedule_deletion`
*since 0.8.0* 

```elixir
@spec schedule_deletion(Sigra.Config.t(), struct(), keyword()) ::
  {:ok, struct(), DateTime.t()} | {:error, term()}
```

Schedule account deletion with grace period.

Immediately deactivates the account (revokes sessions/tokens) and
schedules final deletion after the configured grace period.

# `set_password`
*since 0.8.0* 

```elixir
@spec set_password(Sigra.Config.t(), struct(), map(), keyword()) ::
  {:ok, struct()} | {:error, term()}
```

Set password for OAuth-only user.

Allows users who registered via OAuth (no password set) to add a
password to their account. Requires sudo mode.

# `unlink_provider`
*since 0.5.0* 

```elixir
@spec unlink_provider(Sigra.Config.t() | map(), map(), atom() | String.t(), keyword()) ::
  {:ok, :unlinked} | {:error, :last_provider | :not_found}
```

Unlinks an OAuth provider from a user.

Blocks if last auth method and no password set (D-03).
Sends notification email. Returns `{:ok, :unlinked}` or `{:error, reason}`.

# `valid_email?`
*since 0.10.0* 

```elixir
@spec valid_email?(term()) :: boolean()
```

Returns true if the given string looks like a valid email address.

Uses a deliberately loose regex — the goal is to catch obvious typos
(`alice@`, `@example.com`, `not-an-email`) before hitting the database,
not to enforce RFC 5322 compliance. Full validation happens when you
actually attempt to deliver the message.

Accepts only binaries. Non-binary input (including `nil`) returns
`false` rather than raising, so the helper composes inside pipelines.

## Examples

    iex> Sigra.Auth.valid_email?("alice@example.com")
    true

    iex> Sigra.Auth.valid_email?("bob@example.co.uk")
    true

    iex> Sigra.Auth.valid_email?("not-an-email")
    false

    iex> Sigra.Auth.valid_email?("alice@")
    false

    iex> Sigra.Auth.valid_email?("@example.com")
    false

    iex> Sigra.Auth.valid_email?(nil)
    false

# `verify_confirmation_code`
*since 0.3.0* 

```elixir
@spec verify_confirmation_code(module(), String.t(), keyword()) ::
  {:ok, struct()} | {:error, :invalid_code | :rate_limited | :already_confirmed}
```

Confirms a user by 6-digit code.

SHA-256 hashes the submitted code, looks up in DB with context "confirm_code".
Rate-limited to 5 attempts per user per 15 minutes.

## Options

- `:user_id` - Required. The user ID to confirm.
- `:user_token_schema` - Required. The Ecto schema module for user tokens.
- `:user_schema` - Required. The Ecto schema module for users.
- `:secret_key_base` - Required. The host app's secret key base.
- `:rate_limiter` - Module implementing `Sigra.RateLimiter`. Default: `nil`.
- `:max_code_attempts` - Max code verification attempts per window. Default: `5`.
- `:code_window_ms` - Rate limit window in milliseconds. Default: `900_000` (15 min).

# `verify_magic_link`
*since 0.2.0* 

```elixir
@spec verify_magic_link(module(), String.t(), keyword()) ::
  {:ok, struct()} | {:error, :invalid | :expired}
```

Verifies a magic link token. Confirms user if unconfirmed.

The token is single-use: it is deleted from the database after successful
verification. If the user has not confirmed their email, `confirmed_at`
is set to the current time.

## Options

- `:user_schema` - Required. The Ecto schema module for users.
- `:user_token_schema` - Required. The Ecto schema module for user tokens.
- `:magic_link_ttl` - Token TTL in seconds. Default: `600` (10 minutes).

---

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