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/CreateServerChallengeauthentication flow and theGenerateOffLineCode*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 (orPSprinted serial number; one is required).O– the OTP to validate.OCODES– number of offline codes to pre-generate and return (short, default0).
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:
- Load partner settings and configure
KeyManagement(encryption/HSM provider) fromPartnerSettings.EncyptionType. - Resolve and validate the partner user (enabled check, bypass-MFA check, printed serial number check).
- For each of the user's devices, attempt validation:
- Send-OTP / push path (
VerifySendOTP) and temporary OTP path (VerifyTemporaryOTP) — on success these also callGenerateOffLineCodes(...). - Event/HOTP path — walk the OTP window from
NextCounterlooking for a match.
- Send-OTP / push path (
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
NextCounteris 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, defaulttrue) keeps any linked virtual token copy's counter aligned.- Optional audit row in
PartnerUserOTPis written whenServer.LogEventOtpUsageInOtpTable(defaulttrue) is enabled.
5. Offline-code generation details
Method:
GenerateOffLineCodeDevice(KeyManagement, TableDevice, int numberOfflineCodes)
in AuthServerApiOtp.cs.
Rules:
- Returns
false(no codes) if the device isnull, notEnabled, or not an event/offline-based type (Event,Event256,Event512,OffLine,DynamicCvcEvent). numberOfflineCodes == 0-> returnstruebut generates no codes (feature off for this call).numberOfflineCodes == -1-> uses the token'sOTPWindowSizeas the count.- Otherwise generates exactly
numberOfflineCodescodes.
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
OTPLengthand 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
- Online validate returns cache
- Call
validate_otp(orValidateOTP.aspx) with a valid event/offline token OTP andocodes=N(OCODES=N). - Expect success and an
offlineCodeslist of exactlyNcomma-separated codes.
- Call
- Counter advances / no reuse
- Confirm
Devices.NextCounteradvanced 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).
- Confirm
ocodes=0-> success but empty/omittedofflineCodes.ocodes=-1-> number of returned codes equals the token'sOTPWindowSize.- Disabled token / non-event token -> success (if OTP valid) but no offline codes.
- Virtual token mirroring
(
Server.UpdateVirtualTokens=true) -> linked virtual token counter matches the primary token. - FIDO offline ->
PreSign.aspx?...&OFF=1returns the extra ECC-256 public cert line.
9. Key source references
Submodules/SurePassIdDotNetLibs/SurePassClassLib/AuthServerAPI/AuthServerApiOtp.csValidateOtpOfflineCodes,ValidateOtpInternalGenerateOffLineCodeDevice,GenerateOffLineCodes
SurePassIdLegacyMfaServer/server/ApiActions/ValidateOtp.cs(JSON endpoint)SurePassIdLegacyMfaServer/server/ApiActions/CreateServerChallenge.csSurePassIdLegacyMfaServer/server/Models/ValidateOathOtpResponse.csSurePassIdLegacyMfaServer/AuthServer/ValidateOTP.aspx.cs(legacy endpoint)SurePassIdLegacyMfaServer/AuthServer/PreSign.aspx.cs(FIDO offline pre-sign)Submodules/SurePassIdDotNetLibs/SurePassClassLib/Enums/OTPTypesEnum.csSubmodules/SurePassIdDotNetLibs/SurePassClassLib/Extensions/OTPTypeExtensions.cs
© 2013–2026 SurePassID. All rights reserved. Protected by patents pending. SurePassID, the SurePassID logo and design, and Secure SSO are registered trademarks or trademarks of SurePassID, Corp. in the United States and/or other jurisdictions. All other marks and names mentioned herein may be trademarks of their respective companies.
SurePassID 360 Central Avenue #800 St. Petersburg, FL 33701 USA +1 (888) 200-8144 surepassid.com