Monthly Archives: August 2022

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.

How Spring Security Filter Chain Works

In this post, I will discuss how the Spring Security Filter chain works. Spring Security uses a chain of filters to execute security features. If you want to customize or add your logic for any security feature, you can write your filter and call during the chain execution.

Introduction

If you use Spring security in a web application, the request from the client will go through a chain of security filters. Security filters adapt this concept from Web Servlets.

Basically, you have a controller to receive user requests. Security filters will intercept the incoming request and perform validation for authentication or authorization before redirecting the request to the target controller.

Spring Security Filter Chain - Introduction

In short, the flow goes like

  • The user accesses the application that is secured through Spring Security. Usually, this will be through a web browser and the application will send the request to a web server.
  • The web server parses the incoming request HttpServletRequest and passes it through Spring Security filters. Each filter will perform its logic to make sure the incoming request is secure.
  • If everything goes well, the request will eventually come to MVC Controller which hosts the backend for the application. Filters can create HttpServletResponse and return to the client without even reaching the controller.

What is Spring Security Filter Chain?

Let’s create a simple web app using Spring Boot and Spring Security.

Add these two dependencies in your build.gradle file to get started

implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'

Controller

I will keep this app simple, so let’s add a REST controller to our web app.

package com.betterjavacode.securityfilterdemo.controllers;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MainController
{
    @GetMapping("/home")
    public String home() {
        return "Welcome, home!!!!";
    }
}

Consequently, we will run our application now.

Run the application

Once, we execute the app, we will see the log that Spring Boot prints by default. This log looks like the below:

2022-08-13 10:24:13.120  INFO 9368 --- [           main] c.b.s.SecurityfilterdemoApplication      : Starting SecurityfilterdemoApplication using Java 1.8.0_212 on YMALI2019 with PID 9368 (C:\projects\securityfilterdemo\build\libs\securityfilterdemo-0.0.1-SNAPSHOT.jar started by Yogesh Mali in C:\projects\securityfilterdemo\build\libs)
2022-08-13 10:24:13.123  INFO 9368 --- [           main] c.b.s.SecurityfilterdemoApplication      : No active profile set, falling back to 1 default profile: "default"
2022-08-13 10:24:14.543  INFO 9368 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2022-08-13 10:24:14.553  INFO 9368 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2022-08-13 10:24:14.553  INFO 9368 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.65]
2022-08-13 10:24:14.619  INFO 9368 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2022-08-13 10:24:14.619  INFO 9368 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1433 ms
2022-08-13 10:24:14.970  WARN 9368 --- [           main] .s.s.UserDetailsServiceAutoConfiguration :

Using generated security password: 22bd9a92-2130-487c-bf59-71e61c8124ee

This generated password is for development use only. Your security configuration must be updated before running your application in production.

2022-08-13 10:24:15.069  INFO 9368 --- [           main] o.s.s.web.DefaultSecurityFilterChain     : Will secure any request with [org.springframework.security.web.session.DisableEncodeUrlFilter@22555ebf, org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@36ebc363, org.springframework.security.web.context.SecurityContextPersistenceFilter@34123d65, org.springframework.security.web.header.HeaderWriterFilter@73a1e9a9, org.springframework.security.web.csrf.CsrfFilter@1aafa419, org.springframework.security.web.authentication.logout.LogoutFilter@515c6049, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@408d971b, org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@41d477ed, org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@45752059, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@c730b35, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@65fb9ffc, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@1bb5a082, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@34e9fd99, org.springframework.security.web.session.SessionManagementFilter@7b98f307, org.springframework.security.web.access.ExceptionTranslationFilter@14cd1699, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@1d296da]
2022-08-13 10:24:15.127  INFO 9368 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2022-08-13 10:24:15.138  INFO 9368 --- [           main] c.b.s.SecurityfilterdemoApplication      : Started SecurityfilterdemoApplication in 2.477 seconds (JVM running for 2.856)

We can see the spring security-generated password. But there is also a log message

Will secure any request with [org.springframework.security.web.session.DisableEncodeUrlFilter@22555ebf, 
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@36ebc363, 
org.springframework.security.web.context.SecurityContextPersistenceFilter@34123d65, 
org.springframework.security.web.header.HeaderWriterFilter@73a1e9a9, 
org.springframework.security.web.csrf.CsrfFilter@1aafa419, 
org.springframework.security.web.authentication.logout.LogoutFilter@515c6049, 
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@408d971b, 
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@41d477ed, 
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@45752059, 
org.springframework.security.web.authentication.www.BasicAuthenticationFilter@c730b35, 
org.springframework.security.web.savedrequest.RequestCacheAwareFilter@65fb9ffc, 
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@1bb5a082, 
org.springframework.security.web.authentication.AnonymousAuthenticationFilter@34e9fd99, 
org.springframework.security.web.session.SessionManagementFilter@7b98f307, 
org.springframework.security.web.access.ExceptionTranslationFilter@14cd1699, 
org.springframework.security.web.access.intercept.FilterSecurityInterceptor@1d296da]

The above list shows the number of filters in the chain of security filters. Spring Security automatically configures these filters on every incoming request. Filters are executed in that specific order. One can change the order by the configuration of modules.

Security Filters

Now, we have covered the basics of Spring Security Filters. Let’s look at how these filters are stacked with Servlet filters and Spring’s application context.

DelegatingFilterProxy is the filter that acts as a bridge between the Servlet container’s life cycle and Spring’s application context. Once the initial request comes to DelegatingFilterProxy filter, it delegates the request to Spring Bean to start the security filter flow.

FilterChainProxy is the filter that contains information about all the security filters. It matches the incoming request with URI mapping and accordingly passes the request to that filter. DelegatingFilterProxy start the security flow by calling FilterChainProxy.

FilterChainProxy determines which SecurityFilterChain to call from the incoming request. One can implement RequestMatcher interface to create rules for your security filter chain.

As shown above, Spring Security contains different security filters, but there are certain filters that are critical when the incoming request passes through them.

UsernamePasswordAuthenticationFilter – If your application is configured for Username and Password, the request will pass through this filter to process username/password authentication.

SecurityContextPersistenceFilter – Once the user is authenticated, the user information is configured in a security context. This filter populates SecurityContextHolder.

Conclusion

In this post, I showed the details of the Spring Security Filter Chain and how it works. Once you understand these fundamentals, it becomes easier to configure and customize Spring Security for your web application.

If you want to read more about Spring Security and how to use it for SAML, and OAuth flows, you can buy my book Simplifying Spring Security.