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:

Two-Factor Authentication Login Spring Security

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.

Two-Factor Authentication Login Flow Spring Security

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.

Two Factor Authentication Spring Security

  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:

Two Factor Authentication Login - Spring Security

User accessing the application for first time, will choose Register first time option.

Two Factor Authentication Registration

Once the user enters details and submits the form for registration, the user will see a screen with QR Code for two-factor authentication.

Two Factor Authentication - QR Code

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.

Communication Patterns between Microservices

In this post, I will cover different communication patterns between microservices. As microservices have become more industry pattern, it has also become important how these microservices communicate with each other.

The most common pattern for communication has been synchronous REST API calls. But this pattern comes with its own set of trade-offs. Another pattern is asynchronous communication. Let’s dive deeper into these patterns.

Synchronous Calls

The most standard way for services to communicate between themselves is through HTTP synchronous calls. One source service calls a target service. Target service returns with a response that the source service uses for further processing.

In many web applications, a client (front end) calls a backend service (microservice) to fetch or create data.

Synchronous Communication Pattern

Similarly, two microservices can communicate with each other through HTTP. Most frameworks provide an HTTP library that allows one service to call another service. For Example – Axios or Feign.

Challenges with Synchronous Communication

Timeout

If service A calls service B to fetch data, but service B takes forever to respond, service A can time out. What happens if the call is going to cause some side-effect on the service B side? In that scenario, there will be data inconsistencies between both services.

Strong coupling

Synchronous communication between services can create strong coupling between services and can be detrimental to microservice architecture overall. Loose coupling was one of the main features of microservices. If any of the services are down, the dependent services might not work the way they were intended.

Circuit Breakers or Retry are some of the ways these challenges can be overcome.

Asynchronous Calls

With event-driven architecture, asynchronous communication has become more popular. One service publishes a message and another service consumes that message. This does not necessarily happen in real-time. Service A publishes a message and still continues to function without knowing in that moment if other services have consumed that message. Consumer services consume the earlier published message when they are ready.

Asynchronous Communication

Usually, these services use message-broker to publish and consume the message from. These services may not know each other and offer the advantage of loose coupling.

Another advantage of asynchronous calls is that the message broker offers a retry mechanism. In the scenario, the consumer did not receive the message, the message can be republished.

Challenges with Asynchronous Communication

Message broker

With asynchronous communication, we introduce a centralized entity message broker. If a message broker is down, there will not be any communication between services.

Schema changes

If the publishing service changes the message schema, it can break consumer service unless there is backward compatibility. This defeats the purpose of microservices which allow independent deployments.

Two-phase commit

Publisher service publishes the message as part of business logic. In most cases, it does this by first committing a transaction and then publishing a message. It must perform this action with two-phase commit. But for whatever reason, if the transaction fails and rolled back, then we are in soup since the message has already been published.

In such cases, it is best to avoid a two-phase commit and store the messages in a database on both sides publisher as well consumer.

When to use these patterns?

It’s not very clear when to use Synchronous or Asynchronous calls. When you start designing a system, you will have to make calls and take into account all the trade-offs. Irrespective of that, one can follow certain notions about when to use these patterns

  • If you want to query data from another service and want to use that data immediately, use Synchronous communication.
  • If a call to another service is allowed to fail and does not bring down the calling service or any dependent services, you can use Synchronous communication without any fancy retry mechanism.
  • If a service wants to change the state of certain business logic, in such a scenario, it can publish a message with an Asynchronous communication pattern.
  • Two services involved in a business logic do not need to perform the action immediately or do not depend on the results of the action.

Conclusion

In this post, I discussed the communication patterns of microservices. In synchronous communication patterns, one can use HTTP or gRPC protocols. In asynchronous communication patterns, one can use a message broker for publishing and subscribing to messages.

If you are still interested to read about Spring Security, here is my book Simplifying Spring Security.

Basic Authentication with Passport in NestJS Application

In this post, I will show how to implement basic authentication using Passport.js in a NestJS Application. So far in the NestJS series, we have covered

Basic Authentication

Basic authentication though not secure for production applications, has been an authentication strategy for a long time.

Usually, a user accesses the application and enters a username and password on the login screen. The backend server will verify the username and password to authenticate the user.

There are a few security concerns with basic authentication

  • Password entered is plain text. It all depends on how the backend is handling the verification of passwords as well as the storage of passwords. The recommended way is to store the hash of the password when the account is created. Hashing is a one-way mechanism. So we will never know user passwords and if a database is breached, we won’t be exposing passwords.
  • If we don’t use re-captcha mechanism, it is easy for attackers to attack with a DDOS attack.

Passport Library

We will use Passport library as part of this demo. Passport is authentication middleware for Node JS applications. NestJS Documentation also recommends using the passport library.

