documentation/tutorials/totp.md

<!--
SPDX-FileCopyrightText: 2025 Alembic Pty Ltd

SPDX-License-Identifier: MIT
-->

# TOTP (Time-based One-Time Password) Tutorial

TOTP allows users to authenticate using time-based one-time passwords generated
by authenticator apps like Google Authenticator, Authy, or 1Password.

## Use Cases

TOTP can be used in two ways:

1. **Two-Factor Authentication (2FA)** - As a second factor after password authentication
2. **Standalone Authentication** - As the primary authentication method (passwordless)

This tutorial covers both approaches.

## Prerequisites

- AshAuthentication configured with a User resource
- A token resource if using `confirm_setup_enabled?` (recommended)

## Add Required Attributes

Add the following attributes to your User resource:

```elixir
# lib/my_app/accounts/user.ex
attributes do
  # ... existing attributes ...

  attribute :totp_secret, :binary do
    allow_nil? true
    sensitive? true
    public? false
  end

  attribute :last_totp_at, :utc_datetime do
    allow_nil? true
    public? false
  end
end
```

The `totp_secret` stores the shared secret, and `last_totp_at` prevents replay
attacks by tracking the last successful authentication time.

## Basic TOTP Setup (2FA Mode)

For 2FA, users set up TOTP after registering with another method (like password):

```elixir
# lib/my_app/accounts/user.ex
authentication do
  strategies do
    password :password do
      identity_field :email
    end

    totp do
      identity_field :email
      # Required: choose a brute force protection strategy
      brute_force_strategy {:preparation, MyApp.TotpBruteForcePreparation}
    end
  end
end
```

This generates:
- `setup_with_totp` action - generates a secret and stores it on the user
- `verify_with_totp` action - verifies a code without signing in
- `totp_url_for_totp` calculation - generates the `otpauth://` URL for QR codes

### Brute Force Protection

TOTP requires a brute force protection strategy. Options:

**1. Custom Preparation (simplest)**
```elixir
brute_force_strategy {:preparation, MyApp.TotpBruteForcePreparation}
```

Create a preparation that implements your protection logic:

```elixir
# lib/my_app/accounts/totp_brute_force_preparation.ex
defmodule MyApp.TotpBruteForcePreparation do
  use Ash.Resource.Preparation

  def prepare(query, _opts, _context) do
    # Implement rate limiting, account lockout, etc.
    # Return the query unchanged if allowed to proceed
    query
  end
end
```

**2. Rate Limiting (with AshRateLimiter)**
```elixir
brute_force_strategy :rate_limit
```

Requires the `AshRateLimiter` extension and rate limit configuration for TOTP actions.

**3. Audit Log**
```elixir
brute_force_strategy {:audit_log, :my_audit_log}
```

Requires an audit log add-on that logs TOTP actions.

## Two-Step Setup with Confirmation (Recommended)

For better security, use two-step setup. This ensures users have correctly saved
their secret before it's activated:

```elixir
authentication do
  tokens do
    enabled? true
    token_resource MyApp.Accounts.Token
  end

  strategies do
    totp do
      identity_field :email
      confirm_setup_enabled? true
      setup_token_lifetime {10, :minutes}
      brute_force_strategy {:preparation, MyApp.TotpBruteForcePreparation}
    end
  end
end
```

This changes the flow:

1. **Setup** - `setup_with_totp` returns a `setup_token` and `totp_url` in metadata
   (secret is NOT stored on user yet)
2. **Display QR Code** - Show the QR code to the user
3. **Confirm** - User enters a code, call `confirm_setup_with_totp` with the token and code
4. **Activation** - If code is valid, secret is stored on user

### Example Setup Flow

```elixir
# Step 1: Initiate setup
{:ok, user} = Ash.update(user, action: :setup_with_totp)
setup_token = user.__metadata__.setup_token
totp_url = user.__metadata__.totp_url

# Step 2: Display QR code (use totp_url with a QR code library)
# The URL format is: otpauth://totp/Issuer:user@example.com?secret=BASE32SECRET&issuer=Issuer

# Step 3: User scans QR code and enters the code from their app
{:ok, user} = Ash.update(user,
  action: :confirm_setup_with_totp,
  params: %{setup_token: setup_token, code: "123456"}
)

# User now has TOTP enabled
```

## Standalone TOTP Sign-In

To use TOTP as a primary authentication method:

```elixir
authentication do
  strategies do
    totp do
      identity_field :email
      sign_in_enabled? true
      brute_force_strategy {:preparation, MyApp.TotpBruteForcePreparation}
    end
  end
end
```

This generates a `sign_in_with_totp` action that takes an identity and code,
returning an authenticated user with a token.

## Verifying TOTP Codes

The `verify_with_totp` action checks if a code is valid without signing in.
This is useful for 2FA flows where you want to verify the code as a second step:

```elixir
# After password authentication, verify TOTP
strategy = AshAuthentication.Info.strategy!(MyApp.Accounts.User, :totp)
{:ok, true} = AshAuthentication.Strategy.action(strategy, :verify, %{
  user: user,
  code: "123456"
})
```

## Generating QR Codes

The `totp_url_for_totp` calculation generates the standard `otpauth://` URL:

```elixir
user = Ash.load!(user, :totp_url_for_totp)
qr_url = user.totp_url_for_totp
# => "otpauth://totp/MyApp:user@example.com?secret=JBSWY3DPEHPK3PXP&issuer=MyApp"
```

Use a QR code library to render this URL:

```elixir
# With eqrcode
qr_code = EQRCode.encode(qr_url)
svg = EQRCode.svg(qr_code)
```

## Configuration Options

| Option | Default | Description |
|--------|---------|-------------|
| `identity_field` | `:username` | Field that identifies users (e.g., `:email`) |
| `secret_field` | `:totp_secret` | Attribute storing the TOTP secret |
| `last_totp_at_field` | `:last_totp_at` | Attribute tracking last successful auth |
| `issuer` | Strategy name | Displayed in authenticator apps |
| `period` | `30` | Code validity period in seconds (recommended: 15-300) |
| `secret_length` | `20` | Secret length in bytes (recommended: 16+, per RFC 4226) |
| `setup_enabled?` | `true` | Generate setup action |
| `sign_in_enabled?` | `false` | Generate sign-in action |
| `verify_enabled?` | `true` | Generate verify action |
| `confirm_setup_enabled?` | `false` | Use two-step setup flow (requires `setup_enabled?`) |
| `setup_token_lifetime` | `{10, :minutes}` | How long setup tokens are valid |

## Security Considerations

1. **Always use brute force protection** - TOTP codes are only 6 digits
2. **Use confirm_setup_enabled?** - Ensures users correctly saved their secret
3. **Store secrets securely** - Mark the secret field as `sensitive?: true`
4. **Track last_totp_at** - Prevents replay attacks within the same time window
5. **Provide backup codes** - Consider implementing backup codes for account recovery