This is a two-post series in which I will show how to implement two-factor authentication with Spring Security.
In this post, we will cover how to implement user registration for two-factor authentication. Sometimes two-factor authentication is also known as multi-factor authentication (MFA).
Previously, I have covered different Spring Security scenarios. If you want to start with the fundamentals, how spring security filter chain works is a good post to start with.
Two-Factor Authentication
With the advent of web applications, the security of applications and user data has become even more important. Back in the day, a simple username and password form was enough. But that was never secure enough. Adding an additional layer of security to a login form can dramatically improve the application’s security. Two-Factor authentication adds another layer for authentication. Overall, the user enters credentials and if that is validated, the user has to enter a time-based one-time password (TOTP).
Two-Factor authentication is two-step authentication. In the first step, user credentials are verified and in the next step, a one-time password is. How is this one password generated? How user can set up two-factor authentication? What is the password validity duration?
In this post, we will cover the details of the user registration process where a user can register for two-factor authentication.
User Flow for Two-Factor Authentication
As part of user registration, we will be following the user flow shown below.
- The user accesses the application.
- The application shows a login screen.
- If a user is not signed up before, the user selects the registration option.
- The user enters details and chooses to enable MFA (multi-factor authentication).
- Spring Security (as part of our application) will show a QR Code screen.
- Spring Security will assign that secret key (QR Code) to the user profile and store in DB.
- The user scans QR Code on the Google Authenticator app.
That covers the registration flow. Let’s see how we implement this now.
Demo Application
To demonstrate two-factor authentication, we will create a demo application using Spring Boot and Spring Security. This will be a minimal application with a login screen, registration screen and a home screen.
1. Dependency Configuration
We will need some specific dependencies for our application to implement two-factor authentication.
TOTP dependency is
implementation 'dev.samstevens.totp:totp-sprint-boot-starter:1.7.1'
This dependency provides us options to set up QR Code authentication, verify codes, and also recovery codes if you lose your phone for the authenticator app.
Other dependencies for this app will be
dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'dev.samstevens.totp:totp-spring-boot-starter:1.7.1'
implementation 'org.springframework.boot:spring-boot-starter-mail'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.apache.commons:commons-lang3:3.11'
runtimeOnly 'com.mysql:mysql-connector-j'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
}
We are using spring-boot-starter-mail dependency to send confirmation emails when the user signs up. The rest of the dependencies are pretty common if you have built a spring boot application.
2. User Registration
Previously, I stated about the user registration flow. Now, we will implement a registration controller that takes the request from the client.
package com.betterjavacode.twofactorauthdemo.controllers;
import com.betterjavacode.twofactorauthdemo.dtos.MfaTokenDto;
import com.betterjavacode.twofactorauthdemo.dtos.UserDto;
import com.betterjavacode.twofactorauthdemo.exceptions.InvalidTokenException;
import com.betterjavacode.twofactorauthdemo.services.UserService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.MessageSource;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import javax.annotation.Resource;
@Controller()
@RequestMapping("/register")
public class RegistrationController
{
private static final String REDIRECT_LOGIN= "redirect:/login";
@Resource
private UserService userService;
@Resource
private MessageSource messageSource;
@GetMapping
public String register(final Model model){
model.addAttribute("userData", new UserDto());
return "useraccount/register";
}
@PostMapping
public String userRegistration(final UserDto userData, final BindingResult bindingResult,
final Model model) {
if (bindingResult.hasErrors()) {
model.addAttribute("userData", userData);
return "useraccount/register";
}
try {
userService.register(userData);
MfaTokenDto mfaData = userService.mfaSetup(userData.getEmail());
model.addAttribute("qrCode", mfaData.getQrCode());
model.addAttribute("qrCodeKey", mfaData.getMfaCode());
model.addAttribute("qrCodeSetup", true);
} catch (Exception e) {
bindingResult.rejectValue("email", "userData.email","An account already exists for this email.");
model.addAttribute("userData", userData);
return "useraccount/register";
}
model.addAttribute("registrationMsg", "Thanks for your registration. We have sent a " +
"verification email. Please verify your account.Please scan the QR code for generating MFA token for login.");
return "useraccount/register";
}
@GetMapping("/verify")
public String verifyCustomer(@RequestParam(required = false) String token, final Model model, RedirectAttributes redirAttr){
if(StringUtils.isEmpty(token)){
redirAttr.addFlashAttribute("tokenError", "Token is empty");
return REDIRECT_LOGIN;
}
try {
userService.verifyUser(token);
} catch (InvalidTokenException e) {
redirAttr.addFlashAttribute("tokenError", "Token is invalid. Provide a valid token.");
return REDIRECT_LOGIN;
}
redirAttr.addFlashAttribute("verifiedAccountMsg", "Your account is verified. You can " +
"login now");
return REDIRECT_LOGIN;
}
}
You can see two methods in this controller. One is GET to show the registration page and the other one is POST to process form submission from the user.
We are using @Autowired
User Service class UserService
to register users and to set up MFA.
Let’s look at those methods.
@Override
public void register (UserDto user) throws UserAlreadyExistsException
{
if(checkIfUserExist(user.getEmail())){
throw new UserAlreadyExistsException("User already exists for this email");
}
UserEntity userEntity = new UserEntity();
BeanUtils.copyProperties(user, userEntity);
encodePassword(user, userEntity);
userEntity.setSecret(mfaTokenManager.generateSecretKey());
userEntity.setMfaEnabled(true);
userRepository.save(userEntity);
sendRegistrationConfirmationEmail(userEntity);
}
Here, we throw an exception for user already exists if user is already registered. We save user information with userEntity
and assign a secret key (QR Code) for this user. Each user will receive a unique QR Code. This allows linking the user profile with the secret key. We will use this secret key during authentication code verification and I will show this in the next post.
Once the user is created, we send a confirmation email for the user to verify. That’s why we have /verify
method in RegistrationController
.
As part of registration, we also set up MFA.
@Override
public MfaTokenDto mfaSetup (String email) throws UnknownIdentifierException,
QrGenerationException
{
UserEntity user= userRepository.findByEmail(email);
if(user == null ){
throw new UnknownIdentifierException("unable to find account or account is not active");
}
return new MfaTokenDto(mfaTokenManager.getQRCode( user.getSecret()), user.getSecret());
}
We use MFATokenManager to build a QR Code.
@Override
public String getQRCode (String secret) throws QrGenerationException
{
QrData data = new QrData.Builder().label("MFA")
.secret(secret)
.issuer("Two Factor Authentication Demo")
.algorithm(HashingAlgorithm.SHA256)
.digits(6)
.period(30)
.build();
return Utils.getDataUriForImage(
qrGenerator.generate(data),
qrGenerator.getImageMimeType()
);
}
Most of QrGeneration is using the totp library that we are using in this app.
3. Demo
So far, we have shown user registration through code. I have not covered everything in detail, but I will share my github repository with all the code to understand this. As part of demo, we will start the application and you will see the login screen as below:
User accessing the application for first time, will choose Register first time
option.
Once the user enters details and submits the form for registration, the user will see a screen with QR Code for two-factor authentication.
Now, the user can scan the QR Code with Google/Authy Authenticator apps.
That’s all for user registration. When the next time, the user wants to login, they will have to provide TOTP code. We will see this in the next post.
Conclusion
In this post, I showed how to implement user registration for two-factor authentication.
If you are diving into Spring Security and want to learn more, here is my book Simplifying Spring Security which is on a Black-Friday sale currently.