As part of using Passport library, you will implement an authentication strategy (local for basic authentication OR saml for SAML SSO).

In this implementation, we will implement a method validate to validate user credentials.

Let’s create a project for this demo and we will create two separate directories for frontend (ui) and backend.

Frontend application with React

In our ui directory, we will use reactjs framework to build the UI. If you are using react-scripts, we will start with

npx create-react-app ui

Create the login page

Once we have react app created, we have the bare bones of the app to make sure it is running. Now, we will add a login page where the user will enter credentials.

We will need two libraries in the login page Signin.js

  • axios to call backend API
  • useNavigation to navigate to different pages.

handleSubmit is a function that we will call when a user submits the form on the login screen.


  const handleSubmit = async (event) => {
    event.preventDefault();
    const formData = new FormData(event.currentTarget);
    const form = {
      email: formData.get('email'),
      password: formData.get('password')
    };
    const { data } = await axios.post("http://localhost:3000/api/v1/user/signin", form);
    console.log(data);
    if (data.status === parseInt('401')) {
      setErrorMessage(data.response)
    } else {
      localStorage.setItem('authenticated', true);
      setIsLoggedIn(true)
      navigate('/home')
    }

  };

Once the user submits the form, handleSubmit collects submitted data and calls backend API with that form data.

User sign up

The sign in page can be of less help if there is no for users to sign up. Of course, it all depends on your user flow.

On the user signup page, we will be asking for firstName, lastName, email and password. Similar to signin page, we will have a handleSubmit function that will submit the signup form. It will call the backend API for signup.


  let navigate = useNavigate();
  const handleSubmit = async (event) => {
    event.preventDefault();
    const data = new FormData(event.currentTarget);
    console.log(data);
    const form = {
      firstName : data.get('fname'),
      lastName: data.get('lname'),
      email: data.get('email'),
      password: data.get('password')
    };
    await axios.post("http://localhost:3000/api/v1/user/signup", form);  
    navigate('/')
  };

We will call this function handleSubmit on the event call onSubmit

Box component="form" noValidate onSubmit={handleSubmit} sx={{ mt: 3 }}

As far as the Home page is concerned, we have a simple home page that shows Welcome to Dashboard. The user will navigate to the home page if authenticated successfully.

Backend application with NestJS

Let’s look at the backend side of this application. I will not show the basics of creating a NestJS app and setting up Prisma as ORM. You can follow those details here .

We will create a user table as part of the Prisma setup.


// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

model User {
  id         String     @id @default(uuid())
  email      String  @unique
  first_name String
  last_name  String?
  password   String
  createdAt  DateTime    @default(now())
  updatedAt  DateTime    @updatedAt
}

When a user signs up for our application, we will store that information in User table.

Controller for backend APIs

We will need two APIs – one for signup and one for sign-in. We already showed in the frontend section about the sign-up page. When the user submits sign-up page, we will call sign-up API on the backend.


  @Post('/signup')
  async create(@Res() response, @Body() createUserDto: CreateUserDto) {    
    const user = await this.usersService.createUser(createUserDto);
    return response.status(HttpStatus.CREATED).json({
      user
    });
  }

The frontend will pass an object for CreateUserDto and our UsersService will use that DTO to create users with the help of a repository.

Controller -> Service -> Repository.

Service performs the business logic and the repository interacts with the database.

 


import { Injectable } from '@nestjs/common';
import { PrismaService } from 'src/common/prisma.service';
import { CreateUserDto } from './dtos/create-user.dto';
import * as bcrypt from 'bcryptjs';
import { UserRepository } from './user.repository';
import { User } from './entities/user.entity';

@Injectable()
export class UsersService {
    constructor(private readonly prismaService: PrismaService, private readonly userRepository: UserRepository) {}
   
    async createUser(user: CreateUserDto) {
        const hashedPassword = await bcrypt.hash(user.password, 12);

        const userToBeCreated = User.createNewUser({            
            firstName: user.firstName,
            lastName: user.lastName,
            email: user.email,
            password: hashedPassword,
        });
        return await this.userRepository.save(userToBeCreated);
    }

   async findById(id: string) {
    return await this.userRepository.findById(id);    
   }

   async getByEmail(email: string) {
    const user = await this.userRepository.findByEmail(email);

    return user;
   }
}

As you can see above, while creating a new user, we are storing the hash of the password in the database.

And here is the other API for user signin.

  @UseGuards(LocalAuthGuard)
  @Post('/signin')
  async signIn(@Req() request: RequestWithUser) {    
    const user = request.user;    
    return user;

  } 

I will explain this API in detail soon.

Add Passport and Authentication Strategy

We already discussed the Passport library. Add the following libraries to your backend application:


npm install @nestjs/passport passport @types/passport-local passport-local @types/express

We will use a basic authentication mechanism for our application. Passport calls these mechanisms as strategies. So, we will be using the local strategy.

