Radiator Server Documentation — v10.33.3

totp

TOTP action for Time-based One-Time Password authentication

Table of Contents
  • totp
  • Context
  • Basic Syntax
  • Two-Factor Authentication Example
  • Parameters
  • resync_window
  • secret_type
  • min_secret_bits
  • secret
  • range
  • Result
  • Backend Mapping
  • Replay Attack Protection
  • See Also

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 @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 TOTP code in password field
    totp {
        resync_window 1 0;
        secret_type "hex";
    }
}

Two-Factor Authentication Example

Use the filter pipeline with secret to extract the right portion of the combined <password><totp> 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 TOTP code (last 6 characters)
    totp {
        secret radius.request.password | substring(-6);
        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;.

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><totp> PAP field:

totp {
    secret radius.request.password | substring(-6);
    resync_window 1 0;
}

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 TOTP code
totp {
    range -6 0;
}

Result

The totp action produces the following pipeline results:

  • Accept: The submitted TOTP code matches a valid code within the configured time 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 time window.
    • The OTP code was already used (replay protection, when hmac-otp.timestep.last is set).

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;
    }
}

@execute {
    # ... 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;
    }
}

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
  • hotp
  • pap
  • chap
  • mschapv2