Radiator Server Documentation — latest

YubiKey OTP Filter

Validate Yubico OTPs and extract their components without hand-written substring offsets

Table of Contents
  • Yubico OTP structure
  • Syntax
  • Placement
  • Accepted input value types
  • Validation and error handling
  • Examples
  • Extract public UID before a user lookup
  • Forward the full OTP to a cloud validator
  • Extract the static password from a combined PAP field
  • Related

The yubikey filter validates a Yubico OTP that arrives in a string value and returns either the full OTP or one of its components. It is intended for the common deployment patterns described in YubiKey Authentication, where the OTP is read out of a PAP password field that may also contain a static password.

Yubico OTP structure

A Yubico OTP is a fixed 44-character modhex string. The first 12 characters are the public UID — a stable identifier per token — and the trailing 32 characters are the AES-128-encrypted ciphertext that carries the private UID, counters, timestamp, and CRC.

┌──────────────────── 44-character OTP ─────────────────────┐
│ <12 chars: public UID> │ <32 chars: encrypted ciphertext> │
└────────────────────────┴──────────────────────────────────┘

In two-factor deployments the OTP is concatenated with a static password in the same PAP field. The OTP is always 44 characters; the password is whatever sits next to it:

password-otp:  <password>vvccccriigjnXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
otp-password:  vvccccriigjnXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX<password>

Each filter <extract> value selects one slice of this layout:

ExtractSlice returnedLength
otpThe full validated OTP.44 chars
public-uidLeading 12 modhex characters of the OTP.12 chars
ciphertextTrailing 32 modhex characters of the OTP.32 chars
passwordThe static password sitting next to the OTP.Whatever is left

public-uid is the most commonly used component. It is a stable per-token identifier, so it is the natural value to bind a SQL or LDAP user lookup to when the same user can present more than one YubiKey.

password returns whatever sits next to the OTP in the input value. It is only valid with placement = password-otp or placement = otp-password; combining it with placement = otp is rejected at configuration parse time because there is no password slice to return.

Syntax

The filter is positional and always takes two arguments:

<input> | yubikey(<placement>, <extract>)
  • <placement> describes where the OTP sits inside the input value.
  • <extract> selects which slice of the structure above to return.

Placement

The placement names describe the layout of the input value.

PlacementInput layoutNotes
otp<otp>The whole input value is the 44-character OTP.
password-otp<password><otp>The OTP is the trailing 44 characters; the leading text is the password.
otp-password<otp><password>The OTP is the leading 44 characters; the trailing text is the password.

For unusual layouts that none of these shorthands describe, preprocess the input with substring or some other filter to isolate the 44-character OTP, then use placement = otp.

Accepted input value types

The filter accepts plain strings, byte strings, and masked passwords. Masked passwords are not revealed in logs or stored output; the filter only inspects the wrapped bytes long enough to validate the OTP and return the requested component.

Validation and error handling

Before extracting a component, the filter checks that the candidate OTP slice:

  • is exactly 44 characters long, and
  • contains only ASCII characters.

If either check fails, or if the slice is not a well-formed Yubico OTP, the filter produces a value error. The OTP slice is always validated first, so a malformed OTP cannot smuggle arbitrary bytes through the password extract.

The filter only performs these cheap structural checks; it does not decrypt the OTP or verify replay counters. Cryptographic validation still happens in the yubikey action (offline) or in the yubikey HTTP backend (cloud). The filter's job is to extract the right substring so those validators receive clean input and so user lookups can key on the public UID.

Wrap the call in recover when the policy needs to treat malformed input as an authentication reject rather than a backend execution error:

modify vars.presented_yubikey_public_uid =
    radius.request.password | yubikey(password-otp, public-uid) | recover(none);

if all {
    vars.presented_yubikey_public_uid == none;
} then {
    reject "Malformed YubiKey OTP";
}

Examples

Extract public UID before a user lookup

Use yubikey(password-otp, public-uid) to pull the per-token identifier out of a combined <password><otp> PAP field, then use it as a binding for the user lookup query.

@execute {
    modify vars.presented_yubikey_public_uid =
        radius.request.password | yubikey(password-otp, public-uid) | recover(none);

    if all {
        vars.presented_yubikey_public_uid == none;
    } then {
        reject "Malformed YubiKey OTP";
    }

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

Forward the full OTP to a cloud validator

When the OTP is the whole PAP value, yubikey(otp, otp) validates length and encoding before the cloud backend sees the request.

modify vars.validated_otp = radius.request.password | yubikey(otp, otp);

Extract the static password from a combined PAP field

yubikey(password-otp, password) returns the leading static password from a <password><otp> PAP field. Use it to feed the password portion into the pap action or to compare against a stored value without writing substring() offsets.

modify vars.static_password = radius.request.password | yubikey(password-otp, password);
  • Filters - All available filters and the general <value> | <filter> syntax.
  • yubikey action - Offline OTP decryption and replay-counter check.
  • YubiKey Authentication - End-to-end configuration patterns for cloud and offline validation.
Navigation
  • About Radiator software development security

  • Architecture Overview

  • Backend Load Balancing

  • Basic Installation

  • Built-in Environment Variables

  • Comparison Operators

  • Configuration Editor

  • Configuration Import and Export

  • Containers

  • Data Types

  • Duration Units

  • Environment Variables

  • Execution Context

  • Execution Pipelines

  • Filters

    • YubiKey OTP Filter

  • Getting a Radiator License

  • Health check /live and /ready

  • High Availability and Load Balancing

  • High availability identifiers

  • HTTP Basic Authentication

  • Introduction

  • Linux systemd support

  • Local AAA Backends

  • Log storage and formatting

  • Management API privilege levels

  • Namespaces

  • Password Hashing

  • Probabilistic Sampling

  • Prometheus scraping

  • PROXY Protocol Support

  • Radiator server health and boot up logic

  • Radiator sizing

  • Radiator software releases

  • Rate Limiting

  • Rate Limiting Algorithms

  • Reverse Dynamic Authorization

  • Service Level Objective

  • TACACS+ Authentication, Authorization, and Accounting

  • Template Rendering CLI

  • Tools radiator-client

  • TOTP/HOTP Authentication

  • What is Radiator?

  • YubiKey Authentication

  • YubiKey Context Variables