Category Archives: Programming

Best Practices for Securing Spring Security Applications with Two-Factor Authentication

As more and more sensitive information is being shared online, it is becoming increasingly important to have strong security measures in place to protect against unauthorized access. One way to enhance the security of web applications is to implement two-factor authentication (2FA) in addition to traditional username and password authentication. In this article, we’ll explore best practices for securing Spring Security applications with 2FA.

What is Two-Factor Authentication?

Two-factor authentication (2FA) is a security process that requires users to provide two different authentication factors to access an application. These factors typically fall into three categories:

  1. Something the user knows, such as a password or PIN
  2. Something the user has, such as a smart card or mobile device
  3. Something the user is, such as a biometric identifier like a fingerprint or facial recognition

By requiring two different authentication factors, 2FA adds an extra layer of security to traditional username and password authentication.

Why Use Two-Factor Authentication in Spring Security Applications?

Implementing 2FA in Spring Security applications can greatly enhance the security of your application. Here are some reasons why:

  1. Protection against password guessing attacks: Attackers may use automated scripts to guess passwords until they gain access to an account. 2FA adds an extra layer of security, making it more difficult for attackers to gain access.
  2. Compliance with security regulations: Many industries have regulations that require 2FA, such as the Payment Card Industry Data Security Standard (PCI DSS).
  3. Enhanced user trust: By providing an additional layer of security, users will have more trust in your application, which can lead to increased usage and adoption.

Best Practices for Implementing Two-Factor Authentication in Spring Security Applications

Here are some best practices for implementing 2FA in Spring Security applications:

1. Choose a secure 2FA method

When implementing 2FA, it is important to choose a secure method for the second factor. Some common 2FA methods include:

  • SMS authentication: Sends a unique code to the user’s mobile phone via SMS
  • Mobile authentication app: Users install an app on their mobile device to generate a unique code
  • Email authentication: Sends a unique code to the user’s email address

Each of these methods has its pros and cons. It is important to choose a method that is secure and convenient for your users.

2. Use a separate authentication provider

When implementing 2FA, it is recommended to use a separate authentication provider for the second factor. This provider should be separate from the authentication provider used for the initial login. This helps prevent attackers from gaining access to both factors.

3. Store sensitive data securely

Sensitive data, such as user passwords and 2FA codes, should be stored securely. This includes hashing and salting passwords and encrypting 2FA codes.

4. Provide clear instructions to users

Users may be unfamiliar with 2FA and may need clear instructions on how to use it. It is important to provide clear and concise instructions to users to help them understand how to use 2FA.

5. Test thoroughly

Before implementing 2FA in a production environment, it is important to test thoroughly to ensure that it is working correctly. This includes testing for usability, security, and performance.

Conclusion

In this post, I shared some of the best practices that one can follow while implementing two-factor authentication (2FA) in a Spring Security application. You can learn more about Spring Security in my book Simplifying Spring Security.

Building a Scalable NestJS API with AWS Lambda

In this post, we show an example of building a scalable NestJS API with AWS Lambda.

If you are not familiar with the NestJS framework, I would recommend going through their documentation. NestJS is a popular Node.js framework for building scalable and efficient web applications. AWS Lambda is a serverless computing service that enables developers to run code without provisioning or managing servers. We will combine the two technologies to build a scalable and cost-effective API.

Setting up NestJS Project

To set up a NestJS project, we first need to install the NestJS CLI using the following command:

npm install -g @nestjs/cli

Once the CLI is installed, we can create a new project using the following command:

nest new nestjsapi-demo

This will create a new NestJS project in a directory named nestjsapi-demo. We can then navigate to this directory and start the development server using the following command:

npm run start:dev

AWS Lambda Support

To add AWS Lambda support to our NestJS project, we need to install the aws-serverless-express package using the following command:

npm install aws-serverless-express @types/aws-serverless-express @types/aws-lambda

This package allows us to run our NestJS application on AWS Lambda using the Express framework.

To eventually be able to use AWS Lambda resources, we will need an AWS account. Here I assume that you have an AWS account and be able to use your credentials to deploy AWS resources. You can configure your credentials in your local machine with aws configure command. This should store your credentials in .aws file

aws_access_key_id = YOUR_ACCESS_KEY
aws_secret_access_key = YOUR_SECRET_ACCESS_KEY

Creating an AWS Lambda Function

