Implementing Two-Factor Authentication in a Spring Boot application
As the number of threats and data breaches increase, so does the need to further secure your web applications and users. One of the most powerful ways to do that's by employing Two-Factor Authentication (2FA). Two-Factor Authentication is the process in which users need to verify themselves via two distinct steps. In the following lines, we'll describe the main parts of how to implement 2FA in a Spring Boot application.
What is Two-Factor Authentication?
As briefly described above, Two-Factor Authentication, also known as 2FA, is a security process in which a user provides two different authentication factors to verify their identity. These factors are usually a password and a mobile device, as well as biometric data like fingerprints and retina scans. In this blog post, we'll stick to a password and a mobile device. By employing these factors, 2FA adds an extra layer of security, which makes it even more challenging for someone to maliciously get access to your application.
In this example, we'll use a TOTP (Time-based One Time Password) library (java-totp), which generates QR codes that are recognisable by applications like Google Authenticator, and verify the one-time passwords they produce.
Step 1: Add the TOTP dependency
In your pom.xml, add the necessary dependencies:
<!-- Time-based one time passwords to enable MFA -->
<dependency>
<groupid>dev.samstevens.totp</groupid>
<artifactid>totp</artifactid>
<version>1.7.1</version>
</dependency>
Step 2: Implement a service to include the logic for Two-Factor Authentication
In this step, we need to put together a service for all the 2FA actions we will need. This service will mainly make use of the TOTP API.
@Service
@RequiredArgsConstructor
public class TwoFactorAuthService {
private final SecretGenerator secretGenerator;
/**
* Generate the secret key for a user
*
* @return the Two-Factor Authentication secret key
*/
public String generateTwoFactorAuthSecret() {
return secretGenerator.generate();
}
/**
* Generate the QR code
*
* @param email identifies the user
* @param twoFactorAuthSecret the Two-Factor Authentication secret key
* @return the QR code
* @throws QrGenerationException a {@link QrGenerationException}
*/
public String getQRCode(String email, String twoFactorAuthSecret) throws QrGenerationException {
QrData data = new QrData.Builder()
.label(email)
.secret(twoFactorAuthSecret)
.issuer("CIVIC UK - TOTP example") // sets the issuer. That's the title that will be used by the Authenticator app
.algorithm(HashingAlgorithm.SHA1) // sets the hashing algorithm to use
.digits(6) // sets how many digits long the generated codes are
.period(30) // sets the time period for codes to be valid for 30 seconds
.build();
return Utils.getDataUriForImage(qrGenerator.generate(data), qrGenerator.getImageMimeType());
}
/**
* Verifies the code entered by the user
*
* @param code the QR code
* @param twoFactorAuthSecret the user's Two-Factor Auth secret
* @return if the code is valid or not
*/
public boolean verifyTotp(String code, String twoFactorAuthSecret) {
return codeVerifier.isValidCode(twoFactorAuthSecret, code);
}
}
Step 3: REST endpoints implementation
Create a new, or use an existing controller for handling 2FA setup and verification. In this controller, we'll mainly need two endpoints: one to enable 2FA and another one to verify 2FA.
@PostMapping("/enable2fa")
public ResponseEntity enable2FA(@AuthenticationPrincipal UserDetailsImpl userDetails) throws QrGenerationException {
...
// generate and set the secret to user
user.setTwoFactorAuthSecret(twoFactorAuthService.generateTwoFactorAuthSecret());
...
return ResponseEntity.ok().body(response);
}
@PostMapping("/verify2fa")
public ResponseEntity verify2FA(@AuthenticationPrincipal UserDetailsImpl userDetails, @RequestBody UserDTO userDTO) {
...
// verify verification code with user's secret and return true or false
String verificationCode = userDTO.getTwoFactorVerificationCode();
String twoFactorAuthSecret = user.getTwoFactorAuthSecret();
return ResponseEntity.ok().body(twoFactorAuthService.verifyTotp(verificationCode, twoFactorAuthSecret));
}
Within the enable2FA method, we generate and store a QR code and a secret for the user to scan. The verify2FA method is used to verify the provided code from the authenticator app against the user's secret.
Step 4: Login
In this example the use of 2FA is optional, so we let users decide whether to enable it or not. For this reason, we should always take this detail into consideration and handle each case separately. To do that we need to extend Spring's DaoAuthenticationProvider and conditionally include our 2FA logic.
public class CustomDaoAuthenticationProvider extends DaoAuthenticationProvider {
...
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
...
// Users with 2FA disabled, no checks to be done
if (!Boolean.TRUE.equals(principal.getTwoFactorAuthEnabled())) {
return;
}
// 2FA Users - Validate TOTP
String twoFaToken = authenticationDetails.getVerificationCode();
if (twoFaToken == null || !twoFactorAuthService.verifyTotp(twoFaToken, principal.getTwoFactorAuthSecret())) {
throw new InvalidTOTPException("Invalid TOTP");
}
...
}
...
}
In the above class, we override the additionalAuthenticationChecks and examine if the user who tries to login has 2FA enabled or not. If it's enabled, a verification code is also expected, which is verified against the user's two-factor secret.
Step 5: Relax...
...you're much more secure than before!