totp
TOTP directive for Time-based One-Time Password authentication
totp
Validates Time-based One-Time Password (TOTP) codes per RFC 6238. TOTP generates time-synchronized one-time codes using HMAC-SHA1 and a shared secret. The authentication compares the submitted code against codes generated for the current time window and configurable past/future windows to account for clock drift.
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 TOTP code in password field
totp {
resync_window 1 0;
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 TOTP code (last 6 characters)
totp {
range -6 0;
resync_window 1 0;
}
}
Parameters
resync_window
Defines the timestep tolerance for clock drift. Accepts one or two parameters:
- Single parameter:
resync_window 1;creates a symmetric window checking 1 timestep before and 1 timestep after current time - Two parameters:
resync_window 2 1;checks 2 timesteps before and 1 timestep after current time
Default: 1 0 (checks 1 timestep in the past, none in the future)
With the default 30-second timestep, resync_window 1 0 accepts codes from the current 30-second window and the previous 30-second window (allowing up to 60 seconds of clock drift into the past).
Values must be non-negative integers (0-65535).
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 TOTP validation. Useful for two-factor authentication where password and TOTP 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 TOTP-related context variables:
backends {
sqlite "USERS" {
filename "users.db";
query "FIND_USER" {
statement "SELECT username, password, totp_secret, totp_digits, totp_timestep FROM users WHERE username = ?";
bindings {
aaa.identity;
}
mapping {
user.username = username;
user.password = password;
hmac-otp.secret = totp_secret; # Required
hmac-otp.digits = totp_digits; # Optional (default: 6)
hmac-otp.timestep = totp_timestep; # Optional (default: 30)
}
}
}
}
Required context variables:
hmac-otp.secret- Shared secret (hex or base32 encoded)
Optional context variables:
hmac-otp.digits- Number of digits in TOTP code (default: 6, typically 6-8)hmac-otp.timestep- Time window in seconds (default: 30)hmac-otp.timestep.origin- Unix timestamp origin (default: 0, rarely changed)
Replay Attack Protection
To prevent replay attacks, track the last used timestep:
# In backend
mapping {
hmac-otp.secret = totp_secret;
hmac-otp.timestep.last = totp_last_timestep; # Load last used timestep
}
# In policy
caches {
cache "last_timestep" {
timeout 1800s;
}
}
authentication {
# ... backend query ...
# Load last timestep from cache
if any {
cache.last_timestep[aaa.identity] != none;
} then {
modify {
hmac-otp.timestep.last = cache.last_timestep[aaa.identity][0];
}
}
totp {
resync_window 1 0;
}
# Save current timestep after successful auth
modify {
cache.last_timestep[aaa.identity] = hmac-otp.timestep.last;
}
}