hotp

HOTP directive for HMAC-based One-Time Password authentication

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 authentication blocks. Can be combined with other authentication methods like pap for two-factor authentication.

Basic Syntax

authentication {
    backend {
        name "USERS";
        query "FIND_USER";
    }

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

Two-Factor Authentication Example

authentication {
    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;

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" {
            authentication {
                backend {
                    name "USERS";
                    query "FIND_USER";
                }
                hotp {
                    resync_window 10;
                }
            }

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

See Also

Navigation