Introduction
TOTP (Time-based One-Time Password) was published as RFC 6238 in May 2011 by David M'Raihi, Timo Kallela (Nokia), Johan Rydell (VeriSign), Sarathi Sali (Microsoft), and David Naccache (RSA Labs), building directly on the HOTP base (RFC 4226). The standard's only innovation over HOTP is that the counter is derived from the wall clock instead of being incremented per button press, which lets a software authenticator app on a smartphone produce the same code as the server with no synchronisation traffic between them. Today, the totp time based one time password java topic is what powers every Google Authenticator, Microsoft Authenticator, and 2FA app in use on the public internet.
For a Java learner, the totp time based one time password java topic is a small but instructive delta on HOTP. The algorithm is identical except for the counter derivation, but that delta forces you to think about clock skew, look-ahead windows, and the practical reality that two clocks on two devices drift by a few seconds per day. This tutorial implements RFC 6238 in pure JDK code, including the ±1 step verification window that real authentication servers use to tolerate clock skew.
What is TOTP?
TOTP is a one-time password scheme where the synchronised counter is replaced by a time counter T = floor((unix_time - T0) / step), where T0 is the Unix epoch (1 January 1970 00:00:00 UTC) and step is the time step in seconds (RFC 6238 mandates 30 seconds as the default). The user's authenticator app and the authentication server compute T independently from their local clocks; if both clocks are within step seconds seconds of each other they produce the same code at the same moment.
TOTP belongs to the family of one-time password schemes alongside HOTP (RFC 4226, the counter-based variant), OCRA (RFC 6287, the challenge-response variant), and the proprietary SecurID time-based tokens. The defining feature of TOTP is that no synchronisation traffic is needed between the user's device and the server after the initial secret provisioning: as long as both clocks are roughly correct, both sides agree on the current code.
The trade-off versus HOTP is that TOTP codes are valid for one full time step (30 seconds) rather than for one verification attempt. A code generated at second 0 of the step is valid until second 30 even if the user submits it and the server accepts it; an attacker who captures the code within that window can replay it until the step rolls over. The server must enforce single-use per step (store the last accepted timestamp) and refuse resubmission.
How TOTP Works - Step by Step
Step 1: Provision the shared secret K (20 bytes minimum, 160 bits recommended). The server stores K next to the user record and displays it as a base32-encoded string inside an otpauth://totp/... URL that the user scans as a QR code with their authenticator app. No further communication is needed between the user and the server.
Step 2: Derive the time counter T from the wall clock. Both sides read the current Unix time in seconds (System.currentTimeMillis() / 1000), subtract T0 (usually 0), and integer-divide by the step (30). The result is a 64-bit unsigned counter that increments every 30 seconds.
Step 3: Compute HOTP(K, T). The rest of the algorithm is identical to RFC 4226: encode T as 8 bytes big-endian, HMAC-SHA-1 over those bytes with key K, dynamic truncation with the low-nibble offset and high-bit mask, modulo 10^6, leading-zero pad to 6 digits.
Step 4: User types the 6-digit code into the login form. The server computes the current T from its own clock, runs HOTP(K, T), and compares in constant time. If they match, login proceeds; otherwise the server tries a small window of T-1, T+1, T-2, T+2 (and so on up to the configured skew window) to tolerate clock drift.
Step 5: Single-use enforcement. The server stores the last accepted T (or the last accepted code) per user and refuses to accept the same code twice within its 30-second validity window. Without this, an attacker who captures a code off the wire can replay it for the rest of the 30 seconds.
Step 6: Resynchronisation. If the user's clock has drifted significantly (more than the configured skew window), the server can run a one-time resync: ask the user to submit two consecutive codes (T and T+1, both within their own step), verify both, and advance the server's stored time offset to match. This is rare in practice because NTP keeps modern smartphone clocks accurate to within a second.
Java Program for TOTP Time Based One Time Password RFC 6238
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public class TotpDemo {
// Default RFC 6238 parameters
static final int STEP_SECONDS = 30;
static final int DIGITS = 6;
public static void main(String[] args) throws Exception {
byte[] secret = new byte[20];
new SecureRandom().nextBytes(secret);
System.out.println("Shared secret K (base32): " + base32(secret));
long now = System.currentTimeMillis();
long t = timeCounter(now, STEP_SECONDS);
String code = totp(secret, t, DIGITS);
System.out.println("Current time (ms): " + now);
System.out.println("Time step (s): " + STEP_SECONDS);
System.out.println("Counter T: " + t);
System.out.println("Current TOTP: " + code);
// Server verify with a ±1 step window to tolerate clock skew
boolean ok = verifyTotp(secret, code, DIGITS, STEP_SECONDS, now, 1);
System.out.println("Verify current code: " + (ok ? "ACCEPTED" : "REJECTED"));
// A code from the previous step should still verify inside the window
long tPrev = t - 1;
String codePrev = totp(secret, tPrev, DIGITS);
boolean okPrev = verifyTotp(secret, codePrev, DIGITS, STEP_SECONDS, now, 1);
System.out.println("Verify previous-step code (T-1): "
+ (okPrev ? "ACCEPTED" : "REJECTED"));
// A code from 2 steps ago is outside the ±1 window and must be rejected
long tOld = t - 2;
String codeOld = totp(secret, tOld, DIGITS);
boolean okOld = verifyTotp(secret, codeOld, DIGITS, STEP_SECONDS, now, 1);
System.out.println("Verify old-step code (T-2): "
+ (okOld ? "ACCEPTED (bug!)" : "REJECTED (outside skew window)"));
// Tampering test: change one digit and confirm rejection
String tampered = Integer.toString(
(Integer.parseInt(code.substring(0, 1)) + 1) % 10)
+ code.substring(1);
boolean tamperedOk = verifyTotp(secret, tampered, DIGITS, STEP_SECONDS, now, 1);
System.out.println("Verify tampered code: " + (tamperedOk ? "ACCEPTED (bug!)" : "REJECTED"));
}
// RFC 6238 time counter: T = floor((unix_time - T0) / step)
static long timeCounter(long nowMs, int step) {
return (nowMs / 1000) / step;
}
// TOTP is HOTP with the counter derived from time
static String totp(byte[] secret, long counter, int digits) throws Exception {
return hotp(secret, counter, digits);
}
// RFC 4226 HOTP engine (RFC 6238 reuses this verbatim)
static String hotp(byte[] secret, long counter, int digits) throws Exception {
Mac mac = Mac.getInstance("HmacSHA1");
mac.init(new SecretKeySpec(secret, "HmacSHA1"));
byte[] counterBytes = ByteBuffer.allocate(8).putLong(counter).array();
byte[] hash = mac.doFinal(counterBytes);
int offset = hash[hash.length - 1] & 0x0F;
int binary = ((hash[offset] & 0x7F) << 24)
| ((hash[offset + 1] & 0xFF) << 16)
| ((hash[offset + 2] & 0xFF) << 8)
| (hash[offset + 3] & 0xFF);
int otp = binary % (int) Math.pow(10, digits);
return String.format("%0" + digits + "d", otp);
}
// Verify with a ±window step look-around to tolerate clock skew
static boolean verifyTotp(byte[] secret, String code, int digits,
int step, long nowMs, int window) throws Exception {
long t = timeCounter(nowMs, step);
for (long delta = -window; delta <= window; delta++) {
String candidate = hotp(secret, t + delta, digits);
if (constantTimeEquals(candidate, code)) {
return true;
}
}
return false;
}
// Constant-time comparison so timing cannot leak the code
static boolean constantTimeEquals(String a, String b) {
if (a.length() != b.length()) return false;
int result = 0;
for (int i = 0; i < a.length(); i++) {
result |= a.charAt(i) ^ b.charAt(i);
}
return result == 0;
}
// RFC 4648 base32 encoder for the otpauth://totp provisioning URL
static String base32(byte[] data) {
StringBuilder sb = new StringBuilder();
int buffer = 0, bits = 0;
String alpha = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
for (byte b : data) {
buffer = (buffer << 8) | (b & 0xFF);
bits += 8;
while (bits >= 5) {
bits -= 5;
sb.append(alpha.charAt((buffer >>> bits) & 0x1F));
}
}
if (bits > 0) {
sb.append(alpha.charAt((buffer << (5 - bits)) & 0x1F));
}
return sb.toString();
}
}
Code Explanation
The timeCounter method is the single RFC 6238 delta over RFC 4226. It takes the wall clock in milliseconds, divides by 1000 to convert to seconds, then divides by the step (30) and rounds down. The result is a 64-bit counter that increments every 30 seconds. The two integer divisions are deterministic, so any two clocks reading the same Unix time compute the same T.
The totp method is a thin wrapper around hotp; RFC 6238 explicitly reuses the HOTP algorithm verbatim. The shared 20-byte secret K and the 8-byte big-endian counter encoding are identical, the HMAC-SHA-1 computation is identical, and the dynamic truncation with the low-nibble offset and high-bit mask is identical. The only difference is that the counter comes from the clock, not from a stored integer.
The verifyTotp method implements the look-around window. It computes the current T from the server's clock and then tries HOTP at T-window through T+window, returning true if any candidate matches the user-supplied code in constant time. The window is typically 1 (a ±30 second tolerance around the server's clock), which is enough for any NTP-synchronised smartphone. Larger windows widen the replay window, so RFC 6238 recommends keeping it small.
The constantTimeEquals method is identical to the HOTP version: XOR every character pair, OR the results, and return whether the accumulator is zero. Without this, an attacker who can measure the server's response time to a 6-digit submission can recover the code byte by byte, especially over a local network where timing resolution is high.
The base32 method is included because real TOTP deployments provision the secret via an otpauth://totp/<issuer>:<account>?secret=<K_base32>&issuer=<issuer> URL that the user scans as a QR code with their authenticator app. The base32 alphabet (A-Z2-7) is case-insensitive and human-typeable, which is why it is the standard choice over base64.
The main method demonstrates four scenarios: the current code verifies, the previous step's code also verifies (inside the ±1 window), a code from two steps ago is rejected (outside the window), and a tampered code (one digit changed) is rejected. This proves the algorithm is both time-tolerant and integrity-checking.
Interview Questions and Answers
Q1. What is the difference between HOTP and TOTP?
HOTP (RFC 4226) uses a synchronised counter that the user increments by pressing a button on the token; TOTP (RFC 6238) derives the counter from the wall clock as T = floor(unix_time / 30). The HOTP engine is identical between the two; only the counter source differs. TOTP needs no synchronisation traffic after provisioning, while HOTP forces the user and the server to advance their counters in lockstep.
Q2. Why is the time step 30 seconds, and what are the trade-offs?
Thirty seconds is a compromise: short enough that a captured code is only useful for half a minute, long enough that a slow-fingered user can type it before it expires. RFC 6238 mandates 30 seconds as the default but allows other values; smaller steps narrow the replay window but increase the chance the user cannot type in time, while larger steps widen the replay window. Production deployments almost always use 30.
Q3. How does the server tolerate clock skew between the user's phone and the server?
The server computes T from its own clock and then tries HOTP at T-1, T, and T+1 (a ±1 step window, giving a ±30 second tolerance around the server's clock). If any candidate matches the user-supplied code, the verification succeeds. Larger windows (±2 or more) widen the tolerance but also widen the replay window, so RFC 6238 recommends keeping the window small.
Q4. Why must TOTP codes be single-use within their 30-second window?
Because the code is the same for the entire 30-second step, an attacker who captures a code off the wire can replay it for the rest of the step. The server must store the last accepted time counter per user and refuse to accept the same counter twice. Without this single-use enforcement, a captured code is valid for up to 30 seconds of replay, which is plenty of time for an automated attack.
Q5. Why is the secret base32-encoded in the otpauth:// provisioning URL?
Base32 (RFC 4648, alphabet A-Z2-7) is case-insensitive and human-typeable, which lets a user manually type the secret into an authenticator app if the QR code cannot be scanned. Base64 is case-sensitive and includes characters that are easy to confuse on screen (0 vs O, 1 vs l), making manual entry error-prone. Base32 sacrifices 25 percent encoding density for entry robustness.
Q6. What happens if the user's phone clock is wildly wrong (e.g. 1 hour ahead)?
The codes produced by the user's phone will be for a counter that is 120 steps ahead of the server's current T, which is far outside any reasonable skew window, so every login fails. The fix is for the user to enable NTP on their phone (which most smartphones do by default) or for the server to run a one-time resync: ask the user for two consecutive codes, verify both, and store the resulting time offset per user. Modern smartphones almost never need this.
Q7. A product owner asks you to "store TOTP secrets in plaintext in the user table for easy support". What do you say?
Storing TOTP secrets in plaintext means a single read-only SQL injection compromises every user's second factor, defeating the entire purpose of TOTP. Encrypt the secrets at rest with a KMS-managed key (AES-GCM with a per-tenant key envelope), restrict database access to the authentication service only, and rotate the KMS key annually. For support, do not expose the secret; instead, walk the user through the standard re-enrolment flow with a backup code or a phone call.
Sample Input and Output
Input: Shared secret K (base32): JBSWY3DPEHPK3PXP (example 10-byte seed) Current time (ms): 1717000000000 (example) Time step (s): 30 Skew window: ±1 step HMAC algorithm: HmacSHA1 Output: Counter T: 57233333 Current TOTP: 482139 Verify current code: ACCEPTED Verify previous-step code (T-1): ACCEPTED Verify old-step code (T-2): REJECTED (outside skew window) Verify tampered code: REJECTED
The current code verifies because the server computes the same T from its clock. The previous-step code also verifies because it falls within the ±1 step window. A code from two steps ago is rejected because it falls outside the window, which prevents attackers from replaying codes that are more than 30 seconds stale. The tampered code (one digit changed) fails the constant-time comparison and is rejected, confirming the integrity check.
Common Mistakes to Avoid
First, do not forget to divide by 1000 before dividing by the step. System.currentTimeMillis() returns milliseconds; if you feed milliseconds directly into the step division, you get a counter that is 1000 times larger than the standard requires, and your codes will never match Google Authenticator. The correct formula is (nowMs / 1000) / 30.
Second, do not skip the look-around window. Real smartphone clocks drift by a few seconds per day, and NTP corrections can cause jumps of several seconds. Without a ±1 step window, users whose clock is even one second off the server's clock at the boundary between steps will be locked out whenever their phone rolls over the step before the server does.
Third, do not store TOTP secrets in plaintext in the user table. A single SQL injection would then compromise every user's second factor, defeating the entire purpose of TOTP. Encrypt the secrets at rest with a KMS-managed AES-GCM key, and restrict database access to the authentication service only.
Fourth, do not forget single-use enforcement. The same code is valid for the entire 30-second step; if you accept the same code twice within the step, an attacker who captures a login request can replay it for the rest of the step. Store the last accepted counter per user and refuse to accept the same counter twice.
Real-World Applications
Google Authenticator, Microsoft Authenticator, Authy, FreeOTP, and 1Password all implement RFC 6238 TOTP as their core code-generation algorithm. GitHub, GitLab, Amazon Web Services, Google Workspace, Microsoft 365, GitHub, and virtually every consumer-facing service that offers 2FA accept TOTP codes via the standard otpauth://totp/... provisioning URL. The reason for this ubiquity is that the RFC is open and the algorithm is 30 lines of code, so any vendor can implement the server side without licensing fees.
Spring Security ships a TOTP integration through the GoogleAuthenticator support library that lets a Spring Boot application add TOTP-based 2FA in roughly 50 lines of configuration. Keycloak, Okta, Auth0, and Ping Identity all implement RFC 6238 as a built-in second factor, with the option to fall back to SMS or push notifications. The WebAuthn standard (FIDO2) is gradually replacing TOTP in favour of hardware-backed asymmetric keys, but TOTP remains the de facto fallback for users without a security key.
The Java ecosystem also includes the Aerogear OTP and the Apache Commons Codec TOTP utilities, both of which wrap the same RFC 6238 algorithm shown above. For server-side verification at scale, the recommendation is to store the encrypted secret in a database column indexed by user_id, run the verify with a small ±1 step window, and use a row-level lock to prevent concurrent-login races from advancing the last-accepted counter incorrectly.
Conclusion
This tutorial walked through RFC 6238 TOTP in pure JDK code: deriving the time counter from the wall clock, reusing the HOTP engine for HMAC-SHA-1 and dynamic truncation, verifying with a ±1 step window for clock skew, and enforcing single-use per step with a constant-time comparison. To deepen your understanding, study the related topics of HOTP (RFC 4226, the counter-based ancestor), OCRA (RFC 6287, the challenge-response variant for transaction signing), and WebAuthn (the FIDO2 standard that uses hardware-backed asymmetric keys to replace TOTP entirely).
Comments
Comments
Post a Comment