This is the final post in our series on building a full-stack MERN app using JWT authentication. Before forging ahead, read through part one, part two, and especially part three — the extra context will help you to better understand this continuation.
Up to now, we have successfully created a basic system that talks to the REST endpoint and provides the response, changes the states as required, and shows the right content. It also has a persistent login, too.
Here, we will be dealing with creating users, validating them on the server side, and generating different types of responses, like user not found, incorrect credentials, etc.
We will start with a sample store for the server and validate the users. Before that, we need an endpoint for the users to sign in. Let us start by editing our server.js
and adding a new route, like this:
app.post("/api/Users/SignIn", (req, res) => { res.json(req.body); });
A store is similar to a data store, a static database. All we are going to do is create key-value pairs for the users and make them co-exist. We also need to export the module to import them in the main server.js
.
So, in users.js
, we will add a few users. The key is the username, and the value for the object is the password.
const Users = { Praveen: "Pr@v33n", Cloudroit: "C!0uDr0!7" }; module.exports = Users;
Finally, we use the module.exports
to export the Users
object as the default export.
Now we should be using the require
method to import the user store inside our server.js
to consume the contents of the User
object.
const Users = require("./users");
This is where we are validating the input from the user (real human using the front end here). The first validation is checking whether the user is present in the system. This can be checked in two ways: by finding the key in the Object.keys(User)
or by checking to ensure the type is not undefined
using typeof
.
If the user isn’t found, we send an error saying that user isn’t found. If the key is present, we validate the password against the value, and if it doesn’t equate, we send an error saying that the credentials aren’t right.
In both cases, we send a status code of HTTP 403 Forbidden
. If the user is found and validated, we send a simple message saying "Successfully Signed In!"
. This holds a status code of HTTP 200 OK
.
app.post("/api/Users/SignIn", (req, res) => { // Check if the Username is present in the database. if (typeof Users[req.body.Username] !== "undefined") { // Check if the password is right. if (Users[req.body.Username] === req.body.Password) { // Send a success message. // By default, the status code will be 200. res.json({ Message: "Successfully Signed In!" }); } else { // Send a forbidden error if incorrect credentials. res.status(403).json({ Message: "Invalid Username or Password!" }); } } else { // Send a forbidden error if invalid username. res.status(403).json({ Message: "User Not Found!" }); } });
With the above change, we need to update the consuming logic in the front end. We currently don’t have a service for talking to the Users/SignIn
API endpoint, so we will be creating an auth service to consume the API.
Let’s create a file inside the services
directory as services/AuthService.js
. The function AuthUser
will take up Username
, Password
, and a callback function, cb
, as parameters. The Username
and Password
are sent to the /api/Users/SignIn
endpoint as POST
data parameters, and in the promise’s then()
, the callback function is called with the response res
as its parameter.
The same thing happens with an error condition, where the status code is anything but 2xx
. In that case, we send a second parameter as true
to the callback function, passing the error object as the first one. We will be handling the error functions appropriately in the client side using the second parameter.
import axios from "axios"; export const AuthUser = (Username, Password, cb) => { axios .post("/api/Users/SignIn", { Username, Password }) .then(function(res) { cb(res); }) .catch(function(err) { cb(err, true); }); };
Since we are not generating any JWT in the client side, we can safely remove the import of the GenerateJWT()
function. If not, React and ESLint might throw the error no-unused-vars
during the compile stage.
- import { GenerateJWT, DecodeJWT } from "../services/JWTService"; + import { DecodeJWT } from "../services/JWTService"; + import { AuthUser } from "../services/AuthService";
Now we just need to get our GenerateJWT
function — and the other dependencies for that function like claims
and header
— replaced with AuthUser
and a callback function supporting the err
parameter.
Handling errors here is very simple. If the err
parameter is true
, immediately set an Error
state with the received message, accessed by res.response.data.Message
, and stop proceeding by returning false
and abruptly halting the function.
If not, we need to check the status to be 200
. Here’s where we need to handle the success function. We need a JWT to be returned from the server, but as it stands, it doesn’t currently return the JWT since it’s a dummy. Let’s work on the server-side part next to make it return the JWT.
handleSubmit = e => { // Here, e is the event. // Let's prevent the default submission event here. e.preventDefault(); // We can do something when the button is clicked. // Here, we can also call the function that sends a request to the server. // Get the username and password from the state. const { Username, Password } = this.state; // Right now it even allows empty submissions. // At least we shouldn't allow empty submission. if (Username.trim().length < 3 || Password.trim().length < 3) { // If either of Username or Password is empty, set an error state. this.setState({ Error: "You have to enter both username and password." }); // Stop proceeding. return false; } // Call the authentication service from the front end. AuthUser(Username, Password, (res, err) => { // If the request was an error, add an error state. if (err) { this.setState({ Error: res.response.data.Message }); } else { // If there's no error, further check if it's 200. if (res.status === 200) { // We need a JWT to be returned from the server. // As it stands, it doesn't currently return the JWT, as it's dummy. // Let's work on the server side part now to make it return the JWT. } } }); };
Let’s also update our little data viewer to reflect the error message, if it is available. The <pre>
tag contents can be appended, with the below showing the contents of this.state.Error
.
{this.state.Error && ( <> <br /> <br /> Error <br /> <br /> {JSON.stringify(this.state.Error, null, 2)} </> )}
Currently, our sign-in API "/api/Users/SignIn"
response just sends out HTTP 200
. We need to change that so it sends a success message along with a JWT generated on the server.
After checking if the Username
is present in the database, we need to check whether the password is right. If both conditions succeed, we have to create a JWT in the server side and send it to the client.
Let’s create a JWT based on our default headers. We need to make the claims based on the Username
provided by the user. I haven’t used Password
here because it would be highly insecure to add the password in the response as plaintext.
app.post("/api/Users/SignIn", (req, res) => { const { Username, Password } = req.body; // Check if the Username is present in the database. if (typeof Users[Username] !== "undefined") { // Check if the password is right. if (Users[Username] === Password) { // Let's create a JWT based on our default headers. const header = { alg: "HS512", typ: "JWT" }; // Now we need to make the claims based on Username provided by the user. const claims = { Username }; // Finally, we need to have the key saved on the server side. const key = "$PraveenIsAwesome!"; // Send a success message. // By default, the status code will be 200. res.json({ Message: "Successfully Signed In!", JWT: GenerateJWT(header, claims, key) }); } else { // Send a forbidden error if incorrect credentials. res.status(403).json({ Message: "Invalid Username or Password!" }); } } else { // Send a forbidden error if invalid username. res.status(403).json({ Message: "User Not Found!" }); } });
After updating the above code, the res.data
holds both Message
and JWT
. We need the JWT
, then we we need to decode it by calling the DecodeJWT
service and store it in the state. Once that is done, we also need to persist the login after refresh, so we will be storing the JWT
in localStorage
, as discussed in the previous post.
As usual, we check if localStorage
is supported in the browser and, if it is, save the JWT
in the localStore
by using the localStorage.setItem()
function.
handleSubmit = e => { // Here, e is the event. // Let's prevent the default submission event here. e.preventDefault(); // We can do something when the button is clicked. // Here, we can also call the function that sends a request to the server. // Get the username and password from the state. const { Username, Password } = this.state; // Right now it even allows empty submissions. // At least we shouldn't allow empty submission. if (Username.trim().length < 3 || Password.trim().length < 3) { // If either of the Username or Password is empty, set an error state. this.setState({ Error: "You have to enter both username and password." }); // Stop proceeding. return false; } // Call the authentication service from the front end. AuthUser(Username, Password, (res, err) => { // If the request was an error, add an error state. if (err) { this.setState({ Error: res.response.data.Message }); } else { // If there's no errors, further check if it's 200. if (res.status === 200) { // We need a JWT to be returned from the server. // The res.data holds both Message and JWT. We need the JWT. // Decode the JWT and store it in the state. DecodeJWT(res.data.JWT, data => // Here, data.data will have the decoded data. this.setState({ Data: data.data }) ); // Now to persist the login after refresh, store in localStorage. // Check if localStorage support is there. if (typeof Storage !== "undefined") { // Set the JWT to the localStorage. localStorage.setItem("JWT", res.data.JWT); } } } }); };
There are a few mistakes that we have missed when developing the whole application, which we would have noticed if we used it like an end user. Let’s find how they crept in and fix them all.
The error message is not cleared after a successful sign-in and then signing out. We need to clear the error messages when we get signed in successfully.
AuthUser(Username, Password, (res, err) => { // If the request was an error, add an error state. if (err) { this.setState({ Error: res.response.data.Message }); } else { // If there's no errors, further check if it's 200. if (res.status === 200) { + // Since there aren't any errors, we should remove the error text. + this.setState({ Error: null }); // We need a JWT to be returned from the server. // The res.data holds both Message and JWT. We need the JWT. // Decode the JWT and store it in the state. DecodeJWT(res.data.JWT, data => // Here, data.data will have the decoded data. this.setState({ Data: data.data }) ); // Now to persist the login after refresh, store in localStorage. // Check if localStorage support is there. if (typeof Storage !== "undefined") { // Set the JWT to the localStorage. localStorage.setItem("JWT", res.data.JWT); } } } });
Same thing here. After signing out, it is better to perform a cleanup of all the content, namely the Error
, Response
, and Data
. We are already setting the Response
and Data
to null
, but not the Error
.
SignOutUser = e => { // Prevent the default event of reloading the page. e.preventDefault(); // Clear the errors and other data. this.setState({ + Error: null, Response: null, Data: null }); // Check if localStorage support is there. if (typeof Storage !== "undefined") { // Check if JWT is already saved in the local storage. if (localStorage.getItem("JWT") !== null) { // If there's something, remove it. localStorage.removeItem("JWT"); } } };
server/server.js
const express = require("express"); const morgan = require("morgan"); const { GenerateJWT, DecodeJWT, ValidateJWT } = require("./dec-enc.js"); const Users = require("./users"); const app = express(); app.use(express.json()); app.use(morgan("dev")); const port = process.env.PORT || 3100; const welcomeMessage = "Welcome to the API Home Page. Please look at the documentation to learn how to use this web service."; app.get("/", (req, res) => res.send(welcomeMessage)); app.post("/api/GenerateJWT", (req, res) => { let { header, claims, key } = req.body; // In case, due to security reasons, the client doesn't send a key, // use our default key. key = key || "$PraveenIsAwesome!"; res.json(GenerateJWT(header, claims, key)); }); app.post("/api/DecodeJWT", (req, res) => { res.json(DecodeJWT(req.body.sJWS)); }); app.post("/api/ValidateJWT", (req, res) => { let { header, token, key } = req.body; // In case, due to security reasons, the client doesn't send a key, // use our default key. key = key || "$PraveenIsAwesome!"; res.json(ValidateJWT(header, token, key)); }); app.post("/api/Users/SignIn", (req, res) => { const { Username, Password } = req.body; // Check if the Username is present in the database. if (typeof Users[Username] !== "undefined") { // Check if the password is right. if (Users[Username] === Password) { // Let's create a JWT based on our default headers. const header = { alg: "HS512", typ: "JWT" }; // Now we need to make the claims based on Username provided by the user. const claims = { Username }; // Finally, we need to have the key saved on the server side. const key = "$PraveenIsAwesome!"; // Send a success message. // By default, the status code will be 200. res.json({ Message: "Successfully Signed In!", JWT: GenerateJWT(header, claims, key) }); } else { // Send a forbidden error if incorrect credentials. res.status(403).json({ Message: "Invalid Username or Password!" }); } } else { // Send a forbidden error if invalid username. res.status(403).json({ Message: "User Not Found!" }); } }); app.listen(port, () => console.log(`Server listening on port ${port}!`));
client/src/components/Login.js
import React, { Component } from "react"; import { DecodeJWT } from "../services/JWTService"; import { AuthUser } from "../services/AuthService"; class Login extends Component { state = { Username: "", Password: "" }; handleChange = e => { // Here, e is the event. // e.target is our element. // All we need to do is update the current state with the values here. this.setState({ [e.target.name]: e.target.value }); }; handleSubmit = e => { // Here, e is the event. // Let's prevent the default submission event here. e.preventDefault(); // We can do something when the button is clicked. // Here, we can also call the function that sends a request to the server. // Get the username and password from the state. const { Username, Password } = this.state; // Right now it even allows empty submissions. // At least we shouldn't allow empty submission. if (Username.trim().length < 3 || Password.trim().length < 3) { // If either of the Username or Password is empty, set an error state. this.setState({ Error: "You have to enter both username and password." }); // Stop proceeding. return false; } // Call the authentication service from the front end. AuthUser(Username, Password, (res, err) => { // If the request was an error, add an error state. if (err) { this.setState({ Error: res.response.data.Message }); } else { // If there's no errors, further check if it's 200. if (res.status === 200) { // Since there aren't any errors, we should remove the error text. this.setState({ Error: null }); // We need a JWT to be returned from the server. // The res.data holds both Message and JWT. We need the JWT. // Decode the JWT and store it in the state. DecodeJWT(res.data.JWT, data => // Here, data.data will have the decoded data. this.setState({ Data: data.data }) ); // Now to persist the login after refresh, store in localStorage. // Check if localStorage support is there. if (typeof Storage !== "undefined") { // Set the JWT to the localStorage. localStorage.setItem("JWT", res.data.JWT); } } } }); }; SignOutUser = e => { // Prevent the default event of reloading the page. e.preventDefault(); // Clear the errors and other data. this.setState({ Error: null, Response: null, Data: null }); // Check if localStorage support is there. if (typeof Storage !== "undefined") { // Check if JWT is already saved in the local storage. if (localStorage.getItem("JWT") !== null) { // If there's something, remove it. localStorage.removeItem("JWT"); } } }; componentDidMount() { // When this component loads, check if JWT is already saved in the local storage. // So, first check if localStorage support is there. if (typeof Storage !== "undefined") { // Check if JWT is already saved in the local storage. if (localStorage.getItem("JWT") !== null) { // If there's something, try to parse and sign the current user in. this.setState({ Response: localStorage.getItem("JWT") }); DecodeJWT(localStorage.getItem("JWT"), data => // Here, data.data will have the decoded data. this.setState({ Data: data.data }) ); } } } render() { return ( <div className="login"> <div className="container"> <div className="row"> <div className="col-6"> <div className="card"> {this.state.Data ? ( <div className="card-body"> <h5 className="card-title">Successfully Signed In</h5> <p className="text-muted"> Hello {this.state.Data.Username}! How are you? </p> <p className="mb-0"> You might want to{" "} <button className="btn btn-link" onClick={this.SignOutUser} > sign out </button> . </p> </div> ) : ( <div className="card-body"> <h5 className="card-title">Sign In</h5> <h6 className="card-subtitle mb-2 text-muted"> Please sign in to continue. </h6> <form onSubmit={this.handleSubmit}> {this.state.Error && ( <div className="alert alert-danger text-center"> <p className="m-0">{this.state.Error}</p> </div> )} {["Username", "Password"].map((i, k) => ( <div className="form-group" key={k}> <label htmlFor={i}>{i}</label> <input type={i === "Password" ? "password" : "text"} name={i} className="form-control" id={i} placeholder={i} value={this.state[i]} onChange={this.handleChange} /> </div> ))} <button type="submit" className="btn btn-success"> Submit </button> </form> </div> )} </div> </div> <div className="col-6"> <pre> State Data <br /> <br /> {JSON.stringify( { Username: this.state.Username, Password: this.state.Password }, null, 2 )} {this.state.Response && ( <> <br /> <br /> Response Data (JWT) <br /> <br /> {this.state.Response} </> )} {this.state.Data && ( <> <br /> <br /> Decoded Data <br /> <br /> {JSON.stringify(this.state.Data, null, 2)} </> )} {this.state.Error && ( <> <br /> <br /> Error <br /> <br /> {JSON.stringify(this.state.Error, null, 2)} </> )} </pre> </div> </div> </div> </div> ); } } export default Login;
client/src/services/JWTService.js
import axios from "axios"; export const GenerateJWT = (header, claims, key, cb) => { // Send POST request to /api/GenerateJWT axios .post("/api/GenerateJWT", { header, claims, key }) .then(function(res) { cb(res); }) .catch(function(err) { console.log(err); }); }; export const DecodeJWT = (sJWS, cb) => { // Send POST request to /api/DecodeJWT axios .post("/api/DecodeJWT", { sJWS }) .then(function(res) { cb(res); }) .catch(function(err) { console.log(err); }); }; export const ValidateJWT = (header, token, key, cb) => { // Send POST request to /api/ValidateJWT axios .post("/api/ValidateJWT", { header, token, key }) .then(function(res) { cb(res); }) .catch(function(err) { console.log(err); }); };
client/src/services/AuthService.js
import axios from "axios"; export const AuthUser = (Username, Password, cb) => { axios .post("/api/Users/SignIn", { Username, Password }) .then(function(res) { cb(res); }) .catch(function(err) { cb(err, true); }); };
Once your app is created, we need to build the app by creating a production build. The command npm run build
creates a build
directory with a production build of your app. Your JavaScript and CSS files will be inside the build/static
directory.
Each filename inside build/static
will contain a unique hash of the file contents. This hash in the filename enables long-term caching techniques. All you need to do is to use a static HTTP web server and put the contents of the build/
directory into it.
Along with that, you must be deploying your API, too, in the api/
directory on the root of your server.
Since we are already using a Git repository for this, it is a basic requirement for Heroku apps to be in a Git repository. Move to the root of the project to start with, and we need to create an app instance in Heroku. To do so, let’s use the following command in the terminal from the root of the project.
âžś JWT-MERN-App git:(master) $ heroku create [app-name]
In the above line, [app-name]
will be replaced with jwt-mern
. Once the unique app name is chosen, the availability of the name will be checked by Heroku, and it will either proceed or ask for a different name. Once that step is done and a unique app name is chosen, we can deploy to Heroku using the below command:
âžś JWT-MERN-App git:(master) $ git push heroku master
You can read more about deploying to Heroku in its documentation.
The complete code is available along with the commits in this GitHub Repository: praveenscience/JWT-MERN-FullStack: Creating a full-stack MERN app using JWT authentication.
Hope this complete set of articles was informative and interesting. Let me know your thoughts.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowThe useReducer React Hook is a good alternative to tools like Redux, Recoil, or MobX.
Node.js v22.5.0 introduced a native SQLite module, which is is similar to what other JavaScript runtimes like Deno and Bun already have.
Understanding and supporting pinch, text, and browser zoom significantly enhances the user experience. Let’s explore a few ways to do so.
Playwright is a popular framework for automating and testing web applications across multiple browsers in JavaScript, Python, Java, and C#. […]
3 Replies to "Creating a full-stack MERN app using JWT authentication: Part 4"
Storing the jwt acces token in browser’s localstorage opens a security issue in your web app. Instead, it is much better to send jwt in a httponly cookie. The authentication server sends the jwt in a cookie to the client. By doing this, the front end does not have to take care of jwt at all. The browse will send the cookie automatically to the site.
Here is a good blog on this topic: https://stormpath.com/blog/where-to-store-your-jwts-cookies-vs-html5-web-storage.
Transforming the jwt of cookie to authentication http header may be done by the target service or by infrastructure on its own e.g. by a proxy server of the service mesh.
Appreciate your reply. Yes, you are right and I completely agree with you and I have included that in the Part 3, where I say it is better to not store and send it using HTTPOnly Cookie.
Hi Praveen. Great tutorial, really helped me get started with JWT.
One question though.
When the client connects to the page with JWT in local storage, shouldn’t we run a ValidateJWT to make sure nobody tampered with the web token?
Maybe it’s on the tutorial, but I followed it once an my final front end did not include it. That may be because I already had a frontend tho, so I didn’t follow that part step-by-step.
Anywyas, thank you!