Skip to main content

documentation/tutorials/recovery-codes.md

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

SPDX-License-Identifier: MIT
-->

# Recovery Codes (Phoenix Integration)

This guide explains how to add Phoenix UI for recovery code generation and
verification. Recovery codes provide a fallback authentication method when
a user's TOTP authenticator app is unavailable.

## Prerequisites

1. Recovery code strategy configured on your user resource
   (see the [Recovery Codes tutorial](https://hexdocs.pm/ash_authentication/recovery-codes.html))
2. AshAuthentication.Phoenix installed and configured
3. TOTP strategy configured (recovery codes are typically used alongside TOTP)

## Built-in Routes

AshAuthentication.Phoenix provides two router macros for recovery codes:

```elixir
# lib/my_app_web/router.ex
defmodule MyAppWeb.Router do
  use MyAppWeb, :router
  use AshAuthentication.Phoenix.Router

  pipeline :browser do
    # ...
    plug :load_from_session
    plug :set_actor, :user
  end

  scope "/", MyAppWeb do
    pipe_through :browser

    # Standard auth routes
    auth_routes AuthController, MyApp.Accounts.User, path: "/auth"

    # TOTP routes
    totp_2fa_route MyApp.Accounts.User, :totp, auth_routes_prefix: "/auth"
    totp_setup_route MyApp.Accounts.User, :totp, auth_routes_prefix: "/auth"

    # Recovery code routes
    recovery_code_verify_route MyApp.Accounts.User, :recovery_code, auth_routes_prefix: "/auth"
    recovery_code_display_route MyApp.Accounts.User, :recovery_code, auth_routes_prefix: "/auth"
  end
end
```

### recovery_code_verify_route

Generates a recovery code verification page. Supports two modes:

- **Token-based** (`/recovery-code-verify/:token`) — used after password sign-in
  as a 2FA fallback, when the user clicks "Use a recovery code instead" on the
  TOTP verify page.
- **Step-up** (`/recovery-code-verify`) — used when an already-authenticated user
  needs to re-verify their identity.

### recovery_code_display_route

Generates a page for generating and displaying recovery codes
(`/recovery-codes`). This should be placed behind authentication middleware so
only authenticated users can access it.

### Route Options

Both macros accept the same options:

| Option | Default | Description |
|--------|---------|-------------|
| `path` | `/recovery-code-verify` or `/recovery-codes` | Path to mount at |
| `live_view` | Built-in LiveView | Custom LiveView module |
| `auth_routes_prefix` | — | Prefix for auth routes (e.g. `"/auth"`) |
| `overrides` | `[Default]` | Override modules for customisation |
| `gettext_fn` | — | Translation function as `{module, function}` |
| `gettext_backend` | — | Gettext backend as `{module, domain}` |

## Cross-Linking with TOTP

When both TOTP and recovery codes are configured, the verify pages should
link to each other so users can switch between authentication methods.

Add these overrides to your auth overrides module:

```elixir
# lib/my_app_web/auth_overrides.ex
defmodule MyAppWeb.AuthOverrides do
  use AshAuthentication.Phoenix.Overrides

  # Show "Use a recovery code instead" on the TOTP verify page
  override AshAuthentication.Phoenix.Components.Totp.Verify2faForm do
    set :recovery_code_link_path, "/recovery-code-verify"
  end

  # Show "Use authenticator app instead" on the recovery code verify page
  override AshAuthentication.Phoenix.Components.RecoveryCode.VerifyForm do
    set :totp_link_path, "/totp-verify"
  end
end
```

> ### Automatic setup with Igniter {: .info}
>
> When using `mix igniter.install ash_authentication_phoenix --auth-strategy recovery_code`,
> these overrides are automatically added to your auth overrides module.

The cross-links automatically preserve the authentication token in the URL, so
users can switch between TOTP and recovery code verification without losing
their session state.

## Auth Controller Integration

When the recovery code strategy is installed via Igniter alongside TOTP, a
`success/4` clause is added to the auth controller that redirects users to the
recovery codes page after completing TOTP setup:

```elixir
# Added automatically by the igniter
def success(conn, {_, :confirm_setup}, user, token) do
  conn
  |> store_in_session(user)
  |> set_live_socket_id(token)
  |> assign(:current_user, user)
  |> redirect(to: ~p"/recovery-codes")
end
```

This ensures users are prompted to generate recovery codes immediately after
setting up their authenticator app.

## Authentication Metadata

After successful recovery code verification, metadata is attached to the user:

```elixir
# Strategies used in this session
user.__metadata__.authentication_strategies
#=> [:recovery_code]

# Timestamp of recovery code verification
user.__metadata__.recovery_code_used_at
#=> ~U[2026-04-09 02:00:00Z]
```

This metadata is persisted in the session and can be used to detect when a
recovery code was used (e.g. to prompt the user to set up a new authenticator).

## Requiring Recovery Codes

### Using the Plug

For controller-based routes:

```elixir
pipeline :require_recovery_codes do
  plug AshAuthentication.Phoenix.Plug.RequireRecoveryCodes,
    resource: MyApp.Accounts.User,
    on_missing: :redirect_to_setup,
    setup_path: "/recovery-codes"
end
```

| Option | Default | Description |
|--------|---------|-------------|
| `resource` | Required | The user resource module |
| `on_missing` | `:halt` | `:halt`, `:redirect_to_setup`, or `{:redirect, path}` |
| `setup_path` | `"/recovery-codes"` | Path to redirect for setup |
| `current_user_assign` | `:current_user` | Assign key for current user |

### Using the LiveView Hook

For LiveView routes:

```elixir
live_session :require_recovery_codes,
  on_mount: [
    {AshAuthentication.Phoenix.LiveSession, :default},
    {AshAuthentication.Phoenix.LiveSession.RequireRecoveryCodes, :require_recovery_codes}
  ] do
  live "/secure", SecureLive
end
```

## Checking Recovery Code Status

Use `RecoveryCodeHelpers` in controllers, LiveViews, and templates:

```elixir
alias AshAuthentication.Phoenix.RecoveryCodeHelpers

# Check if user has recovery codes generated
RecoveryCodeHelpers.recovery_codes_configured?(user)
#=> true

# Check if resource supports recovery codes
RecoveryCodeHelpers.recovery_code_available?(MyApp.Accounts.User)
#=> true

# Get the recovery code strategy
{:ok, strategy} = RecoveryCodeHelpers.get_recovery_code_strategy(MyApp.Accounts.User)
```

## Next Steps

- [TOTP as Second Factor](totp-2fa.md) — setting up TOTP 2FA
- [Recovery Code Security](https://hexdocs.pm/ash_authentication/recovery-code-security.html) — understanding hashing trade-offs and entropy
- [UI Overrides](ui-overrides.md) — customising the recovery code UI