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)
exclusivekeyword 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";
}
}
}
}
}