Implementing Two-Factor Authentication in Spring Security with Google Authenticator, Authy, and Custom TOTP
This article explains how to add two‑factor authentication to a Spring Security‑based Java web application by integrating Google Authenticator, Authy, or a custom TOTP module, covering required dependencies, service implementations, security configuration, and testing procedures.
1. Introduction
1.1 Spring Security Overview
Spring Security is a security framework built on the Spring ecosystem that provides authentication and authorization services for Java applications.
1.2 Necessity of Two-Factor Authentication
Traditional username‑password authentication can be compromised, so a second factor after login is required to strengthen identity verification.
2. Ways Spring Security Implements Two-Factor Authentication
2.1 Using Existing Two‑Factor Services
2.1.1 Integrating Google Authenticator
Google Authenticator is an open‑source TOTP‑based app that can be installed on smartphones for second‑factor authentication. Spring Security can use it as follows.
2.1.1.1 Add Dependency in pom.xml
<dependency>
<groupId>com.warrenstrange</groupId>
<artifactId>googleauth</artifactId>
<version>1.0.0</version>
</dependency>2.1.1.2 Implement Google Authenticator Service
@Service
public class GoogleAuthenticatorService {
private final GoogleAuthenticator gAuth = new GoogleAuthenticator();
/** Get secret */
public String createSecret() {
final GoogleAuthenticatorKey gak = gAuth.createCredentials();
return gak.getKey();
}
/** Verify TOTP */
public boolean authorize(final String secret, final int otp) {
return gAuth.authorize(secret, otp);
}
/** Generate QR code URL */
public String getQR(final String secret, final String account) {
final String format = "otpauth://totp/%s?secret=%s&issuer=%s";
return String.format(format, account, secret, account);
}
}The above creates a GoogleAuthenticatorService class with createSecret() , authorize() , and getQR() methods.
2.1.1.3 Configure Google Authenticator in Spring Security
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private GoogleAuthenticatorService googleAuthenticatorService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/user/**").authenticated()
.antMatchers("/admin/**").hasAnyRole("ADMIN")
.and()
.formLogin()
.and()
.addFilterBefore(buildGoogleAuthFilter(), UsernamePasswordAuthenticationFilter.class);
}
private GoogleAuthFilter buildGoogleAuthFilter() throws Exception {
final GoogleAuthFilter filter = new GoogleAuthFilter("/check-google-auth");
filter.setSecretProvider((request, username) -> {
String secret = userService.getSecret(username); // get user secret
return secret != null ? secret : "";
});
filter.setGoogleAuthenticator(googleAuthenticatorService.getGoogleAuthenticator());
return filter;
}
}The SecurityConfig registers a GoogleAuthFilter that obtains the user’s secret and validates the TOTP.
2.2 Integrating Authy
Authy is another TOTP provider. The steps are similar.
2.2.1 Add Dependency in pom.xml
<dependency>
<groupId>com.authy</groupId>
<artifactId>authy-client</artifactId>
<version>1.2</version>
</dependency>2.2.2 Implement Authy Service
@Service
public class AuthyService {
private static final String AUTHY_API_KEY = "your-authy-api-key";
private final AuthyApiClient authyApiClient = new AuthyApiClient(AUTHY_API_KEY);
/** Register user */
public User createUser(final String email, final String countryCode, final String phone) throws Exception {
Users users = authyApiClient.getUsers();
User user = users.createUser(email, phone, countryCode);
return user;
}
/** Send verification code */
public void sendVerification(final String userId, final String via) throws Exception {
Tokens tokens = authyApiClient.getTokens();
tokens.requestSms(Integer.valueOf(userId), via);
}
/** Verify token */
public boolean verifyToken(final String userId, final int token) throws Exception {
Tokens tokens = authyApiClient.getTokens();
TokenVerification tokenVerification = tokens.verify(Integer.valueOf(userId), token);
return tokenVerification.isOk();
}
}2.2.3 Configure Authy in Spring Security
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AuthyService authyService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/user/**").authenticated()
.antMatchers("/admin/**").hasAnyRole("ADMIN")
.and()
.formLogin()
.and()
.addFilterBefore(buildAuthyFilter(), UsernamePasswordAuthenticationFilter.class);
}
private AuthyFilter buildAuthyFilter() throws Exception {
final AuthyFilter filter = new AuthyFilter("/check-authy");
filter.setAuthyService(authyService);
return filter;
}
}3. Developing a Custom Two‑Factor Module
3.1 Implementing TOTP Service
A custom TOTP implementation based on the standard algorithm:
@Service
public class TotpService {
private static final int WINDOW_SIZE = 3;
private static final int CODE_DIGITS = 6;
private static final String HMAC_ALGORITHM = "HmacSHA1";
private static final int[] DIGITS_POWER = {1,10,100,1000,10000,100000,1000000,10000000,100000000};
/** Create secret */
public String createSecret() {
SecureRandom random = new SecureRandom();
byte[] bytes = new byte[64];
random.nextBytes(bytes);
return Base32Utils.encode(bytes);
}
/** Generate TOTP code */
public int generateCode(final String secret) throws Exception {
long timeIndex = System.currentTimeMillis() / 30000L; // 30‑second step
byte[] keyBytes = Base32Utils.decode(secret);
byte[] data = new byte[8];
for (int i = 7; i >= 0; i--) {
data[i] = (byte)(timeIndex & 0xff);
timeIndex >>= 8;
}
SecretKeySpec signingKey = new SecretKeySpec(keyBytes, HMAC_ALGORITHM);
Mac mac = Mac.getInstance(HMAC_ALGORITHM);
mac.init(signingKey);
byte[] hmac = mac.doFinal(data);
int offset = hmac[hmac.length - 1] & 0xf;
int binCode = ((hmac[offset] & 0x7f) << 24)
| ((hmac[offset+1] & 0xff) << 16)
| ((hmac[offset+2] & 0xff) << 8)
| (hmac[offset+3] & 0xff);
return binCode % DIGITS_POWER[CODE_DIGITS];
}
/** Verify TOTP */
public boolean authorize(final String secret, final int otp, final int tolerance) throws Exception {
long timeIndex = System.currentTimeMillis() / 30000L;
byte[] keyBytes = Base32Utils.decode(secret);
for (int i = -tolerance; i <= tolerance; i++) {
long ti = timeIndex + i;
byte[] data = new byte[8];
for (int j = 7; j >= 0; j--) {
data[j] = (byte)(ti & 0xff);
ti >>= 8;
}
SecretKeySpec signingKey = new SecretKeySpec(keyBytes, HMAC_ALGORITHM);
Mac mac = Mac.getInstance(HMAC_ALGORITHM);
mac.init(signingKey);
byte[] hmac = mac.doFinal(data);
int offset = hmac[hmac.length - 1] & 0xf;
int binCode = ((hmac[offset] & 0x7f) << 24)
| ((hmac[offset+1] & 0xff) << 16)
| ((hmac[offset+2] & 0xff) << 8)
| (hmac[offset+3] & 0xff);
if (binCode % DIGITS_POWER[CODE_DIGITS] == otp) {
return true;
}
}
return false;
}
}3.2 Configuring the Custom Module in Spring Security
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private TotpService totpService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/user/**").authenticated()
.antMatchers("/admin/**").hasAnyRole("ADMIN")
.and()
.formLogin()
.and()
.addFilterBefore(buildTotpFilter(), UsernamePasswordAuthenticationFilter.class);
}
private TotpFilter buildTotpFilter() throws Exception {
final TotpFilter filter = new TotpFilter("/check-totp");
filter.setTotpService(totpService);
return filter;
}
}4. Implementation Process
4.1 Integrating Google Authenticator
Install the Linux PAM module:
sudo apt-get install libpam-google-authenticator -yGenerate a user‑specific configuration with google-authenticator , answer the prompts, and record the QR code and secret.
Edit /etc/pam.d/sshd to enforce the module:
auth required pam_google_authenticator.so nullok4.2 Configuring Google Authenticator in Spring Security
Add the Maven dependency (shown earlier) and create a custom filter that extracts the TOTP code from the request, validates it with GoogleAuthenticator , and either continues the filter chain or redirects to an error page.
4.3 Testing Google Authenticator Integration
Scan the QR code with the Google Authenticator app, obtain a six‑digit code, and include it on the login form. A valid code grants access to the home page.
4.4 Integrating Authy
Register on the Authy website, obtain an API key, add the authy-java dependency, implement AuthyService , and register an AuthyTotpFilter similar to the Google example.
4.5 Testing Authy Integration
Retrieve the user’s Authy ID, request a token via SMS or the Authy app, and verify it during login.
4.6 Developing a Custom SMS‑Based Two‑Factor Module
Use Twilio to send a verification code and verify it by querying recent messages.
import com.twilio.Twilio;
import com.twilio.rest.api.v2010.account.Message;
import com.twilio.type.PhoneNumber;
public class TwilioSMSVerifier {
private static final String TWILIO_ACCOUNT_SID = "ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
private static final String TWILIO_AUTH_TOKEN = "your_auth_token";
private static final String TWILIO_PHONE_NUMBER = "+1415XXXXXXX";
public static void sendMessage(String toPhoneNumber, String message) {
Twilio.init(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN);
Message twilioMessage = Message.creator(
new PhoneNumber(toPhoneNumber),
new PhoneNumber(TWILIO_PHONE_NUMBER),
message).create();
System.out.println("Twilio message SID: " + twilioMessage.getSid());
}
public static boolean verifyCode(String toPhoneNumber, String code) {
Twilio.init(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN);
List
messages = Message.reader()
.setTo(new PhoneNumber(toPhoneNumber))
.read();
for (Message message : messages) {
if (message.getBody().contains(code)) {
return true;
}
}
return false;
}
}Integrate the SMS verifier into a custom SMSAuthenticationProvider that extends AbstractUserDetailsAuthenticationProvider , storing the mobile number in a custom SMSUserDetails implementation.
public class SMSAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
private UserDetailsService userDetailsService;
private int codeLength;
private int codeExpiration;
// constructor, retrieveUser, additionalAuthenticationChecks, supports ...
public class SMSUserDetails implements UserDetails {
private final String username;
private final String password;
private final String mobile;
private final List
authorities;
// getters, account status methods returning true
}
}4.7 Testing Custom SMS Authentication
Send a test SMS with Twilio, then submit username, password, mobile number, and the received code to a controller that builds a UsernamePasswordAuthenticationToken (setting the mobile as details) and authenticates via the AuthenticationManager .
@RequestMapping(value = "/login", method = RequestMethod.POST)
public String login(HttpServletRequest request) {
String username = request.getParameter("username");
String password = request.getParameter("password");
String mobile = request.getParameter("mobile");
String code = request.getParameter("code");
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password, Arrays.asList(new SimpleGrantedAuthority("ROLE_USER")));
authRequest.setDetails(mobile);
Authentication authentication = authManager.authenticate(authRequest);
SecurityContextHolder.getContext().setAuthentication(authentication);
return "redirect:/home";
}5. Summary
The article introduced the concept of two‑factor authentication, discussed its advantages and drawbacks, and demonstrated how to integrate Google Authenticator, Authy, or a custom TOTP/SMS solution into a Spring Security‑based Java web application. It also covered configuration, testing steps, and common pitfalls.
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.