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.
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);
});
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;
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");
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!"
});
}
});
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);
});
};
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";
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.
}
}
});
};
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)}
</>
)}
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!"
});
}
});
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);
}
}
}
});
};
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);
}
}
}
});
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");
}
}
};
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}!`));
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;
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);
});
};
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]
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.
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 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)