Building a JWT MERN stack application with secure authentication requires a solid understanding of how JWT tokens work.
To begin, you need to create a new Node.js project using Express.js as the web framework. This will serve as the foundation for your MERN stack application.
In a typical MERN stack application, Express.js handles routing and API requests, while MongoDB stores data in a NoSQL database.
For authentication, you'll use JSON Web Tokens (JWT) to securely verify user identities.
Node.js and Express.js Setup
To set up your Node.js and Express.js application, start by creating a new folder for your server using the command `mkdir server`. Then, navigate into the server folder and initialize your project with `npm init --yes`, which will create a `package.json` file with default settings.
Express.js is a popular Node.js web application framework that you'll need to install with the command `npm install express`. You'll also want to install `bcrypt` to hash user passwords, `cookie-parser` to handle cookie-based sessions, and `jsonwebtoken` to create and verify JSON Web Tokens.
Here are the dependencies you'll need to install:
- Express.js: our Node.js web application framework.
- Bcrypt: helps us hash the user's password.
- Cookie-parser: handles cookie-based sessions.
- Jsonwebtoken: creates and verifies JSON Web Tokens.
After installing the required dependencies, create a new file called `index.js` in the root directory of your server subfolder. In this file, add the following code to set up your Express.js server:
In the `package.json` file, add a script to restart your server on any update:
This will ensure that your application restarts automatically whenever you make changes to the code. Now, you can start your server by running `npm start` in your terminal.
Implementing JWT Auth
To implement JWT auth, you'll need to configure the auth key in your app. This involves creating an auth.config.js file in the app/config folder and adding a secret key as a string.
The jsonwebtoken functions such as verify() or sign() use an algorithm that needs a secret key to encode and decode tokens. You'll use this secret key to verify the authenticity of the JWT.
To generate a token, you'll need to create a function in the util folder called SecretToken.js. This function will handle the generation of a token, which will be used to authenticate the user.
The token is composed of three parts: Header, Payload, and Signature. The Header contains the algorithm used to sign the token, the Payload contains the user's claims, and the Signature is used to verify the token's authenticity.
To decode the token, you'll need to split it into its three parts and parse the Payload section. This can be done on both the client-side and server-side, but for security reasons, it's recommended to do it on the server-side.
Here's a summary of the steps involved in implementing JWT auth:
- Configure the auth key in the auth.config.js file
- Create a function in the util folder to generate a token
- Split the token into its three parts: Header, Payload, and Signature
- Parse the Payload section to extract the user's claims
By following these steps, you'll be able to implement JWT auth in your MERN application and securely authenticate your users.
Server-Side Generation
The backend should support algorithms HS512 and RS512, as these are recommended by a few banking clients.
This is because they are secure and reliable, making them suitable for financial applications.
To generate a JWT on the server, we need to retrieve the necessary data from the Users DB.
The private key should be kept private and not used in any client-side code, as its name suggests.
This is a crucial security measure to prevent unauthorized access to sensitive information.
Authentication Flow
In a microservice-based architecture, the Authentication Service is one of the backend services that retrieves a user claim based on a reference token in a domain cookie and generates a JWT for this claim.
The JWT flow involves forwarding the call to the corresponding service, passing the JWT in the request header as an OAuth bearer token for further authorization by the backing service.
A valid JWT is typically attached in the Authorization header with a Bearer prefix.
There are three important parts of a JWT: the Header, Payload, and Signature, combined to form a standard structure: header.payload.signature.
Here's a brief overview of the flow for Signup & Login with JWT Authentication:
- Client sends a request to the Authentication Service
- Authentication Service verifies the user's credentials
- Upon successful verification, a JWT is generated
- JWT is attached to the Authorization header with a Bearer prefix
- Client accesses protected resources with the JWT
In the Controller for Authentication, there are two main functions: signup and signin. The signin function finds the username in the database, compares the password using bcrypt, generates a token using jsonwebtoken, and returns user information & access Token.
Middleware and Controllers
Middleware functions in JWT MERN are used to verify actions, such as signups, and process authentication and authorization. Two middleware functions are created: `verifySignUp.js` and `authJwt.js`.
The `verifySignUp.js` function checks for duplications in username and email, and verifies the roles in the request.
The `authJwt.js` function checks if a token is provided, legal or not, by verifying it using jsonwebtoken's `verify()` function. It also checks if the roles of the user contain the required role.
Middleware functions are used in combination with controller functions to handle specific actions. In the case of authentication, there are two main functions: `signup` and `signin`. The `signup` function creates a new user in the database with a default role of 'user'.
The `signin` function finds the username in the database, compares the password using bcrypt, generates a token using jsonwebtoken, and returns the user information and access token.
Here are the main actions that involve middleware and controllers:
- POST /api/auth/signup
- POST /api/auth/signin
For authorization, there are four test functions: `/api/test/all` for public access, `/api/test/user` for logged-in users, `/api/test/mod` for moderator users, and `/api/test/admin` for admin users.
Service Calls and Data
We need to make service calls to the right endpoints to avoid CORS issues. This is achieved by making requests relative to the current domain.
The secret keys for JWT are sensitive and should not be stored on the client-side, as they can be easily found and compromised.
To call the JWT service, we import the service from the services/JWTService.js file and update the handleSubmit function to call the GenerateJWT function, passing null as the third parameter to handle the secret key on the server-side.
We can decode the received JWT on the server-side and receive it via AJAX, which is done by importing the DecodeJWT function and updating the handleSubmit function to use it.
Service Calls
Service calls are essential for interacting with your server.
To make service calls, you need to send requests to the right endpoints, which are relative to the current domain to avoid CORS issues.
You should never store sensitive secret keys, like those used for JWT, on the client side, as they can be easily compromised.
To call a JWT service, you'll need to import the service from a JavaScript file and update your handleSubmit function to use it.
The secret key should be handled by the server, not stored on the client side.
By using a service like GenerateJWT, you can handle the JWT generation on the server side and receive the response via AJAX.
Persisting Data
When you reload the screen, it most likely clears the session data and any data that is stored in the memory.
We need to persist this data to avoid losing the information of the state. This can be done using httpOnly cookies with the httpOnly flag, which makes it impossible for the browser to read any cookies.
Using server-side cookies instead of localStorage is a good approach. You can read more about this in a nice article by Jeff Atwood.
Alternatively, we can temporarily store the contents like the JWT in the local storage, although it's widely not recommended due to security concerns.
To check for storage support in the browser, we can use the following code:
localStorage.getItem('key') !== null;
We can use this code to determine if the browser supports local storage, and then proceed to save the JWT in the local storage.
The data can be saved in the local storage using the following code:
localStorage.setItem('jwt', jwt);
We should also clear the data when we sign out of the system, which can be done by creating a separate function, such as the SignOutUser function.
Architecture and Setup
To set up a solid foundation for your JWT MERN application, you'll want to establish a robust architecture. This involves installing and configuring the necessary dependencies, including Express.js, CORS, and Mongoose.
First, create a new package.json file by running `npm init --yes` in your terminal. This will create a new file with default settings without asking any questions.
To install the required dependencies, run the following commands: `npm install express bcrypt cookie-parser nodemon cors jsonwebtoken dotenv`. These dependencies include Express.js, bcrypt for password hashing, cookie-parser for handling sessions, nodemon for automatic restarting, CORS for cross-origin resource sharing, jsonwebtoken for token creation and verification, and dotenv for storing sensitive information.
Once installed, create a new file called index.js in the root directory of your server subfolder. This file will contain your Node.js server.
To ensure your application restarts on any update, add the following code to your package.json file: `"scripts": { "start": "nodemon index.js" }`. This will make sure your application restarts on any update.
Here are the key dependencies and their purposes:
- Express.js: Our Node.js web application framework.
- CORS: Middleware used to enable Cross-Origin Resource Sharing (CORS) for an Express.js application.
- Mongoose: Interacts with MongoDB Database.
- jsonwebtoken: Helps create and verify JSON Web Tokens.
- dotenv: Stores configuration data in a .env file.
Before starting your server, make sure to update your package.json file with the correct script for nodemon. Now, you can start your server by running `npm start` in your terminal.
Sources
- https://blog.logrocket.com/mern-app-jwt-authentication-part-1/
- https://blog.logrocket.com/mern-app-jwt-authentication-part-3/
- https://www.bezkoder.com/node-js-mongodb-auth-jwt/
- https://www.freecodecamp.org/news/how-to-secure-your-mern-stack-application/
- https://medium.com/@aaqil.ruzzan/how-to-implement-jwt-authentication-with-the-mern-stack-4948d4423c64
Featured Images: pexels.com