SurePassID Offline OTP and Offline-Code Cache Handling

SurePassID Authentication Server

Offline OTP and Offline-Code Cache Handling

This document describes how the SurePassId Legacy MFA Server generates, returns, and "refreshes" (updates the cache of) offline OTP codes, and how those codes relate to event/HOTP token counters.

Audience: engineers and QA validating the offline authentication path. Scope: the ValidateOtp / CreateServerChallenge authentication flow and the GenerateOffLineCode* routines that produce the code cache returned to clients.


1. What "offline OTP" means here

An offline client (for example a Windows/macOS/Linux Login agent, or an RDP/console agent) cannot always reach the server at the moment a user authenticates. To keep working while disconnected, the client keeps a local cache of pre-computed OTP codes for the user's token(s).

The server produces these codes by pre-generating the next N event-based (HOTP) codes starting from the token's current counter, and returns them to the client on the next successful online authentication. The client stores them and validates against them locally until it can talk to the server again.

Relevant token type: OTPTypesEnum.OffLine = 22 (see Submodules/.../Enums/OTPTypesEnum.cs). Offline pre-generation is also performed for the other event/counter-based token types (Event, Event256, Event512, DynamicCvcEvent) because they share the HOTP counter model.

Helper: OTPTypeExtensions.IsTokenOfflineBased(otpType) returns true only for OTPTypesEnum.OffLine.


2. Endpoints involved

Endpoint File Purpose How offline count is passed
JSON API action validate_otp server/ApiActions/ValidateOtp.cs Primary OTP validation used by modern clients. Returns freshly generated offline codes on success. json["ocodes"] (count of codes to pre-generate)
JSON API action create_server_challenge server/ApiActions/CreateServerChallenge.cs Server-challenge / OCRA style flow; also returns offline codes on success. (via challenge request)
Legacy HTML endpoint AuthServer/ValidateOTP.aspx AuthServer/ValidateOTP.aspx.cs Legacy GET endpoint returning a plain-text <textarea> block. OCODES query-string parameter
Legacy pre-sign endpoint AuthServer/PreSign.aspx AuthServer/PreSign.aspx.cs FIDO/U2F "pre-sign" for offline; when OFF=1 also returns the app's ECC-256 public cert so the client can verify signatures offline. OFF=1 query-string flag

All of these ultimately call AuthServerApi.ValidateOtpOfflineCodes(...).

Legacy ValidateOTP.aspx query parameters

  • PLN / PLP – partner (API) login name / password (authorizes the API caller).
  • PUL – partner user login (or PS printed serial number; one is required).
  • O – the OTP to validate.
  • OCODES – number of offline codes to pre-generate and return (short, default 0).

Success response (legacy endpoint)

0000
OK
<OcraServerCode>
<OfflineCodes>        <- comma-separated list of pre-generated codes

Success response (JSON action)

ValidateOathOtpResponse (see server/Models/ValidateOathOtpResponse.cs) with an offlineCodes property that is omitted when empty (DefaultValueHandling.Ignore).


3. End-to-end flow

Client (online auth) --> validate_otp / ValidateOTP.aspx (ocodes = N)
        |
        v
AuthServerApi.ValidateOtpOfflineCodes(conn, partner, userLogin, otp, psn, N)
        |
        v
AuthServerApi(Otp).ValidateOtpInternal(...)   // core validation
        |  (on success, for the matching event/offline token)
        |     1. advance token counter (NextCounter++)
        |     2. persist token (UpdateDevice)
        |     3. GenerateOffLineCodeDevice(...) -> fill OfflineCodes
        v
Response includes OfflineCodes (the refreshed client cache)

Core method: ValidateOtpInternal in Submodules/SurePassIdDotNetLibs/SurePassClassLib/AuthServerAPI/AuthServerApiOtp.cs.

Key steps performed there:

  1. Load partner settings and configure KeyManagement (encryption/HSM provider) from PartnerSettings.EncyptionType.
  2. Resolve and validate the partner user (enabled check, bypass-MFA check, printed serial number check).
  3. For each of the user's devices, attempt validation:
    • Send-OTP / push path (VerifySendOTP) and temporary OTP path (VerifyTemporaryOTP) — on success these also call GenerateOffLineCodes(...).
    • Event/HOTP path — walk the OTP window from NextCounter looking for a match.

4. Counter advance = the "server side" of the cache

For event/HOTP/offline tokens, when a submitted OTP matches within the window:

device.LastOTPValidation = now;
eventCounter++;                       // consume the matched code
device.NextCounter = eventCounter.ToString();
device.FailedOTPRequests = 0;
interfaceDevice.UpdateDevice(conn, device);   // persist advanced counter

GenerateOffLineCodeDevice(keyManagement, device, numberOfflineCodes);  // rebuild cache

// Optionally mirror the counter to virtual tokens:
if (Server.UpdateVirtualTokens)
    interfaceDevice.UpdateVirtualTokenCounterByPrintedSerialNumber(conn, device);

