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.