In NestJS application, we basically implement the local strategy by extending PassportStrategy.

import { Injectable, } from "@nestjs/common";
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-local';
import { User } from "src/users/entities/user.entity";
import { AuthService } from "./auth.service";

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
    constructor(private readonly authService: AuthService) {
        super({
            usernameField: 'email'
        });
    }

    async validate(email: string, password: string): Promise {
        return this.authService.getAuthenticatedUser(email, password);
    }
}

For the local strategy, passport calls validate the method with email and password as parameters. Eventually, we will also set up an Authentication Guard.

Validate method uses authService to get authenticated user. This looks like below:


import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import * as bcrypt from 'bcryptjs';
import { User } from 'src/users/entities/user.entity';
import { UsersService } from 'src/users/users.service';

@Injectable()
export class AuthService {   

    constructor(private readonly userService: UsersService) {}

    async getAuthenticatedUser(email: string, password: string): Promise {
        try {
            const user = await this.userService.getByEmail(email);
            console.log(user);
            await this.validatePassword(password, user.password);            
            return user;
        } catch (e) {
            throw new HttpException('Invalid Credentials', HttpStatus.BAD_REQUEST);
        }
    }

    async validatePassword(password: string, hashedPassword: string) {
        const passwordMatched = await bcrypt.compare(
            password,
            hashedPassword,
        );

        if (!passwordMatched) {
            throw new HttpException('Invalid Credentials', HttpStatus.BAD_REQUEST);
        }
    }
}

Passport provides in-built guard AuthGuard. Depending on what strategy you are using, we can extend this AuthGuard as below:


import { Injectable } from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";

@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {
    
}

Now in our controller, we will use this guard for authentication purposes. Make sure you have set up your Authentication Module to provide LocalStrategy.


import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { UsersModule } from 'src/users/users.module';
import { PrismaModule } from 'src/common/prisma.module';
import { LocalStrategy } from './local.strategy';
import { PassportModule } from '@nestjs/passport';

@Module({
  imports: [UsersModule, PrismaModule, PassportModule],
  controllers: [AuthController],
  providers: [AuthService, LocalStrategy]
})
export class AuthModule {}

In our User Controller, we have added @UseGuards(LocalAuthGuard).

Demonstration of Basic Authentication with Passport

We have covered the front end and backend. Let’s take a look at the actual demo of this application now.

Go to frontend directory and start our react app

npm start

It will run by default on port 3000, but I have set port 3001 to use.

start : set PORT=3001 && react-scripts start

Also start the backend NestJS app

npm run start

This will run by default on port 3000. I have not changed that and using the default port. The frontend will call backend APIs on port 3000. Of course, in real-world applications, you will have some middleware like a load balancer or gateway to route your API calls.

Once the applications are running, access the demo app at http://localhost:3001 and it should show the login screen.

Basic Authentication Passport NestJS - Login Screen

If you don’t have a user, we will create a new user with sign-up option. Once the user enters credentials, user will see the home page

User Authentication Passport

That’s all. The code for this demo is available here.

Conclusion

In this post, I showed the details of the Passport library and how you can use this library for basic authentication in a Nest JS and React application.

Upload File to S3 Using NestJS Application

Introduction

In this post, I will show how to upload a file to S3 using a NestJS application and Multer. S3 is Amazon’s Simple Storage Service (S3). In old systems, one could upload files to a database. However, with storage services, it is easier to upload and retrieve files. This is also more performant.

If you are a beginner with the NestJS framework, I would recommend reading their documentation. I have written some posts about NestJS previously herehere, and here.

Simple Storage Service

Simple storage service is also known as S3. Irrespective of what kind of application you build, you have to store static files somewhere. AWS offers a simple and effective service called S3. As previously mentioned in the post, application developers used to save files in the database, but that is not very performant.

With S3, now you can store the file in a storage service and create a file link that you can save in the database.

To understand S3 is to understand a hash table. Usually, when you store any file on S3, the service generates a random string as a key to identify that file. The file is the blob format data.

AWS S3 is targeted towards application builders to individual users. The advantages of S3 are scalability, high availability, performance, and security. S3 can also be used for redundancy. One thing to remember is that you can not use S3 to host a static website, especially if you want to use it with HTTPS.

Setting up an S3 bucket

For this demo, let’s set up an S3 bucket in the AWS S3 service. I would assume that you have an AWS account with enough permissions to manage resources like creating or deleting buckets. If you don’t, you can always create an account for AWS with a free tier. Over the years, AWS has done a great job to help developers play with their services and improve them.

Once you have your user, make sure to download the AWS Access Key and AWS Secret Key. We will need these keys to call AWS services programmatically.

Let’s create a bucket in S3.

S3 bucket file upload NestJS

NestJS Application

