Implementing Multi-Factor Authentication (MFA) with TOTP in Java

Overview

Time-based One-Time Password (TOTP) is a popular method for implementing Multi-Factor Authentication (MFA) that adds an extra layer of security to user authentication. This article demonstrates how to implement TOTP-based MFA in Java applications.

Key Concepts

TOTP Algorithm

TOTP is defined in RFC 6238 and extends the HMAC-based One-Time Password (HOTP) algorithm by using the current time as the counter value. The formula is:

TOTP = HMAC-SHA-1(K, T) mod 10^d

Where:

  • K is the shared secret key
  • T is the current timestamp divided by the time step (typically 30 seconds)
  • d is the number of digits in the OTP (usually 6)

Implementation

1. Dependencies

Add the following dependency to your pom.xml:

<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.16.0</version>
</dependency>

2. TOTP Generator Class

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.Base64;
import org.apache.commons.codec.binary.Base32;
public class TOTPGenerator {
private static final int TIME_STEP = 30; // 30-second intervals
private static final int CODE_DIGITS = 6;
private static final String HMAC_ALGORITHM = "HmacSHA1";
/**
* Generates a random secret key for TOTP
*/
public static String generateSecretKey() {
byte[] buffer = new byte[20]; // 160 bits recommended by RFC 4226
new java.security.SecureRandom().nextBytes(buffer);
Base32 base32 = new Base32();
return base32.encodeToString(buffer);
}
/**
* Generates TOTP code for the current time
*/
public static String generateTOTP(String secretKey) {
long timeWindow = Instant.now().getEpochSecond() / TIME_STEP;
return generateTOTP(secretKey, timeWindow);
}
/**
* Generates TOTP code for a specific time window
*/
private static String generateTOTP(String secretKey, long timeWindow) {
try {
// Decode base32 secret key
Base32 base32 = new Base32();
byte[] keyBytes = base32.decode(secretKey);
// Convert time window to byte array
byte[] timeBytes = new byte[8];
for (int i = 7; i >= 0; i--) {
timeBytes[i] = (byte) (timeWindow & 0xFF);
timeWindow >>= 8;
}
// Calculate HMAC-SHA1
Mac mac = Mac.getInstance(HMAC_ALGORITHM);
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, HMAC_ALGORITHM);
mac.init(keySpec);
byte[] hmacResult = mac.doFinal(timeBytes);
// Dynamic truncation
int offset = hmacResult[hmacResult.length - 1] & 0xF;
int binary = ((hmacResult[offset] & 0x7F) << 24) |
((hmacResult[offset + 1] & 0xFF) << 16) |
((hmacResult[offset + 2] & 0xFF) << 8) |
(hmacResult[offset + 3] & 0xFF);
int otp = binary % (int) Math.pow(10, CODE_DIGITS);
return String.format("%0" + CODE_DIGITS + "d", otp);
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new RuntimeException("Error generating TOTP", e);
}
}
/**
* Verifies a TOTP code with tolerance for clock drift
*/
public static boolean verifyTOTP(String secretKey, String code, int timeTolerance) {
long currentTimeWindow = Instant.now().getEpochSecond() / TIME_STEP;
// Check code in current and adjacent time windows (for clock drift)
for (int i = -timeTolerance; i <= timeTolerance; i++) {
String testCode = generateTOTP(secretKey, currentTimeWindow + i);
if (testCode.equals(code)) {
return true;
}
}
return false;
}
}

3. User Service with MFA

