DEV Community

Cover image for Creating a full-stack MERN app using JWT authentication: Part 4
Brian Neville-O'Neill
Brian Neville-O'Neill

Posted on • Originally published at blog.logrocket.com on

Creating a full-stack MERN app using JWT authentication: Part 4

Written by Praveen Kumar✏️

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.

LogRocket Free Trial Banner

Adding a new endpoint

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);
});
Enter fullscreen mode Exit fullscreen mode

Creating a store for users

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;
Enter fullscreen mode Exit fullscreen mode

Finally, we use the module.exports to export the Users object as the default export.

Importing the user

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");
Enter fullscreen mode Exit fullscreen mode

User validation logic

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!"
    });
  }
});
Enter fullscreen mode Exit fullscreen mode

Creating a service to consume the users logic

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.

Creating the auth service

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);
    });
};
Enter fullscreen mode Exit fullscreen mode

Getting rid of JWT on the client side

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";
Enter fullscreen mode Exit fullscreen mode

Calling auth service on form submission

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.
      }
    }
  });
};
Enter fullscreen mode Exit fullscreen mode

Showing the error on the screen

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)}
  </>
)}
Enter fullscreen mode Exit fullscreen mode

Generate and send JWT from the server

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.

Updating response for signing in

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!"
    });
  }
});
Enter fullscreen mode Exit fullscreen mode

Updating client-side logic for signing in

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);
        }
      }
    }
  });
};
Enter fullscreen mode Exit fullscreen mode

Bug fixes and comments

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.

Clearing all error messages during successful events

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);
       }
     }
   }
 });
Enter fullscreen mode Exit fullscreen mode

Clearing error messages after sign-out

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");
     }
   }
 };
Enter fullscreen mode Exit fullscreen mode

Final commented files

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}!`));
Enter fullscreen mode Exit fullscreen mode

Client side

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;
Enter fullscreen mode Exit fullscreen mode

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);
    });
};
Enter fullscreen mode Exit fullscreen mode

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);
    });
};
Enter fullscreen mode Exit fullscreen mode

Deploying the complete code

Using React’s production build

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.

Using Heroku

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]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

You can read more about deploying to Heroku in its documentation.

GitHub repository and final thoughts

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.


Editor's note: Seeing something wrong with this post? You can find the correct version here.

Plug: LogRocket, a DVR for web apps

 
LogRocket Dashboard Free Trial Banner
 
LogRocket is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
 
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.
 
Try it for free.


The post Creating a full-stack MERN app using JWT authentication: Part 4 appeared first on LogRocket Blog.

Top comments (0)