TOTP (Time-based One-Time Password) Tutorial
View SourceTOTP 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:
- Two-Factor Authentication (2FA) - As a second factor after password authentication
- 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:
# 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
endThe 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):
# 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
endThis generates:
setup_with_totpaction - generates a secret and stores it on the userverify_with_totpaction - verifies a code without signing intotp_url_for_totpcalculation - generates theotpauth://URL for QR codes
Brute Force Protection
TOTP requires a brute force protection strategy. Options:
1. Custom Preparation (simplest)
brute_force_strategy {:preparation, MyApp.TotpBruteForcePreparation}Create a preparation that implements your protection logic:
# 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
end2. Rate Limiting (with AshRateLimiter)
brute_force_strategy :rate_limitRequires the AshRateLimiter extension and rate limit configuration for TOTP actions.
3. Audit Log
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:
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
endThis changes the flow:
- Setup -
setup_with_totpreturns asetup_tokenandtotp_urlin metadata (secret is NOT stored on user yet) - Display QR Code - Show the QR code to the user
- Confirm - User enters a code, call
confirm_setup_with_totpwith the token and code - Activation - If code is valid, secret is stored on user
Example Setup Flow
# 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 enabledStandalone TOTP Sign-In
To use TOTP as a primary authentication method:
authentication do
strategies do
totp do
identity_field :email
sign_in_enabled? true
brute_force_strategy {:preparation, MyApp.TotpBruteForcePreparation}
end
end
endThis 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:
# 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:
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:
# 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
- Always use brute force protection - TOTP codes are only 6 digits
- Use confirm_setup_enabled? - Ensures users correctly saved their secret
- Store secrets securely - Mark the secret field as
sensitive?: true - Track last_totp_at - Prevents replay attacks within the same time window
- Provide backup codes - Consider implementing backup codes for account recovery