import java.util.HashMap;
import java.util.Map;
public class UserService {
private Map<String, User> users = new HashMap<>();
public static class User {
private String username;
private String passwordHash;
private String totpSecret;
private boolean mfaEnabled;
public User(String username, String passwordHash) {
this.username = username;
this.passwordHash = passwordHash;
this.mfaEnabled = false;
}
// Getters and setters
public String getTotpSecret() { return totpSecret; }
public void setTotpSecret(String totpSecret) { this.totpSecret = totpSecret; }
public boolean isMfaEnabled() { return mfaEnabled; }
public void setMfaEnabled(boolean mfaEnabled) { this.mfaEnabled = mfaEnabled; }
public String getPasswordHash() { return passwordHash; }
}
/**
* Enable MFA for a user
*/
public String enableMFA(String username) {
User user = users.get(username);
if (user == null) {
throw new IllegalArgumentException("User not found");
}
String secretKey = TOTPGenerator.generateSecretKey();
user.setTotpSecret(secretKey);
user.setMfaEnabled(true);
return secretKey;
}
/**
* Disable MFA for a user
*/
public void disableMFA(String username) {
User user = users.get(username);
if (user != null) {
user.setTotpSecret(null);
user.setMfaEnabled(false);
}
}
/**
* Authenticate user with MFA
*/
public boolean authenticate(String username, String password, String totpCode) {
User user = users.get(username);
if (user == null) {
return false;
}
// Verify password (in real application, use proper password hashing)
if (!user.getPasswordHash().equals(password)) {
return false;
}
// If MFA is enabled, verify TOTP code
if (user.isMfaEnabled()) {
if (totpCode == null || totpCode.isEmpty()) {
return false;
}
return TOTPGenerator.verifyTOTP(user.getTotpSecret(), totpCode, 1); // 30-second tolerance
}
return true;
}
public void addUser(User user) {
users.put(user.username, user);
}
}

4. QR Code Generation for Authenticator Apps

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
public class QRCodeGenerator {
/**
* Generates a URL for QR code generation
*/
public static String generateQRCodeURL(String secret, String account, String issuer) {
try {
String format = "otpauth://totp/%s?secret=%s&issuer=%s";
String label = URLEncoder.encode(issuer + ":" + account, "UTF-8");
String encodedIssuer = URLEncoder.encode(issuer, "UTF-8");
return String.format(format, label, secret, encodedIssuer);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("Error generating QR code URL", e);
}
}
/**
* Generates a Google Charts QR code URL
*/
public static String generateGoogleQRCodeURL(String secret, String account, String issuer) {
String otpAuthURL = generateQRCodeURL(secret, account, issuer);
String format = "https://chart.googleapis.com/chart?chs=200x200&chld=M|0&cht=qr&chl=%s";
try {
return String.format(format, URLEncoder.encode(otpAuthURL, "UTF-8"));
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("Error generating Google QR code URL", e);
}
}
}

5. Example Usage

public class MFAExample {
public static void main(String[] args) {
// Initialize user service
UserService userService = new UserService();
// Create and add a user
UserService.User user = new UserService.User("john.doe", "hashedpassword");
userService.addUser(user);
// Enable MFA for the user
String secretKey = userService.enableMFA("john.doe");
System.out.println("Secret Key: " + secretKey);
// Generate QR code URL for authenticator app
String qrCodeURL = QRCodeGenerator.generateGoogleQRCodeURL(
secretKey, "john.doe", "MyApp");
System.out.println("QR Code URL: " + qrCodeURL);
// Simulate authentication
String currentCode = TOTPGenerator.generateTOTP(secretKey);
System.out.println("Current TOTP: " + currentCode);
boolean authenticated = userService.authenticate(
"john.doe", "hashedpassword", currentCode);
System.out.println("Authentication successful: " + authenticated);
}
}

Security Best Practices

  1. Secure Secret Storage: Store TOTP secrets encrypted in your database
  2. Backup Codes: Provide users with backup codes for account recovery
  3. Rate Limiting: Implement rate limiting on authentication attempts
  4. Session Management: Create new sessions after successful MFA verification
  5. Logging: Log MFA setup and authentication events for security monitoring

Conclusion

Implementing TOTP-based MFA in Java provides a robust security layer for your applications. This implementation follows RFC 6238 standards and is compatible with popular authenticator apps like Google Authenticator, Authy, and Microsoft Authenticator. Remember to handle secrets securely and provide users with proper backup options for account recovery.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper