3 min read

Implementing Two-Factor Authentication in a Spring Boot application

Categories:

Application development Web development

Padlock on keyboard

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!

Related insights

3 min read

Our journey with Erasmus+

CIVIC has been collaborating in Erasmus+ projects for 7 years now and 2023 marks the year of the end of our journey together. In this blog, we’ll sum up some conclusions and reflections and recap some of our most successful projects.
2 min read

The cost efficiency of investing in your digital presence: a digital agency’s perspective

In the rapidly evolving digital landscape, businesses face a critical decision: to invest in their digital presence or risk falling behind their competitors. As a project manager and an account manager in various digital agencies, I’ve witnessed firsthand how strategic investments in a robust Content Management System (CMS) like Drupal can lead to substantial cost savings and long-term growth. Let's delve into how prioritising your digital presence and structure isn't just an expense, but a smart investment in your business' future.