MFA (Take Two)

It’s been a while since I’ve written a post. I’ve been heads down working on the platform thinking more about use cases, revising code and introducing a basic React client. If I haven’t mentioned already, I love IntelliJ. It’s my tool of choice and have been using it for years. Every time they make a release they introduce something cool that is helpful.

Ok, back to the platform. After I wrote the first post on multi-factor authentication, I began filling out more of a full user registration flow where a user comes to the site, creates an account and then verifies it via an e-mail link. I really REALLY wanted to have people opted in by default to use MFA. I set the code up to do this so that when a user verified their registration I e-mailed them their secret QR code to scan into Google Authenticator or Authy that they could easily scan in. As I looked at this beautiful e-mail (yes, beauty is in the eye of the beholder) I thought back to a business I worked for where we asked people to use MFA and paused…

Whisked away into thoughts about the not so distant past I know MFA is still hard for many people. Many smart people who care about security don’t even turn it on. As much as I want this platform I’m writing to be secure I want to protect my users even more and I’m also cognizant that I can’t force people to do it. What I can do is provide users options and incentives to become more secure. Give them small steps to accomplish to help them protect themselves and have a platform that is flexible enough to support these use cases.

So… I revised my login code to have a normal username and password login flow and then added support to allow a user to opt into multi-factor authentication. After looking at some other solutions build out there by bigger people (I’m looking at you Google) I came upon some basic rules for updating security settings.

  1. Regardless of whether you are logged in or not, you MUST re-authenticate with a password before toggling ANY security setting.
  2. Opting into multi-factor authentication is a multi-step process and SHOULD be time bound.
  3. Opting out of multi-factor authentication is a single step process.

Here’s the code that follows the rules set above and allow for a user to enable or disable their multi-factor authentication setting.

@PostMapping(value = "/user/multifactorauth")
public ResponseEntity<?> updateMultiFactorAuth(@Valid @RequestBody MultiFactorPreferenceRequest request) {
    // Get the logged in user.
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    String username = authentication.getName();
    CustomUserDetails userDetails = (CustomUserDetails) userDetailsManager.loadUserByUsername(username);
    User user = userDetails.getUser();

    TOTPAuthenticationToken authToken = new TOTPAuthenticationToken(username, request.getPassword(),
            request.getVerificationCode());


    // Password must be valid to update any security setting.
    if (!authenticationProvider.authenticatePassword(authToken)) {
        return ResponseEntity.badRequest().build();
    }

    // User is in the process of setting up multi-factor auth. Validate their verification code and enable
    // 2-factor auth or return a bad request response.
    if (request.isEnable2FA() && !userDetails.isUsing2FA() && userDetails.getSecret2FA() != null &&
            authToken.getOneTimePassword() != null) {
        // Get the verification code they sent and verify it.
        if (!googleAuthenticator.authorize(userDetails.getSecret2FA(), authToken.getOneTimePassword())) {
            return ResponseEntity.badRequest().build();
        }

        // Go ahead and update multi-factor setting based on request.
        user.setUsing2FA(true);
        userDetailsManager.updateUser(userDetails);

        Map<String, String> map = new HashMap<>();
        map.put("firstName", user.getFirstName());
        emailService.sendEmail(user.getEmail(),
            "no-reply@99milestoempty.com", "99 Miles to Empty Multi-factor Enabled", map, MULTI_FACTOR_ENABLED);

        return ResponseEntity.ok().build();

        // TODO: Send success email for setting up multi-factor auth.
    }

    // If the request is to enable multi-factor, verify that the user doesn't already have it enabled,
    // generate a new secret token and return it. A subsequent call to this endpoint will be made to verify
    // that the user can generate a verification code from that secret and then we will enable MFA.
    if (request.isEnable2FA() && !user.isUsing2FA()) {
        if (user.getSecret2FA() == null) {
            // User does not have multi-factor auth and is requesting to enable it.

            // Create a TOTP secret that can be used for two-factor authentication.
            GoogleAuthenticatorKey googleAuthenticatorKey = googleAuthenticator.createCredentials();
            user.setSecret2FA(googleAuthenticatorKey.getKey());
            // Save the changes
            userDetailsManager.updateUser(userDetails);

            Map<String, String> map = new HashMap<>();
            map.put("firstName", user.getFirstName());
            map.put("secret", user.getSecret2FA());

            // TODO: Send email to let user know that a request has been made to enable multi factor auth for their account.

            // Return the secret. Client application can show user a QR code to scan in Authy or Google Authenticator.
            return ResponseEntity.ok(map);
        }
    } else if (!request.isEnable2FA()) {
        // Disable multi-factor auth.
        user.setSecret2FA(null);
        user.setUsing2FA(false);
    }

    return ResponseEntity.badRequest().build();
}

This code has not yet been integrated into the example project 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. Required fields are marked *

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