Authentication & Authorization using React, NestJS & JWT Token
The blog post provides an overview of the various methods and technologies used to verify the identity of users in a secure and reliable manner. The article also includes detailed code examples to demonstrate how to implement authentication techniques in practice using JWT token. These code snippets provide a step-by-step guide for developers and can be used as a reference when building authentication and authorization systems.
NOTE: This article is part one of series of articles. In this part, we will implement authentication only using passport-local and passport-jwt. Also, we will not use a database, the values needed are hard-coded.
In next parts of the project, we will add a mongo DB database, create encrypted password for the user, implement authorization, and continue updating the source code.
In this blog post, we’ll learn how to implement an authentication and authorization mechanism using NestJS, Passport and JWT libraries for the server side, React, and Redux toolkit (RTK query) for the client side.
We will build an article management system, where users can post their article, and later edit or delete it. Other users can view the articles but cannot edit or delete them.
You can find all the code in this article in my GitHub repositories:
https://github.com/islavi/authentication-authorization-server
https://github.com/islavi/authentication-authorization-client
Let’s start by understanding the difference between authentication and authorization.
Authentication
Authentication is the act of validating users and checking that they are who they claim to be. This is the first step in any security process.
An authentication process includes one or more of the following:
- Passwords. Usernames and passwords are the most common authentication factors. If a user enters the correct data, the system assumes the identity is valid and grants access.
- One-time pins. Grant access for only one session or transaction.
- Authentication apps. Generate security codes via an outside party that grants access.
- Biometrics. A user presents a fingerprint or eye/face scan to gain access to the system.
In some instances, systems require the successful verification of more than one factor before granting access. This multi-factor authentication (MFA) requirement is often deployed to increase security beyond what passwords alone can provide.
Authorization
Authorization in system security is the process of giving the user permission to access a specific resource or function. This term is often used interchangeably with access control or client privilege.
Assigning permission to a user to download a specific file on a server or providing individual users with administrative access to an application are good examples of authorization.
In secure environments, authorization must always follow authentication. Users should first prove that their identities are genuine before an organization’s administrators grant them access to the requested resources.
Server implementation
In this section, we will implement the login flow.
We will create the LocalStrategy for validating user credentials and throwing UnauthorizedException in case the credentials are not valid.
Also will create the AuthService for returning the JWT token to the client.
We will start by creating a new server using NestJS
Creating a new NestJS project
Start a new NestJS project, by running the following commands in terminal:
- npm install -g @nestjs/cli
- nest new authentication-authorization-server
Installing dependencies
Next, we will install the dependencies (with their types) needed for the project.
Install dependencies and types, by running the following commands in terminal:
- npm install — save @nestjs/passport @nestjs/jwt passport passport-local passport-jwt
- npm install — save-dev @types/passport-local @types/passport-jwt
Explanation about passport
Passport is Express-compatible authentication middleware for Node.js.
Passport supports many strategies. For any strategy you choose, you’ll always need the @nestjs/passport and passport packages. Then, you’ll need to install the strategy-specific package (e.g., passport-jwt or passport-local) that implements the specific authentication strategy you are building.
In our example, we will use the passport-local strategy that supports username and password authentication, and passport-jwt strategy for validating the JWT token sent.
You can learn more about passport & authentication using NestJS here: https://docs.nestjs.com/security/authentication
Creating the authentication module
LocalAuthGuard
First, we will create the LocalAuthGuard class. This will enable us to use @UseGuards(LocalAuthGuard) decorator in the controller methods.
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}
See the file: /src/auth/guards/local-auth.guard.ts
LocalStrategy
Next, we need to create LocalStrategy class responsible for validating the username and password sent by the client.
The file contains one inherited function “validate” that gets 3 arguments: HTTP request, username and password. Passport will automatically call this function when we add @UseGuards(LocalAuthGuard) decorator to the controller function.
You can learn more about NestJs Guards here: https://docs.nestjs.com/guards
NOTE: The file extends PassportStrategy(Strategy), and in the constructor we instruct passport to replace “username” field with “email” field (because the client is sending “email” and not “username”.
The validate function below uses authService class to validate the user. If the email and password are valid the function will return the user. If the email and password are not valid, we will throw a new UnauthorizedException function and the client will receive an HTTP 401 error.
import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthService } from './../auth.service';
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private authService: AuthService) {
super({
usernameField: 'email',
passwordField: 'password',
passReqToCallback: true
});
}
async validate(req: any, email: string, password: string): Promise<any> {
console.log(`[LocalStrategy] validate: email=${email}, password=${password}`)
const user = await this.authService.validateUser(email, password);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}
See the file: /src/auth/strategies/local.strategy.ts
Now we can create AuthModule, AuthController and AuthService.
AuthService
The AuthService contains two functions:
- validateUser: receives email and password as arguments. This function validates that the username and password sent are found in the database (in our case are hard-coded) and returns the user object.
Note: The function call usersService.validateUser(email, password) that performs the validation. But you can perform the validation also here.
This function is called from the LocalStrategy validate function. - login: Receives the authenticated user as an argument and creates the payload to be encrypted inside the JWT token.
The function will return a JSON object to the client that contains:
- access_token: the encrypted token.
- email: email of the user, so the client can use it.
- name: full name of the user, so the client can show it.
You can add additional fields if you need them for the client.
This function is called from AuthController (login endpoint) with the @UseGuards(LocalAuthGuard) decorator.
NOTE: In “real” systems when creating username and password, we will encrypt the password with key + salt, and save it encrypted in the database. When validating the password, we will encrypt the password received in the request with the same key + salt and compare the passwords.
Generally, we will enter the payload unique id of the user (in our case email) inside the JWT token, so when the client sends the JWT token inside the header, we can extract the user email and work with it (fetch data from database, etc.…)
It is important not to relay on the client request sending user email (as query param, or in post body), this can be easily manipulated in client.
Always extract the unique id from JWT token.
import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service';
import { JwtService } from '@nestjs/jwt';
import { IUser } from 'src/users/users.interface';
@Injectable()
export class AuthService {
constructor(
private usersService: UsersService,
private jwtService: JwtService
) {}
async validateUser(email: string, password: string): Promise<any> {
console.log(`[AuthService] validateUser: email=${email}, password=${password}`)
return await this.usersService.validateUser(email, password);
}
async login(user: IUser) {
console.log(`[AuthService] login: user=${JSON.stringify(user)}`)
const payload = { email: user.email, name: user.name };
return {
access_token: this.jwtService.sign(payload),
email: user.email,
name: user.name
};
}
}
See the file: /src/auth/auth.service.ts
For simplification, I did not use a database and kept the users hard-coded (see UsersService).
AuthController
In the AuthController we will define the login function with the decorator @UseGuards(LocalAuthGuard).
Now, when the client calls the login endpoint, the server first executes the decorator and validates the user. If the user is valid, the client continues to execute the login function. If not, it throws an UnauthorizedException and the client will receive an HTTP 401 error.
import { Controller, Get, Request, Post, UseGuards } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LocalAuthGuard } from './guards/local-auth.guard';
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@UseGuards(LocalAuthGuard)
@Post('login')
async login(@Request() req) {
console.log(`[AuthController] login`)
return this.authService.login(req.user);
}
}
See the file: /src/auth/auth.controller.ts
NOTE: Passport automatically adds a user object to the HTTP request. In the login function above we are passing the user to authService login function (to build JWT token and additional data and return it to client).
AuthModule
The authModule should import the PassportModule, and JwtModule with the secret key used to build JWT token.
NOTE: Here the secret key is taken from a hard-coded constants file, if you are using Kubernetes it is recommended to store the key in secret file.
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LocalStrategy } from './strategies/local.strategy';
import { JwtStrategy } from './strategies/jwt.strategy';
import { UsersModule } from '../users/users.module';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { AuthController } from './auth.controller';
import { appConstants } from '../constants';
@Module({
imports: [
UsersModule,
PassportModule,
JwtModule.register({
secret: appConstants.jwtSecret,
signOptions: { expiresIn: '20m' },
}),
],
controllers: [AuthController],
providers: [AuthService, LocalStrategy, JwtStrategy],
exports: [AuthService],
})
export class AuthModule {}
See the file: /src/auth/auth.module.ts
Explanation of how passport works
Passport is authentication middleware; it will execute when the client calls the server before executing the controller functions.
The middleware will validate the user, and, if the user is valid, it will add the user data to the request object.
The controller function will execute after the middleware (when the user is already validated), and we can get the user data from the HTTP request object.
Checking the server code
To check the server code, especially the login function, you can use:
Postman:
Post request to http://localhost:3100/v1/auth/login
With the following body:
{
"email": "israel@test.com",
"password": "123456"
}
Curl command from terminal:
curl - location - request POST 'http://localhost:3100/v1/auth/login' \
- header 'Content-Type: application/json' \
- data-raw '{
"email": "israel@test.com",
"password": "123456"
}'
Verify that the response looks like this:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImlzcmFlbC5sYXZpQGdtYWlsLmNvbSIsIm5hbWUiOiJJc3JhZWwgTGF2aSIsImlhdCI6MTY3MDM0NDUyNiwiZXhwIjoxNjcwMzQ1NzI2fQ.flpwterxvY6_UfDg5x2Od9KOjJ-sO0Uae_bQlsIX98E",
"email": "israel@test.com",
"name": "Israel Lavi"
}
You can find all server code on my GitHub: https://github.com/islavi/authentication-authorization-server
Client implementation
I’m not going to go over all the code, we’ll talk about the main components of the app.
App.tsx
The main file of the app contains code with the following flow:
- Get the access_token and the name of the user from Redux store.
- Get the “user” object from local storage (if the user has already previously performed a successful login) and dispatch the action to update the authenticated user in Redux store.
- Update the local state to notify that we have finished initializing.
- Logout method that dispatches an action to delete “user” from local storage and set the redux store state to the default value.
The files also contain the following components:
- Provider — for Redux store.
- NotificationContainer — for showing different notifications (like authentication success).
- Loader — for showing a progress bar while attempting to connect the server.
- NavBar — for the top menu, and defining all the navigation routes
NOTE: Within the navigation, there are items that will show only for authenticated users (e.g., My Articles and Logout options). This is done by checking if we have access_token.
Services
Auth.service.ts
The auth service uses RTK query for posting HTTP request to the server.
We added one login method that will send a post request with email and password to the server and will get access_token, name and email of the user in case the validation pass (In case the validation fails we will get HTTP 401 error).
The URL of the server is built from baseUrl and the URL inside the query, so the server URL will look like this: http://localhost:3100/v1/auth/login
NOTE: The login request does not send the JWT token in the header (as the user has not yet validated it).
Articles.service.ts
The articles service uses RTK query for posting the HTTP request to the server.
We added two methods:
- getAllArticles - get all articles to show on the home page
- getMyArticles - get articles for the logged-in user
NOTE: The prepareHeaders method will check if we have an access token in the store and will add it as “authorization” header. Without the authorization header the server will return HTTP error 401.
Slices
Auth.slice.ts
This is the Redux reducer for authentication. It includes 2 methods:
- setAuthenticatedUser - set the user email, name, and access token (used after the user logged in successfully)
- resetState - reset the state and delete local storage (used when the user clicks the logout button).
General.slice.ts
The general slice contains only one method setShowLoader to show the loader while fetching data.
Running the client
Open the server project on terminal and run:
npm run start
The server will start to listen on port 3100.
Open the client project on terminal and run:
npm run start
This will open a new browser with the login page on port 3000.
Login page
URL: http://localhost:3000
Enter email: israel@test.com
Enter password: 123456
Agree to the terms and conditions and press on Submit form button.
The login page transfers user credentials to the server.
NOTE: In “real” systems we will use https and not http.
If you look at the network tab in Chrome developer tabs you will see the login request:
URL: http://localhost:3100/v1/auth/login
Method: POST
Payload: {“email”:”israel@test.com”,”password”:”123456"}
If you get a valid response, it should include the access token.
Example of a valid response:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImlzcmFlbEB0ZXN0LmNvbSIsIm5hbWUiOiJJc3JhZWwgTGF2aSIsImlhdCI6MTY3MjE0MTI1NywiZXhwIjoxNjcyMTQyNDU3fQ.l2nL-AS77Ez88-7lemwsS6baZHOgvny0x-o9h_9BYCE",
"email": "israel@test.com",
"name": "Israel Lavi"
}
After successful login, the client will store the access token in local storage, so that the user does not have to login again while refreshing the page and the token is valid (while the app loads, it searches for the access token in local storage and uses it).
Also, you will be redirected to the home page.
If the login is unsuccessful, 401 HTTP error is returned and the client shows a notification to the user that there was a problem with authentication.
Home page
Send a get request to the server, to get all the articles (this API does not require authentication, so the unauthenticated users will also see the articles).
URL: http://localhost:3100/v1/article/all
Method: GET
The response will include an array of articles to show on the page.
If the user is authenticated (the user has an access token), the top menu will also show the “My Articles” tab, this tab is for authenticated users only.
Click “My Articles” to see specific articles related to the user you logged in with.
NOTE: This end point includes AuthGuard in the server controller, so for an unauthenticated user we will get HTTP error 401.
My articles
Show specific articles relevant to the logged-in user.
URL: http://localhost:3000/my
Method: GET
The response will include an array of articles to show in the page.
Now, log out and try to navigate to http://localhost:3000/my.
You will see that the response from the server returns HTTP error 401, and the client redirects the user to the login page.
Summary
We learned how to implement Authentication using React, NestJS and JWT Token.
In the next articles, we will add support for Authorization in the client and server, and connect the server to the mongoDB database.
You can find all the code in this article in my GitHub repositories:
https://github.com/islavi/authentication-authorization-server
https://github.com/islavi/authentication-authorization-client
Appendix
Authentication & Authorization: https://www.okta.com/identity-101/authentication-vs-authorization/