Implement Two-Factor Authentication in Spring Security with Google Authenticator, Authy, and Custom TOTP
This guide explains how to add two‑factor authentication to a Spring Security‑based Java web application using Google Authenticator, Authy, or a custom TOTP module, covering configuration, code implementation, and testing procedures.
1. Introduction
Spring Security is a security framework built on the Spring ecosystem that provides authentication and authorization for Java applications.
Traditional username‑password authentication is vulnerable to cracking, so a second factor is required after login to strengthen security.
2. Implementing Two‑Factor Authentication with Spring Security
2.1 Use Existing Two‑Factor Services
2.1.1 Integrate Google Authenticator
Google Authenticator is an open‑source TOTP implementation that can be installed on a smartphone. The following example shows how to add it to Spring Security.
<dependency>
<groupId>com.warrenstrange</groupId>
<artifactId>googleauth</artifactId>
<version>1.0.0</version>
</dependency>Service implementation:
@Service
public class GoogleAuthenticatorService {
private final GoogleAuthenticator gAuth = new GoogleAuthenticator();
/** Get secret */
public String createSecret() {
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) {
String format = "otpauth://totp/%s?secret=%s&issuer=%s";
return String.format(format, account, secret, account);
}
}Spring Security configuration:
@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 {
GoogleAuthFilter filter = new GoogleAuthFilter("/check-google-auth");
filter.setSecretProvider((request, username) -> {
String secret = userService.getSecret(username);
return secret != null ? secret : "";
});
filter.setGoogleAuthenticator(googleAuthenticatorService.getGoogleAuthenticator());
return filter;
}
}2.1.2 Integrate Authy
Authy is another TOTP provider. Add its Maven dependency:
<dependency>
<groupId>com.authy</groupId>
<artifactId>authy-client</artifactId>
<version>1.2</version>
</dependency>Service implementation:
@Service
public class AuthyService {
private static final String AUTHY_API_KEY = "your-authy-api-key";
private final AuthyApiClient authyApiClient = new AuthyApiClient(AUTHY_API_KEY);
public User createUser(String email, String countryCode, String phone) throws Exception {
Users users = authyApiClient.getUsers();
return users.createUser(email, phone, countryCode);
}
public void sendVerification(String userId, String via) throws Exception {
Tokens tokens = authyApiClient.getTokens();
tokens.requestSms(Integer.valueOf(userId), via);
}
public boolean verifyToken(String userId, int token) throws Exception {
Tokens tokens = authyApiClient.getTokens();
TokenVerification verification = tokens.verify(Integer.valueOf(userId), token);
return verification.isOk();
}
}Spring Security configuration for Authy:
@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 {
AuthyFilter filter = new AuthyFilter("/check-authy");
filter.setAuthyService(authyService);
return filter;
}
}2.2 Develop a Custom Two‑Factor Module
A custom TOTP implementation can be built by coding the algorithm directly.
@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};
/** Generate 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;
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;
}
}Spring Security configuration for the custom module:
@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 {
TotpFilter filter = new TotpFilter("/check-totp");
filter.setTotpService(totpService);
return filter;
}
}3. Integration and Testing
For Google Authenticator, install the PAM module on Linux, generate a secret, and configure /etc/pam.d/sshd with auth required pam_google_authenticator.so nullok. Then add the custom filter to Spring Security as shown above.
For Authy, register an account, obtain the API key, and use the AuthyTotpFilter to verify tokens before the standard authentication filter.
For the custom SMS‑based factor, use Twilio to send and verify codes. Example Twilio helper class:
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.creator(new PhoneNumber(toPhoneNumber), new PhoneNumber(TWILIO_PHONE_NUMBER), message).create();
}
public static boolean verifyCode(String toPhoneNumber, String code) {
Twilio.init(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN);
List<Message> messages = Message.reader().setTo(new PhoneNumber(toPhoneNumber)).read();
for (Message msg : messages) {
if (msg.getBody().contains(code)) {
return true;
}
}
return false;
}
}Integrate the SMS verifier into a custom SMSAuthenticationProvider that extends AbstractUserDetailsAuthenticationProvider, retrieve the user’s mobile number, send a code, and validate it during authentication.
4. Conclusion
The article demonstrates the concept of two‑factor authentication, its advantages and drawbacks, and provides concrete Spring Security examples for Google Authenticator, Authy, and a self‑implemented TOTP/SMS solution. By following the steps, developers can enhance the security of Java web applications while balancing usability.
Java Interview Crash Guide
Dedicated to sharing Java interview Q&A; follow and reply "java" to receive a free premium Java interview guide.
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.