Let’s create a nestjs application. If you do not have nest-cli downloaded, I would recommend use npm i @nestjs/cli.

1. Set up Application

  • Create a new application

nest new fileuploaddemo

Once we have the facade of the application, we will create an API, service, and a database table. If you are wondering, why a database table, then to answer that – We will use a database to store file metadata information including the link and when it was created.

Just like previous articles, we will use prisma as our ORM for creating this nestjs application.

  • Install Prisma dependencies

npm install prisma --save-dev

npm install @prisma/client

  • Create initial Prisma set up

npx prisma init

If you are using Windows environment, you might run into an error while running above command. So set environment variables

set PRISMA_CLI_QUERY_ENGINE_TYPE=binary

set PRISMA_CLIENT_ENGINE_TYPE=binary

  • Add the database table details in schema.prisma file
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

model FileEntity {
  id Int @default(autoincrement()) @id
  fileName String
  fileUrl  String 
  key      String
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt
}

And make sure to set DATABASE_URL as the environment variable before running Prisma migrations.

  • Run prisma migrations

npm prisma migrate dev

2. Set up environment

We will need a few environment variables to make sure our application is functioning.

So create .env file in your application and add the following variables.


AWS_REGION=us-east-1
AWS_ACCESS_KEY_ID=accessKey
AWS_SECRET_ACCESS_KEY=secretKey
AWS_BUCKET_NAME='nestjsfileuploaddemo'
DATABASE_URL="mysql://user:password@localhost:3306/fileuploaddemo?schema=public"

3. Upload API

Usually, a client application will send us file details through form-data. For this demo, we will create an API that the client application can call. This will be our upload API.

import { Controller, Post, UploadedFile, UseInterceptors } from "@nestjs/common";
import { FileInterceptor } from "@nestjs/platform-express";
import { FileUploadService } from "src/services/fileupload.service";
import { Express } from 'express';


@Controller('/v1/api/fileUpload')
export class FileController {
    constructor(private fileUploadService: FileUploadService) {}

    @Post()
    @UseInterceptors(FileInterceptor('file'))
    async uploadFile(@UploadedFile() file: Express.Multer.File): Promise {
        const uploadedFile = await this.fileUploadService.uploadFile(file.buffer, file.originalname);
        console.log('File has been uploaded,', uploadedFile.fileName);        
    }

}

In this API, we are using FileInterceptor from NestJS which extracts file details from the request.

The API is straightforward. We pass the file metadata information to FileUploadService and that does the job of uploading the file to S3 as well as saving the data in the database.

4. FileInterceptor for File Upload

An interceptor is one of the fundamental concepts in NestJS. Interceptor is a class with annotation @Injectable() and implements NestInterceptor interface.

NestJS Interceptor For File Upload

5. Create a FileUploadService

FileUploadService will perform two tasks. One is to upload the file to S3 and the second is to store the metadata in the database.


import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { FileEntity, Prisma } from "@prisma/client";
import { S3 } from "aws-sdk";
import { PrismaService } from "src/common/prisma.service";
import { v4 as uuid } from 'uuid';

@Injectable()
export class FileUploadService {
    constructor(private prismaService: PrismaService,
        private readonly configService: ConfigService,){}
    
    async uploadFile(dataBuffer: Buffer, fileName: string): Promise {
        const s3 = new S3();
        const uploadResult = await s3.upload({
            Bucket: this.configService.get('AWS_BUCKET_NAME'),
            Body: dataBuffer,
            Key: `${uuid()}-${fileName}`,
        }).promise();

        const fileStorageInDB = ({
            fileName: fileName,
            fileUrl: uploadResult.Location,
            key: uploadResult.Key,
        });

        const filestored = await this.prismaService.fileEntity.create({
            data: fileStorageInDB
        });

        return filestored;
    }
}

We have a method called uploadFile in this service class. It takes two parameters one for a data buffer and the other for a file name.

We create an S3 instance using S3() and use that to upload files to S3. It needs a bucket name, a data buffer in the body, and a unique key for storing the file.

We use the location as fileUrl from the uploaded file. And then save the metadata information in the database.

6. Final Demo

Let’s run our application to see how it is working now.

npm run start – will start the application at default port 3000.

We will use postman to call our upload API.

Upload API NestJS

Once we upload the file, we will see a console message of file uploaded.

Upload File To S3

And similarly, a database entry

Database File Upload

Just to make sure that we have our files uploaded in S3, let’s look at the bucket.

S3 File Upload Using NestJS API

You will see two entries in the bucket.

Conclusion

In this post, we discussed how we can upload a file to AWS Simple Storage Service (S3) using NestJS application. We only covered the surface of this vast topic of S3. You can also manage to upload private files, encrypt, and decrypt those files. You can also subscribe to events of file upload. There are a lot of options when it comes to AWS S3.  We will cover more in the upcoming series of NestJS applications.