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

Signed token generation and verification.

Sigra uses two token strategies:

1. **Signed tokens** -- for session cookies and transport. Created via
   `Plug.Crypto.sign/4` using the host app's `secret_key_base` with
   per-purpose salts (`"sigra-session-token"`, `"sigra-email-token"`, etc.).

2. **Hashed tokens** -- for email confirmation, password reset, and API
   keys. A random token is generated, the SHA-256 hash is stored in the
   database, and the raw token is sent to the user. Verification compares
   the hash of the submitted token against the stored hash.

All token comparisons use constant-time comparison via
`Plug.Crypto.secure_compare/2` to prevent timing attacks.

# `generate`
*since 0.1.0* 

```elixir
@spec generate(String.t(), String.t(), term(), keyword()) :: binary()
```

Generates a signed token for the given purpose and data.

Uses `Plug.Crypto.sign/4` with a purpose-specific salt derived from
the host app's `secret_key_base`.

## Parameters

- `secret_key_base` - The host app's secret key base (from `endpoint.config`)
- `purpose` - A string identifying the token's purpose (e.g., `"sigra-session-token"`)
- `data` - The data to embed in the token (typically a user ID)
- `opts` - Options passed to `Plug.Crypto.sign/4` (e.g., `max_age:`, `key_iterations:`)

## Examples

    iex> token = Sigra.Token.generate(secret, "sigra-session-token", user_id)
    iex> is_binary(token)
    true

# `generate_hashed_token`
*since 0.1.0* 

```elixir
@spec generate_hashed_token() :: {String.t(), binary()}
```

Generates a random token and its SHA-256 hash for database storage.

Returns `{raw_token, hashed_token}` where:

- `raw_token` is a URL-safe base64-encoded string (sent to the user)
- `hashed_token` is a 32-byte SHA-256 binary (stored in the database)

## Examples

    iex> {raw, hashed} = Sigra.Token.generate_hashed_token()
    iex> is_binary(raw) and byte_size(hashed) == 32
    true

# `generate_invite_envelope`
*since 0.4.0* 

```elixir
@spec generate_invite_envelope(String.t(), String.t()) :: {String.t(), binary()}
```

Generates a signed invitation envelope binding email into the HMAC payload.

Returns `{encoded_signed_token, hashed_token_for_storage}`.

## Why this diverges from `sigra-confirm-token`

Confirmation tokens sign the raw token only — the holder of the link IS
the user being confirmed, so identity is bound by convention at DB
compare time. Invitations are the exception: the holder of the link is
NOT yet the authenticated principal, so identity must be bound
cryptographically. This closes the Jetstream #907 / Keycloak
CVE-2026-1529 class of invite-hijack bugs by construction.

Payload shape uses STRING keys (`"t"`, `"e"`) to avoid atom-table growth
on decode.

# `hash_token`
*since 0.1.0* 

```elixir
@spec hash_token(binary()) :: binary()
```

Hashes a raw token with SHA-256 for storage comparison.

## Examples

    iex> hashed = Sigra.Token.hash_token("some-raw-token")
    iex> byte_size(hashed) == 32
    true

# `secure_compare`
*since 0.1.0* 

```elixir
@spec secure_compare(binary(), binary()) :: boolean()
```

Performs a constant-time comparison of two strings.

Delegates to `Plug.Crypto.secure_compare/2` to prevent timing attacks.

## Examples

    iex> Sigra.Token.secure_compare("abc", "abc")
    true

    iex> Sigra.Token.secure_compare("abc", "def")
    false

# `verify`
*since 0.1.0* 

```elixir
@spec verify(String.t(), String.t(), binary(), keyword()) ::
  {:ok, term()} | {:error, :invalid | :expired}
```

Verifies a signed token and extracts the embedded data.

Returns `{:ok, data}` if the token is valid and not expired, or
`{:error, :invalid}` / `{:error, :expired}` on failure.

## Parameters

- `secret_key_base` - The host app's secret key base
- `purpose` - The purpose string used when generating the token
- `token` - The token to verify
- `opts` - Options passed to `Plug.Crypto.verify/4` (e.g., `max_age:`)

## Examples

    iex> {:ok, user_id} = Sigra.Token.verify(secret, "sigra-session-token", token, max_age: 86400)

# `verify_invite_envelope`
*since 0.4.0* 

```elixir
@spec verify_invite_envelope(String.t(), String.t(), pos_integer()) ::
  {:ok, %{raw_token: binary(), bound_email: String.t(), hashed_token: binary()}}
  | {:error, :invalid | :expired}
```

Verifies an invitation envelope and returns the raw token, bound email,
and hashed-token-for-DB-lookup.

Fails `{:error, :invalid}` if HMAC verify fails, base64 decode fails, or
the payload shape is wrong. Fails `{:error, :expired}` if the envelope is
older than `max_age_seconds`.

Distinguishing `:invalid` from other errors MUST NOT leak information to
the attacker — callers should treat both `:invalid` and `:expired` as
"invitation link is not valid" with the same user-facing copy.

---

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