Radiator Server Documentation — v10.33.2

hotp

HOTP action for HMAC-based One-Time Password authentication

Table of Contents
  • hotp
  • Context
  • Basic Syntax
  • Two-Factor Authentication Example
  • Parameters
  • resync_window
  • secret_type
  • min_secret_bits
  • range
  • Result
  • Backend Mapping
  • Counter Persistence
  • See Also

hotp

Validates HMAC-based One-Time Password (HOTP) codes per RFC 4226. HOTP generates counter-synchronized one-time codes using HMAC-SHA1 and a shared secret. The counter increments with each authentication attempt, making each code usable only once.

Context

Valid inside @execute blocks. Can be combined with other authentication methods like pap for two-factor authentication.

Basic Syntax

@execute {
    backend {
        name "USERS";
        query "FIND_USER";
    }

    # Validate HOTP code in password field
    hotp {
        resync_window 10;
        secret_type "hex";
    }
}

Two-Factor Authentication Example

@execute {
    backend {
        name "USERS";
        query "FIND_USER";
    }

    # Validate password (all but last 6 characters)
    pap {
        range -6 0 exclusive;
    }

    # Validate HOTP code (last 6 characters)
    hotp {
        range -6 0;
        resync_window 10;
    }
}

Parameters

resync_window

Defines how many counter values ahead of the current counter to check. This accounts for missed authentication attempts or token button presses.

Syntax: resync_window <forward_count>;

  • Single parameter only (HOTP only checks forward, never backward)
  • Default: 10
  • Valid range: 0-65535

Example: resync_window 10; checks the current counter and the next 10 counter values.

A larger window increases tolerance for missed authentications but reduces security (more codes are valid simultaneously). A value of 10-20 is typical for hardware tokens.

secret_type

Specifies the encoding format of the shared secret retrieved from the backend:

  • "hex" - Hexadecimal encoding (default)
  • "base32" - Base32 encoding (RFC 4648)
  • "auto" - Automatically detects hex or base32

Default: "hex"

The secret must be set via hmac-otp.secret in the backend mapping.

min_secret_bits

Enforces a minimum secret length in bits. If the decoded secret is shorter than this value, authentication is rejected with a warning logged and counted.

Syntax: min_secret_bits <bits>;

  • Default: 128 (as required by RFC 4226)
  • Valid range: 0-65535
  • Common values: 128 (16 bytes), 160 (20 bytes, recommended by RFC 4226)

Example: min_secret_bits 160; requires at least a 20-byte (160-bit) secret.

RFC 4226 states: "The length of the shared secret MUST be at least 128 bits. This document RECOMMENDs a shared secret length of 160 bits."

To disable the minimum check (not recommended), use min_secret_bits 0;.

range

Extracts a substring from the password field for HOTP validation. Useful for two-factor authentication where password and HOTP code are concatenated.

Syntax: range <start> <end> [exclusive]

  • Negative indices count from the end (-6 means 6 characters from the end)
  • exclusive keyword excludes the specified range (extracts everything except the range)
  • Indices are 0-based

Examples:

# Extract last 6 characters
range -6 0;

# Extract first 6 characters
range 0 6;

# Extract ALL BUT last 6 characters (for password in 2FA)
range -6 0 exclusive;

Result

The hotp action produces the following pipeline results:

  • Accept: The submitted HOTP code matches a valid code within the configured counter window.
  • Reject: Authentication failed. This occurs when:
    • The shared secret is shorter than min_secret_bits requires.
    • No authentication response is present in the request.
    • The authentication response is too short for the configured range.
    • The OTP digit count does not match the expected number of digits.
    • The OTP contains non-numeric characters.
    • The OTP code does not match any valid code in the counter window.

Backend Mapping

The backend must populate HOTP-related context variables:

backends {
    sqlite "USERS" {
        filename "users.db";
        query "FIND_USER" {
            statement "SELECT username, password, hotp_secret, hotp_counter, hotp_digits FROM users WHERE username = ?";
            bindings {
                aaa.identity;
            }
            mapping {
                user.username = username;
                user.password = password;
                hmac-otp.secret = hotp_secret;    # Required
                hmac-otp.counter = hotp_counter;  # Required
                hmac-otp.digits = hotp_digits;    # Optional (default: 6)
            }
        }
    }
}

Required context variables:

  • hmac-otp.secret - Shared secret (hex or base32 encoded)
  • hmac-otp.counter - Current counter value

Optional context variables:

  • hmac-otp.digits - Number of digits in HOTP code (default: 6, typically 6-8)

Counter Persistence

Critical: After successful HOTP authentication, you must persist the updated counter to prevent replay attacks and allow continued authentication:

backends {
    sqlite "USERS" {
        # ... FIND_USER query ...

        query "UPDATE_HOTP_COUNTER" {
            statement "UPDATE users SET hotp_counter = ? WHERE username = ?";
            bindings {
                hmac-otp.counter;  # Updated counter after successful auth
                user.username;
            }
        }
    }
}

aaa {
    policy "DEFAULT" {
        handler "AUTHENTICATION" {
            @execute {
                backend {
                    name "USERS";
                    query "FIND_USER";
                }
                hotp {
                    resync_window 10;
                }
            }

            @final-execute {
                # Save updated counter after successful authentication
                backend {
                    name "USERS";
                    query "UPDATE_HOTP_COUNTER";
                }
            }
        }
    }
}

See Also

Navigation
  • accept

  • all

  • any

  • append

  • assert

  • backend

  • challenge

  • chap

  • conditions

  • copy

  • count

  • debug

  • discard

  • each

  • eap

  • error

  • filter

  • first

  • hotp

  • http-basic-auth

  • if

  • ignore

  • invoke

  • log

  • map

  • message

  • modify

  • mschap

  • mschapv2

  • none

  • pap

  • reason

  • reject

  • reject_errors

  • replace

  • reply

  • rewrite

  • set

  • sleep

  • sometimes

  • stop

  • totp

  • trace

  • try

  • until

  • while

  • with

  • yubikey

Related
  • totp
  • pap
  • chap
  • mschapv2