Important consequences:

  • The offline codes are always generated from the newly advanced NextCounter, so the returned cache never contains the code that was just consumed.
  • The persisted NextCounter is what keeps the server and the client's offline cache in sync. If the client validates codes offline, it advances its own local counter; when it reconnects, the server advances and hands back a fresh window.
  • Server.UpdateVirtualTokens (config, default true) keeps any linked virtual token copy's counter aligned.
  • Optional audit row in PartnerUserOTP is written when Server.LogEventOtpUsageInOtpTable (default true) is enabled.

5. Offline-code generation details

Method: GenerateOffLineCodeDevice(KeyManagement, TableDevice, int numberOfflineCodes) in AuthServerApiOtp.cs.

Rules:

  • Returns false (no codes) if the device is null, not Enabled, or not an event/offline-based type (Event, Event256, Event512, OffLine, DynamicCvcEvent).
  • numberOfflineCodes == 0 -> returns true but generates no codes (feature off for this call).
  • numberOfflineCodes == -1 -> uses the token's OTPWindowSize as the count.
  • Otherwise generates exactly numberOfflineCodes codes.

Generation loop:

var secretKey    = device.SecretKey.ToByte();
var eventCounter = device.NextCounter.ToInt64();   // starts at the advanced counter
var hmacSha      = GetHmacShaFromOtpType(device.OTPType);

for (var cc = 0; cc < numberOfflineCodes; cc++, eventCounter++)
{
    if (cc != 0) cb.Append(",");
    cb.Append(oathAuthenticationApi.Hotp(keyManagement, secretKey,
                                         eventCounter, false, device.OTPLength, hmacSha));
}
OfflineCodes = cb.ToString();   // e.g. "483920,118273,900142,..."
  • Codes are standard HOTP values (RFC 4226) computed with the token's OTPLength and the hash implied by the OTP type.
  • The result is a comma-separated string exposed on AuthServerApi.OfflineCodes.

GenerateOffLineCodes(KeyManagement, TableDevice[], int) is the multi-device wrapper: it iterates the user's devices and stops at the first device for which generation succeeds (one offline token per user for this purpose).


6. Configuration switches

Setting Default Effect
Server.LogEventOtpUsageInOtpTable true Insert a PartnerUserOTP row recording each consumed event OTP.
Server.UpdateVirtualTokens true After advancing NextCounter, mirror the counter to the linked virtual token.

The number of offline codes is not a server config value — it is supplied per request by the client (ocodes / OCODES). 0 disables generation for that call; -1 uses the token's window size.


7. FIDO/U2F offline pre-sign (PreSign.aspx)

For FIDO/U2F tokens the offline story is different (there are no HOTP codes). Instead the client calls PreSign.aspx with OFF=1. On success the server returns the normal U2F sign material plus the app's ECC-256 public certificate (AppIdECC256PublicCertB64) so the offline agent can verify the authenticator's signature locally without contacting the server.


8. Manual test checklist

  1. Online validate returns cache
    • Call validate_otp (or ValidateOTP.aspx) with a valid event/offline token OTP and ocodes=N (OCODES=N).
    • Expect success and an offlineCodes list of exactly N comma-separated codes.
  2. Counter advances / no reuse
    • Confirm Devices.NextCounter advanced by 1 after the match.
    • Confirm the returned list does not contain the code you just used and that the first returned code equals HOTP(secret, NextCounter).
  3. ocodes=0 -> success but empty/omitted offlineCodes.
  4. ocodes=-1 -> number of returned codes equals the token's OTPWindowSize.
  5. Disabled token / non-event token -> success (if OTP valid) but no offline codes.
  6. Virtual token mirroring (Server.UpdateVirtualTokens=true) -> linked virtual token counter matches the primary token.
  7. FIDO offline -> PreSign.aspx?...&OFF=1 returns the extra ECC-256 public cert line.

9. Key source references

  • Submodules/SurePassIdDotNetLibs/SurePassClassLib/AuthServerAPI/AuthServerApiOtp.cs
    • ValidateOtpOfflineCodes, ValidateOtpInternal
    • GenerateOffLineCodeDevice, GenerateOffLineCodes
  • SurePassIdLegacyMfaServer/server/ApiActions/ValidateOtp.cs (JSON endpoint)
  • SurePassIdLegacyMfaServer/server/ApiActions/CreateServerChallenge.cs
  • SurePassIdLegacyMfaServer/server/Models/ValidateOathOtpResponse.cs
  • SurePassIdLegacyMfaServer/AuthServer/ValidateOTP.aspx.cs (legacy endpoint)
  • SurePassIdLegacyMfaServer/AuthServer/PreSign.aspx.cs (FIDO offline pre-sign)
  • Submodules/SurePassIdDotNetLibs/SurePassClassLib/Enums/OTPTypesEnum.cs
  • Submodules/SurePassIdDotNetLibs/SurePassClassLib/Extensions/OTPTypeExtensions.cs
SurePassID 360 Central Avenue #800 St. Petersburg, FL 33701 USA +1 (888) 200-8144 surepassid.com