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.
- Regardless of whether you are logged in or not, you MUST re-authenticate with a password before toggling ANY security setting.
- Opting into multi-factor authentication is a multi-step process and SHOULD be time bound.
- 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.