Radiator Server Documentation — v10.33.3

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
  • secret
  • 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

Use the filter pipeline with secret to extract the right portion of the combined <password><hotp> PAP field for each action:

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

    # Validate the password portion (everything except the trailing 6-digit code)
    pap {
        secret radius.request.password | substring(0, -6);
    }

    # Validate the HOTP code (last 6 characters)
    hotp {
        secret radius.request.password | substring(-6);
        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;.

secret

Expression that produces the OTP value the action will validate. Combine it with the filter pipeline (for example substring()) to derive the OTP from any context attribute. This is the recommended way to point the action at the right value.

Example, extracting the trailing 6 digits from a combined <password><hotp> PAP field:

hotp {
    secret radius.request.password | substring(-6);
    resync_window 10;
}

When secret is set, its value replaces the protocol PAP response that the action would otherwise read.

range

Slices the PAP response by character offsets before validation. Prefer secret with the filter pipeline for new configurations; range is limited to a single contiguous slice and remains supported mainly for backward compatibility.

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
# Extract the last 6 characters as the HOTP code
hotp {
    range -6 0;
}

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

  • append

  • assert

  • backend

  • challenge

  • chap

  • conditions

  • copy

  • count

  • debug

  • discard

  • EAP

  • error

  • filter

  • hotp

  • http-basic-auth

  • if

  • ignore

  • invoke

  • log

  • map

  • message

  • modify

  • mschap

  • mschapv2

  • must

  • pap

  • reason

  • reject

  • reject_errors

  • replace

  • reply

  • rewrite

  • set

  • sleep

  • sometimes

  • stop

  • totp

  • trace

  • try

  • until

  • yubikey

Related
  • totp
  • pap
  • chap
  • mschapv2