hotp
HOTP action 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 @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)
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" {
@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";
}
}
}
}
}