Multi Factor Authentication

Multi factor authentication has become ubiquitous in web applications. If you work in financial services it’s even a legal requirement in some states. For most of us working professionals I believe we all agree enhanced security for our customers is a good thing. While we have already tackled broader approaches, such as encryption and server hardening, we also want to cover more fine grained approaches that affect an individual’s experience.

For our implementation we are going to focus on Time-Based One-Time Password (TOTP) where a user will supply a generated verification code. Frequently, the user experience for this method will request the user to submit their username and password and then direct them to a separate page where they are asked to enter their verification code. While our implementation will support this flow, for TOTP it isn’t necessary. Since the TOTP is generated based on the current moment, it can be submitted at the same time as the username and password. The reason you normally see it done on a separate page is most likely due to password manager use. Most password managers will fill in the username/password and automatically press ‘submit’ on behalf of the user.

As a first step, we need to update our registration system to require 2-factor authentication for our new users as well as send them a QR code that can be loaded into Google Authenticator. We’ll be using a third party library to help us out, so first things, first. Add the following library to your pom.xml

<!-- TOTP multi-factor authentication support -->
<dependency>
    <groupId>com.warrenstrange</groupId>
    <artifactId>googleauth</artifactId>
    <version>1.4.0</version>
</dependency>

In our AuthController let’s add the following code to create a secret that will be used as the seed to generate our verification codes. We’ll save this secret on our server so we can confirm verification codes later.

// Create a TOTP secret that can be used for two-factor authentication.
GoogleAuthenticatorKey googleAuthenticatorKey = googleAuthenticator.createCredentials();
user.setSecret2FA(googleAuthenticatorKey.getKey());
// Default the user to using 2-factor auth.
user.setUsing2FA(true);

We also need to update our new user registration e-mail to include steps on how to set up 2-factor auth on our client’s phone.

StringBuilder body = new StringBuilder();
body.append("Hello World \n Spring Boot Email.\n Please verify your account by visiting " + "/api/auth/verifyRegistration/").append(registrationToken.getToken());
body.append("\n\nYour two factor authentication secret is: ").append(user.getSecret2FA());
body.append("\n\nYou can also scan this QR code into Google Authenticator\n");
String format = "https://www.google.com/chart?chs=200x200&cht=qr&chl=otpauth://totp/%s:%s?secret=%s&issuer=%s";
body.append(String.format(format, "99_Miles_to_Empty", emailAddress, user.getSecret2FA(), "99%20Miles%20to%20Empty"));

Once the user completes registration they can then login by submitting their username, password and verification code. We will verify all this in our CustomAuthenticationProvider.

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
...
Authentication result = null;
if (userDetails.isEnabled()) {
    // Check to see if the password matches
    if (passwordEncoder.matches(password, userDetails.getPassword())) {
        // Check to see if this user requires 2-factor authentication
        if (userDetails.isUsing2FA()) {
            // Validate the verification code.
            TOTPAuthenticationToken customAuthenticationToken = (TOTPAuthenticationToken) authentication;
            int verificationToken = customAuthenticationToken.getOneTimePassword();

            boolean isVerificationTokenValid =
                    googleAuthenticator.authorize(userDetails.getSecret2FA(), verificationToken);

            if (isVerificationTokenValid) {
                Collection<? extends GrantedAuthority> grantedAuthorities = userDetails.getAuthorities();
                result = new UsernamePasswordAuthenticationToken(userDetails, password, grantedAuthorities);
                logger.info("Successful 2-factor auth login for user '{}' from {}", name,
                        request.getRemoteAddr());
            } else {
                logger.warn("Failed 2-factor login for user '{}' from {}", name, request.getRemoteAddr());
            }
        } else {
            // Login user with just username and password since
            // 2-factor auth is not enabled for this account
            ...
        }
    } else {
        logger.warn("Failed login for user '{}' from {}", name, request.getRemoteAddr());
    }
} else {
    logger.warn("Failed login for disabled user '{}' from {}", name, request.getRemoteAddr());
}

return result;

With these three modifications to our code we now have basic support for 2-factor auth. Adding this feature isn’t a silver bullet for security though. We are quite vulnerable to a brute force attack on our endpoints. In a future post, we’ll discuss how to mitigate this type of attack by throttling requests to our endpoints.

For the full code up to this point, please download it from GitHub at https://github.com/joutwate/99milestoempty.

Also, if you are looking for hosting please consider Dreamhost. If you’d like to sign up with them and feel inclined to throw me some credit, please use my referral link https://www.dreamhost.com/r.cgi?571777.

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.