When you create a serverless project, it generates an API gateway by default. Each serverless needs an entry point to build a NestJS project to a lambda function.

To create an AWS Lambda function, we need to create a new file called lambda.ts in the root of our project directory. We can then add the following code to the file:


import { Handler } from 'aws-lambda';
import { createServer, proxy } from 'aws-serverless-express';
import { AppModule } from './app.module';
import * as express from 'express';
import { NestFactory } from '@nestjs/core/nest-factory';
import { ExpressAdapter } from '@nestjs/platform-express';
import { Server } from 'http';

const server = express();

async function bootstrap(): Promise {
  const app = await NestFactory.create(AppModule, new ExpressAdapter(server));
  app.enableCors();
  await app.init();
  return createServer(server);
}

let cachedServer: Server;

export const handler: Handler = async (event, context) => {
  if (!cachedServer) {
    cachedServer = await bootstrap();
  }
  return proxy(cachedServer, event, context, 'PROMISE').promise;
};

This code creates an instance of our NestJS application and creates an AWS Lambda handler function that proxies requests to our application.

We will need to update our tsconfig.lambda.json file to include where the lambda function is.

{
    "extends": "./tsconfig.json",
    "compilerOptions": {
      "module": "commonjs",
      "outDir": "./dist-lambda",
      "noEmit": false
    },
    "include": ["src/lambda.ts"]
}

Adding scripts to build and deploy

Once we create a lambda entry point, we can configure serverless to build this lambda. Let’s create a YAML file serverless.yml

 


service: nestjsapi-demo

plugins:
 - '@hewmen/serverless-plugin-typescript' 
 - serverless-plugin-optimize
 - serverless-offline
 - serverless-plugin-warmup

provider:
 name: aws
 runtime: nodejs14.x

functions:
 main: # The name of the lambda function
   # The module 'handler' is exported in the file 'src/lambda'
   handler: src/lambda.handler
   events:
     - http:
         method: any
         path: /{any+}

We need those serverless plugins to be able to build this project and create a Lambda main function for our entry point.

@hewmen/serverless-plugin-typescript: Serverless plugin for Typescript support that works out of the box without the need to install any other compiler or plugins.
serverless-plugin-optimize: Plugin to transpile and minify your code
serverless-offline plugin: Plugin to be able to test your app offline.
serverless-warmup plugin:  This plugin solves cold-start by creating a scheduled lambda.

We will need a couple of scripts to build and deploy our NestJS API app to AWS Lambda.

    "build-lambda": "tsc --project tsconfig.lambda.json",
    "deploy-lambda": "sls deploy",

build-lambda script will let us build our nestJS typescript project.

Deploying NestJS application to AWS Lambda

To deploy our NestJS API to AWS Lambda, we need to first create a new AWS Lambda function using the AWS Console. Once the function is created, we can upload our lambda.ts file as the function code.

We also need to configure the function’s handler to be lambda.handler and set the runtime to Node.js 14.x.

We will run our script npm run deploy-lambda and that should build and deploy lambda to AWS Lambda resource. (Make sure you have configured your AWS credentials to be able to deploy this).

Probably, you might come across this error when deploying the lambda –

× Stack nestjsapi-demo-dev failed to deploy (0s)
Environment: win32, node 14.19.0, framework 3.28.1, plugin 6.2.3, SDK 4.3.2
Credentials: Local, "default" profile
Docs:        docs.serverless.com
Support:     forum.serverless.com
Bugs:        github.com/serverless/serverless/issues

Error:
Error: [{"messageText":"Unknown compiler option 'incremental'.","category":1,"code":5023},{"messageText":"Unknown compiler option 'strictBindCallApply'.","category":1,"code":5023}]

One way to fix this issue is to remove – incremental: true option from tsconfig.json

Overall, once you run npm run deploy-lambda , you will see your lambda deployed in  AWS like below:

Nest JS API with AWS Lambda

Testing our AWS Lambda Function

To test our AWS Lambda function, we can use the AWS Lambda Console or the AWS CLI. We can invoke the function using the following command:

aws lambda invoke --function-name function-name --payload {} response.json

This command will invoke our function and store the response in a file called response.json.

Conclusion

In this blog post, we discussed how to build a scalable NestJS API with AWS Lambda. By combining the power of NestJS and AWS Lambda, we can build a scalable and cost-effective API that can handle large amounts of traffic. With the steps outlined in this blog post, you should now be able to create your own NestJS API with AWS Lambda support.

