<!--
SPDX-FileCopyrightText: 2026 Alembic Pty Ltd
SPDX-License-Identifier: MIT
-->
# Recovery Codes
Recovery codes are one-time backup codes that allow users to authenticate when
their primary two-factor method (e.g. TOTP authenticator app) is unavailable.
Each code can only be used once and is deleted after successful verification.
## Prerequisites
- AshAuthentication configured with a User resource
- A primary authentication strategy (e.g. password)
- Typically paired with TOTP for a complete 2FA setup
## Installation
<!-- tabs-open -->
### Using Igniter (recommended)
```sh
mix ash_authentication.add_strategy recovery_code
```
This creates a recovery code resource, adds the relationship and strategy to
your user resource, and generates a brute force preparation module.
### Manual Setup
Follow the steps below to set up recovery codes manually.
<!-- tabs-close -->
## Recovery Code Resource
Create a resource to store hashed recovery codes:
```elixir
# lib/my_app/accounts/recovery_code.ex
defmodule MyApp.Accounts.RecoveryCode do
use Ash.Resource,
data_layer: AshPostgres.DataLayer,
domain: MyApp.Accounts
attributes do
uuid_primary_key :id
attribute :code, :string do
allow_nil? false
sensitive? true
public? false
end
timestamps()
end
relationships do
belongs_to :user, MyApp.Accounts.User, allow_nil?: false
end
actions do
defaults [:read, :destroy]
create :create do
primary? true
accept [:code]
end
end
postgres do
table "recovery_codes"
repo MyApp.Repo
references do
reference :user, on_delete: :delete
end
end
end
```
## Add Strategy to User Resource
Add a `has_many` relationship and the recovery code strategy:
```elixir
# lib/my_app/accounts/user.ex
defmodule MyApp.Accounts.User do
# ...
relationships do
has_many :recovery_codes, MyApp.Accounts.RecoveryCode
end
authentication do
strategies do
recovery_code do
recovery_code_resource MyApp.Accounts.RecoveryCode
brute_force_strategy {:audit_log, :audit_log}
end
end
end
end
```
With the default configuration, recovery codes are 12 characters from an
uppercase alphanumeric alphabet (A-Z, 0-9), hashed with SHA-256.
## Brute Force Protection
Recovery codes require a brute force protection strategy. The options are the
same as for TOTP:
**1. Audit Log (recommended)**
```elixir
brute_force_strategy {:audit_log, :audit_log}
```
Tracks failed verification attempts in the audit log and blocks requests that
exceed the configured failure threshold within a time window. This is the
default when using the Igniter installer, and requires an audit log add-on
(see the [Audit Log tutorial](/documentation/tutorials/audit-log.md)).
The window and threshold are configurable:
```elixir
recovery_code do
recovery_code_resource MyApp.Accounts.RecoveryCode
brute_force_strategy {:audit_log, :audit_log}
audit_log_window {5, :minutes}
audit_log_max_failures 5
end
```
**2. Rate Limiting (with AshRateLimiter)**
```elixir
brute_force_strategy :rate_limit
```
Requires the `AshRateLimiter` extension and rate limit configuration for the
verify action.
**3. Custom Preparation**
```elixir
brute_force_strategy {:preparation, MyApp.CustomBruteForcePreparation}
```
Create a preparation that implements your own protection logic. The preparation
must implement `supports/1` returning a list that includes `Ash.ActionInput`.
## Generated Actions
The strategy generates two actions on the user resource:
- **`verify_with_recovery_code`** — verifies a recovery code for a user. On
success, deletes the used code and returns the user. On failure, returns nil.
- **`generate_recovery_code_codes`** — generates new recovery codes for a user.
Deletes any existing codes and returns the plaintext codes in
`user.__metadata__.recovery_codes`.
## Generating Codes
```elixir
strategy = AshAuthentication.Info.strategy!(MyApp.Accounts.User, :recovery_code)
{:ok, user} = AshAuthentication.Strategy.action(strategy, :generate, %{user: user}, [])
# The plaintext codes are in metadata (only available at generation time)
codes = user.__metadata__.recovery_codes
#=> ["AB3KMN7QR2XY", "CD5FGH8JT4WZ", ...]
```
Display these codes to the user and instruct them to save them securely. The
plaintext codes are only available at generation time — only hashed values are
stored in the database.
## Verifying Codes
```elixir
strategy = AshAuthentication.Info.strategy!(MyApp.Accounts.User, :recovery_code)
case AshAuthentication.Strategy.action(strategy, :verify, %{user: user, code: "AB3KMN7QR2XY"}, []) do
{:ok, user} -> # Code valid, user authenticated
{:error, _} -> # Code invalid or already used
end
```
## Configuration Options
| Option | Default | Description |
|--------|---------|-------------|
| `recovery_code_resource` | — | The Ash resource that stores recovery codes. Required. |
| `hash_provider` | `SHA256Provider` | Hash provider for hashing codes. |
| `code_length` | `12` | Length of each generated code. |
| `code_alphabet` | `A-Z, 0-9` | Characters used when generating codes. |
| `recovery_code_count` | `10` | Number of codes to generate. |
| `code_field` | `:code` | Attribute on the recovery code resource that stores the hash. |
| `recovery_codes_relationship_name` | `:recovery_codes` | Name of the `has_many` relationship on the user. |
| `user_relationship_name` | `:user` | Name of the `belongs_to` relationship on the code resource. |
| `generate_enabled?` | `true` | Whether to generate the generate action. |
| `verify_action_name` | `:verify_with_<name>` | Name of the verify action. |
| `generate_action_name` | `:generate_<name>_codes` | Name of the generate action. |
## Using a Different Hash Provider
The default `AshAuthentication.SHA256Provider` requires codes with at least 60
bits of entropy. With the default 12-character alphabet of 36 characters, this
gives ~62 bits — comfortably above the minimum.
For shorter, more user-friendly codes, use a slow hash provider:
```elixir
recovery_code do
recovery_code_resource MyApp.Accounts.RecoveryCode
hash_provider AshAuthentication.BcryptProvider
code_length 8
brute_force_strategy {:audit_log, :audit_log}
end
```
> ### Slow hashes have performance implications {: .info}
>
> Bcrypt and Argon2 are deliberately slow. Verifying a code requires checking
> against each stored hash individually, which may take up to ~1 second with 10
> codes. SHA-256 verification is near-instant because it uses atomic database
> lookups.
See [Recovery Code Security](../topics/recovery-code-security.md) for a detailed
explanation of the trade-offs.
## Security Considerations
1. **Brute force protection is mandatory** — every configuration must specify a strategy
2. **Codes are hashed at rest** — plaintext codes are only available at generation time
3. **Codes are single-use** — each code is deleted after successful verification
4. **Store codes securely** — instruct users to save codes in a password manager or printed copy
5. **Regenerating codes invalidates old ones** — generating new codes deletes all existing codes
6. **Pair with TOTP** — recovery codes are most useful as a backup for TOTP authentication