A complete guide to integrating Google Authenticator using AuthenticatorAPI.com
This guide shows how to add Google Authenticator-compatible two-factor authentication to Java applications. We cover both a plain Java utility class and a full Spring Boot integration with Spring Security. No third-party TOTP libraries are required — just Java's standard HTTP client.
HttpClient). For Spring Boot, version 3.x is used in examples. A JPA-compatible database for storing user secrets.A self-contained service class that wraps all calls to the AuthenticatorAPI. This works in any Java application — Spring, Jakarta EE, or plain Java.
import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.security.SecureRandom; import java.time.Duration; public class TotpService { private static final String BASE32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; private static final String API_BASE = "https://www.authenticatorApi.com"; private static final SecureRandom RNG = new SecureRandom(); private final HttpClient httpClient; public TotpService() { this.httpClient = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(10)) .build(); } /** Generates a cryptographically random 32-character Base32 secret. */ public String generateSecret() { return generateSecret(32); } public String generateSecret(int length) { StringBuilder sb = new StringBuilder(length); byte[] bytes = new byte[length]; RNG.nextBytes(bytes); for (byte b : bytes) { sb.append(BASE32_ALPHABET.charAt((b & 0xFF) % 32)); } return sb.toString(); } /** Returns the HTML img tag for the QR code setup page. */ public String getQrCodeHtml(String appName, String userInfo, String secret) throws Exception { String url = API_BASE + "/pair.aspx" + "?AppName=" + encode(appName) + "&AppInfo=" + encode(userInfo) + "&SecretCode=" + encode(secret); return get(url); } /** Returns true if the 6-digit pin is valid for the given secret. */ public boolean validatePin(String pin, String secret) throws Exception { if (!pin.matches("\\d{6}")) return false; String url = API_BASE + "/Validate.aspx" + "?Pin=" + pin + "&SecretCode=" + encode(secret); String response = get(url).trim(); return response.equalsIgnoreCase("true"); } private String get(String url) throws Exception { HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(url)) .timeout(Duration.ofSeconds(10)) .GET() .build(); return httpClient .send(request, HttpResponse.BodyHandlers.ofString()) .body(); } private String encode(String value) { return URLEncoder.encode(value, StandardCharsets.UTF_8); } }
// TotpConfig.java import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class TotpConfig { @Bean public TotpService totpService() { return new TotpService(); } }
// TwoFactorController.java import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; import jakarta.servlet.http.HttpSession; @Controller @RequestMapping("/2fa") public class TwoFactorController { private final TotpService totpService; private final UserService userService; public TwoFactorController(TotpService totpService, UserService userService) { this.totpService = totpService; this.userService = userService; } // GET /2fa/setup — show the QR code @GetMapping("/setup") public String setup(HttpSession session, Model model) throws Exception { String secret = totpService.generateSecret(); session.setAttribute("pending2faSecret", secret); String username = /* current principal */ ""; model.addAttribute("qrHtml", totpService.getQrCodeHtml("MyApp", username, secret)); model.addAttribute("secret", secret); return "2fa/setup"; } // POST /2fa/setup — confirm the user scanned correctly @PostMapping("/setup") public String confirmSetup(@RequestParam String pin, HttpSession session, Model model) throws Exception { String secret = (String) session.getAttribute("pending2faSecret"); if (totpService.validatePin(pin, secret)) { userService.saveTotpSecret(/* userId */, secret); session.removeAttribute("pending2faSecret"); return "redirect:/dashboard"; } model.addAttribute("error", "Invalid code — please try again."); return "2fa/setup"; } // POST /2fa/verify — called during login @PostMapping("/verify") public String verify(@RequestParam String pin, HttpSession session, Model model) throws Exception { String secret = userService.getDecryptedTotpSecret(/* userId */); if (totpService.validatePin(pin, secret)) { session.setAttribute("2faVerified", true); return "redirect:/dashboard"; } model.addAttribute("error", "Invalid code — please try again."); return "2fa/verify"; } }
Add a filter that intercepts requests after password authentication and redirects users who haven't yet completed their TOTP check.
// TwoFactorFilter.java import jakarta.servlet.*; import jakarta.servlet.http.*; import org.springframework.web.filter.OncePerRequestFilter; public class TwoFactorFilter extends OncePerRequestFilter { private static final String[] EXEMPT = {"/2fa/", "/login", "/logout", "/css/", "/js/"}; @Override protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws ServletException, IOException { HttpSession session = req.getSession(false); String path = req.getServletPath(); boolean isExempt = false; for (String e : EXEMPT) if (path.startsWith(e)) { isExempt = true; break; } if (!isExempt && session != null && session.getAttribute("userId") != null && !Boolean.TRUE.equals(session.getAttribute("2faVerified"))) { res.sendRedirect("/2fa/verify"); return; } chain.doFilter(req, res); } }
SecureRandom — never java.util.RandomHttpClient instance — it is thread-safe and connection-pooling awareBucket4j integration)