Technical Debt Explained

Tech Debt aka technical debt is part of the software engineering development process. This is also known as Code debt sometimes. The immediate question that pops up is all tech debt bad?

The simple answer to that question is – It depends.

Some tech debt is intentional and some is unintentional. As we explore this topic as part of the software engineering development process, it is necessary to understand every developer has to pay back the debt at some point in time.

What is Tech Debt?

Technical debt is a quick and dirty solution to a software problem. In many cases, to launch an MVP (minimum viable product), engineers come up with a quick solution. It’s not necessarily that a quick solution is bad. But if it is not thoroughly tested in real-world scenarios, it can cause some downstream effects. Is all technical debt bad? Technical debt gives a sense that it is bad. But it all depends on the scenario you are working on.

If you are implementing a solution quickly, it might align temporarily with the expectation, but might not always be right. This is where technical debt comes into the picture. Recognizing technical debt is necessary. Addressing the debt depends on the situation.

You can not prevent technical debt. In many cases, you do not have all the information when you are building a solution and you have to make assumptions.

Bad code is not necessarily a tech debt. But it can incur a cost in long term and that would end up being a tech debt.

Types of Tech Debt

Let’s look at different types of tech debts.

Intentional Tech Debt

To release a product to market early, many times engineers and stakeholders reduce the scope of the requirements. In many cases, everyone involved had to make choices, and that ends up causing an intentional tech debt. To get user feedback quickly, the product makes such a choice and accepts the fact the performance can be poor, the product can be unstable. There is an inherent risk involved with intentional tech debt.

Unintentional Tech Debt

On the other spectrum is unintentional tech debt. Most of this tech debt arises from the complexity of requirements and products. Product managers, domain experts, and engineers make assumptions in case of complexity. With unknowns involved, it becomes really hard to predict what can cause issues with the product. In such cases, engineering teams cut corners, and test the product insufficiently. If teams can recognize this debt, they can address it. Engineering teams have to adjust their development process to address this debt.

Environment Tech Debt

When engineers build a software product, it involves multiple third parties like libraries, other vendor products, or software. As time progresses, if engineers don’t keep the product up-to-date with various components, the product can start accruing an environmental tech debt. The most serious of tech debt in such cases can be a security threat. If engineers don’t patch libraries on time, security vulnerabilities can expose the product to risk. This kind of debt is recognizable over time and the teams involved need to remain on top.

The Effects of Tech Debt

There can be effects of tech debt both business-wise or financially.

  • Poor user experience
  • Security concerns and issues
  • Reduced time for feature development
  • Possible loss of customers
  • Lower productivity

Managing Tech Debt

As previously stated, engineers need to address tech debt at some point in time. How do you manage tech debt then?

  1. Make time to address tech debt. Once tech debt is identified, engineering teams should make time to address tech debt to avoid the long-term effect.
  2. Separate good debt and bad debt. Address bad debt sooner. Evaluate if good debt is to be kept in the system for a long.
  3. Measure the impact of tech debt. If you can create performance metrics around tech debt, measure the performance. This gives clarity on how the debt is affecting and why it needs to be addressed.
  4. Adopt the new processes and quality reviews to avoid future tech debt.

Conclusion

In this post, we talked about technical debt and the kind of impact it can create on user experience and business. As an engineer, it can take the experience to identify this debt. But always make sure to make time to address the tech debt.

If you want to learn more about how to implement two-factor authentication with spring security, here are my previous two posts

  1. How to implement two-factor authentication with Spring Security – Part I
  2. How to implement two-factor authentication with Spring Security – Part II

Two Factor Authentication with Spring Security – Part II – How To

With security threats rising, it becomes important to secure accounts. In my previous post, I showed how to sign up for two-factor authentication with spring security.

In this post, I show how to log in with two-factor authentication with spring security.  Before a user can log in, the user needs to register for the application. Assuming you followed my previous post, we have a user who has registered for multi-factor authentication (MFA).

If you are getting started with Spring Security, I would recommend Simplifying Spring Security.

User flow for authentication

User will access our application and if not logged in, will get redirected to the login page. The login page looks like below:

User will have to enter a username, password, and a token from the GoogleAuthenticator App for this particular application.

In a previous post, I assume you register the application with GoogleAuthenticator App.

On our login page, we pass an extra parameter of token to the backend to verify.

The architecture of the Login Process

To understand the entire login process with Spring Security, it is good to see the overall picture of this process.

