TOTP (Time-based One-Time Password) Tutorial

View Source

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:

# 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):

# 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)

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
end

2. Rate Limiting (with AshRateLimiter)

brute_force_strategy :rate_limit

Requires 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.

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
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

# 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:

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:

# 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

OptionDefaultDescription
identity_field:usernameField that identifies users (e.g., :email)
secret_field:totp_secretAttribute storing the TOTP secret
last_totp_at_field:last_totp_atAttribute tracking last successful auth
issuerStrategy nameDisplayed in authenticator apps
period30Code validity period in seconds (recommended: 15-300)
secret_length20Secret length in bytes (recommended: 16+, per RFC 4226)
setup_enabled?trueGenerate setup action
sign_in_enabled?falseGenerate sign-in action
verify_enabled?trueGenerate verify action
confirm_setup_enabled?falseUse 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