As we know, Spring Security works with filter chains. One of the filters UsernamePasswordAuthenticationFilter is used in Username and Password authentication flow. In this login flow as well, our authentication starts with that filter. Once the user has entered credentials and the token, it will pass through that filter.

We will need to implement a UserDetailsService to fetch user. This service is part of AuthenticationManager that UsernamePasswordAuthenticationFilter provides.

This UserDetailsService will load the user as follows:


    @Override
    public UserDetails loadUserByUsername (String email) throws UsernameNotFoundException
    {
        final UserEntity customer = userRepository.findByEmail(email);
        if (customer == null) {
            throw new UsernameNotFoundException(email);
        }
        LOG.info("Getting User", customer);
        
        CustomUser user = CustomUser.CustomUserBuilder.aCustomUser().
                withUsername(customer.getEmail())
                .withPassword(customer.getPassword())
                .withAuthorities(getAuthorities(customer))
                .withSecret(customer.getSecret())
                .withAccountNonLocked(false)
                .build();

        return user;
    }

Additionally, we call WebAuthenticationDetails implementation to build authentication details from the HttpServletRequest object. We will implement this interface with CustomWebAuthenticationDetails as follows:

public class CustomWebAuthenticationDetails extends WebAuthenticationDetails
{
    private String token;

    public CustomWebAuthenticationDetails (HttpServletRequest request)
    {
        super(request);
        this.token = request.getParameter("customToken");
    }
    @Override
    public String toString() {
        return "CustomWebAuthenticationDetails{" +
                "token='" + token + '\'' +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        if (!super.equals(o)) return false;
        CustomWebAuthenticationDetails that = (CustomWebAuthenticationDetails) o;
        return Objects.equals(token, that.token);
    }

    @Override
    public int hashCode() {
        return Objects.hash(super.hashCode(), token);
    }

    public String getToken() {
        return token;
    }

    public void setToken(String token) {
        this.token = token;
    }

}

Furthermore, we fetched the customToken from our request and set in authentication details.

We have validated user credentials, fetched the user from the database, and also set the token. Moreover, all that is left is to validate if the token is still valid.

We implement a CustomAuthenticationProvider from DaoAuthenticationProvider to fetch user details and validate TOTP token. This looks like below:


@Component
public class CustomAuthenticationProvider extends DaoAuthenticationProvider
{

    private static final Logger LOG = LoggerFactory.getLogger(CustomAuthenticationProvider.class);
    @Resource
    private MfaTokenManager mfaTokenManager;

    @Resource
    private PasswordEncoder passwordEncoder;

    @Autowired
    public CustomAuthenticationProvider(UserDetailsService userDetailsService) {
        super.setUserDetailsService(userDetailsService);
    }

    protected void additionalAuthenticationChecks(UserDetails userDetails,
                                                  UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException
    {

        super.additionalAuthenticationChecks(userDetails, authentication);

        CustomWebAuthenticationDetails authenticationDetails = (CustomWebAuthenticationDetails) authentication.getDetails();
        CustomUser user = (CustomUser) userDetails;
        String mfaToken = authenticationDetails.getToken();

        if(!mfaTokenManager.verifyTotp(mfaToken,user.getSecret())){
            throw new BadCredentialsException(messages.getMessage(
                    "AbstractUserDetailsAuthenticationProvider.badCredentials",
                    "Bad credentials"));
        }
    }
}

 

MfaTokenManager calls TOTP library to verify the code. Nevertheless, take note that we are passing one time token along with the user secret that we created when the user registered for the application and scanned the QR Code.

If token validation is successful, SecurityConfiguration will proceed with calling CustomLoginSuccessHandler . This handler redirects the user with a right role to /home page.

This completes our two-factor authentication login flow with Spring Security.

If you want to learn more details about Spring Security Filters, you can read more about how spring security filter chain works.

Conclusion

In this post, I showed how to use Spring Security for the Two-Factor Authentication Login process. If you have feedback for this post, please post your comment.

Two Factor Authentication with Spring Security – How To

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.

  1. The user accesses the application.
  2. The application shows a login screen.
  3. If a user is not signed up before, the user selects the registration option.
  4. The user enters details and chooses to enable MFA (multi-factor authentication).
  5. Spring Security (as part of our application) will show a QR Code screen.
  6. Spring Security will assign that secret key (QR Code) to the user profile and store in DB.
  7. 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.