In this article, we will be discussing JWT, its structure, its workings, and how to implement authentication and authorization in Express API using JWT.
It is preferred that you are familiar with
Javascript
The basics of express
The basics of MongoDB
The basics of API request
Postman (for API testing)
You need not have any familiarity with JWT as we will discuss it from the very basics.
JSON Web Tokens (JWT) have been introduced as a method of secure communication between two parties.
Although it was meant for any secure communication, JWT is mainly associated with authentication and authorization.
A sample JWT token looks like this:
1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.cThIIoDvwdueQB468K5xDc5633seEFoqwxjF\ _xSJyQQ
Which is constructed as–
1 2 3 4 5
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), your - 256 - bit - secret )
The first part is base64 encoded header, which looks like this when decoded
1 2 3 4
{ "alg": "HS256", "typ": "JWT" }
This basically contains the algorithm type, which is HMAC SHA 256, and token type, which is JWT.
The second part contains base64 encoded JSON data that is being exchanged (mostly a few user details in the case of authentication), which in our token looks like this,
1
2
3
4
5
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
The third part is a signature to verify that the token is legit and information has not been changed.
It is recommended to not include any sensitive data in JWT like user password.
If you want to play around with JWT visit jwt.io
Below is a working diagram of JWT authentication and authorization.
First the client sends a login request with login credentials (mainly username, email, password), then on the server side we check if the given login credentials are correct. If so, we generate a signed JWT token with user info and send it back to the client.
After the user is logged in, a data request is sent by the client with a signed JWT token (to inform the server who is asking for data). On the server side we check if the provided JWT is valid, then we check if the user is allowed to see the data that was requested (this step is known as authorization). We send the data if both steps check out, otherwise we send an error message.
Now let’s implement this.
I am assuming you have NodeJs Installed in your system (or see How to Install NodeJs).
Create your project folder, open it, and then open the terminal (git bash if you are using Windows) at that location and run.
npm init -y
Create a file with name app.js (this is our main server file).
Now we will install some node packages required for this tutorial.
Express (framework to design API)
Mongoose (to manage MongoDB database)
jsonwebtoken (for JWT tokens)
dotenv (store and access environment variables)
bcrypt (to encrypt the password)
To install packages run:
npm i express mongoose jsonwebtoken bcrypt dotenv --save
Now we need to install dev dependency nodemon to make our work easier (it reruns the application when we make changes to it).
To install nodemon run:
npm i nodemon -D
Now open package.json and change the main file to app.js (as given below).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
{ "name": "jwt-authentication", "version": "1.0.0", "description": "", "main": "app.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "bcrypt": "^5.0.1", "dotenv": "^10.0.0", "express": "^4.17.1", "jsonwebtoken": "^8.5.1", "mongoose": "^6.0.7" }, "devDependencies": { "nodemon": "^2.0.12" } }
Now open app.js and write the code given:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const express = require("express"),
app = express();
require("dotenv")
.config();
// parse requests of content-type - application/json
app.use(express.json());
// parse requests of content-type - application/x-www-form-urlencoded
app.use(express.urlencoded({
extended: true
}));
//setup server to listen on port 8080
app.listen(process.env.PORT || 8080, () => {
console.log("Server is live on port 8080");
})
Now create file with the name “.env”, we will store environment passwords in it in the given form
PORT=8080
Open terminal and run
nodemon app.js
You will see something like this:
We will set up MongoDB now.
If you don’t have MongoDB installed on your system see How to install MongoDB community edition. Or, you can use the cloud version (recommended if you are familiar with it). For this tutorial, we will use the MongoDB community edition.
Start MongoDB on a separate terminal by running
sudo systemctl start mongod
Run mongosh.exe on cmd (in administrator mode) in the case of Windows.
Now come back to app.js and modify app.js to connect to the database as given:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const express = require("express"),
app = express(),
mongoose = require("mongoose");
//Connect to database
try {
mongoose.connect("mongodb://localhost:27017/usersdb", {
useUnifiedTopology: true,
useNewUrlParser: true
});
console.log("connected to db");
} catch (error) {
handleError(error);
}
process.on('unhandledRejection', error => {
console.log('unhandledRejection', error.message);
});
// parse requests of content-type - application/json
app.use(express.json());
// parse requests of content-type - application/x-www-form-urlencoded
app.use(express.urlencoded({
extended: true
}));
//setup server to listen on port 8080
app.listen(process.env.PORT || 8080, () => {
console.log("Server is live on port 8080");
})
This try and catch block tries to connect to mongodb running on port 27017 and outputs connected to the database, if connection is successful. Otherwise, catch block will run, which catches the error. In this block handleError block tries to resolve error, if it is still left unresolved next function process.on() outputs error on the terminal.
Now we are connected to the database, so let’s create our user schema.
Create remaining needed files and folders as given below.
Open models/user.js (user.js file in models folder) and write the given code.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
var mongoose = require('mongoose'),
Schema = mongoose.Schema;
/**
* User Schema
*/
var userSchema = new Schema({
fullName: {
type: String,
required: [true, "fullname not provided "],
},
email: {
type: String,
unique: [true, "email already exists in database!"],
lowercase: true,
trim: true,
required: [true, "email not provided"],
validate: {
validator: function (v) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v);
},
message: '{VALUE} is not a valid email!'
}
},
role: {
type: String,
enum: ["normal", "admin"],
required: [true, "Please specify user role"]
},
password: {
type: String,
required: true
},
created: {
type: Date,
default: Date.now
}
});
module.exports = mongoose.model('User', userSchema);
Here we are creating user schema with the fields email, password, fullName, role, and time of creation of the user. We are also validating email with validator function.
Now we will create controllers for signup and signin. Open auth.controller.js file in the controllers folder and write the code given below.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
var jwt = require("jsonwebtoken");
var bcrypt = require("bcrypt");
var User = require("../models/user");
exports.signup = (req, res) => {
const user = new User({
fullName: req.body.fullName,
email: req.body.email,
role: req.body.role,
password: bcrypt.hashSync(req.body.password, 8)
});
user.save((err, user) => {
if (err) {
res.status(500)
.send({
message: err
});
return;
} else {
res.status(200)
.send({
message: "User Registered successfully"
})
}
});
};
exports.signin = (req, res) => {
User.findOne({
email: req.body.email
})
.exec((err, user) => {
if (err) {
res.status(500)
.send({
message: err
});
return;
}
if (!user) {
return res.status(404)
.send({
message: "User Not found."
});
}
//comparing passwords
var passwordIsValid = bcrypt.compareSync(
req.body.password,
user.password
);
// checking if password was valid and send response accordingly
if (!passwordIsValid) {
return res.status(401)
.send({
accessToken: null,
message: "Invalid Password!"
});
}
//signing token with user id
var token = jwt.sign({
id: user.id
}, process.env.API_SECRET, {
expiresIn: 86400
});
//responding to client request with user profile success message and access token .
res.status(200)
.send({
user: {
id: user._id,
email: user.email,
fullName: user.fullName,
},
message: "Login successfull",
accessToken: token,
});
});
};
In the above code we are importing jsonwebtoken, bcrypt, and the user model we created.
Then we have defined signup controller which creates the user in the database with info provided in the request body. Here we are using bcrypt to hash our password before storing it in the database.
Then we have defined signin controller which takes the user and password from the request body, checks if the user with that email exists, and if not, it responds with an error message. It will compare passwords and if the password is wrong it will respond with an error message. Otherwise it creates a JWT token with user-id and responds with a user profile success message and access token.
Notice that we have used process.env.API_SECRET while signing JWT, make sure you declare this variable in the .env file. Mine looks like this:
PORT=8080
API_SECRET=This_is_very_secret_string
Now let’s define API routes where we will utilize these controllers. Open user.js file in the routes folder and write down the code given below.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var express = require("express"),
router = express.Router(),
{
signup,
signin
} = require("../controllers/auth.controller.js");
router.post("/register", signup, function (req, res) {
});
router.post("/login", signin, function (req, res) {
});
module.exports = router;
Now we need to import this route in app.js and use it. Modify your app.js as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const express = require("express"),
app = express(),
mongoose = require("mongoose"),
userRoutes = require("./routes/user");
//Connect to database
try {
mongoose.connect("mongodb://localhost:27017/usersdb", {
useUnifiedTopology: true,
useNewUrlParser: true
});
console.log("connected to db");
} catch (error) {
handleError(error);
}
process.on('unhandledRejection', error => {
console.log('unhandledRejection', error.message);
});
// parse requests of content-type - application/json
app.use(express.json());
// parse requests of content-type - application/x-www-form-urlencoded
app.use(express.urlencoded({
extended: true
}));
//using user route
app.use(userRoutes);
//setup server to listen on port 8080
app.listen(process.env.PORT || 8080, () => {
console.log("Server is live on port 8080");
})
We have covered user registration (signup) and authentication.
Now let’s implement authorization. For this we will implement a middleware function and we will use it while defining the endpoint which will require authorization. Open authJWT.js in middleware and write code as given below.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const jwt = require("jsonwebtoken");
User = require("../models/user");
const verifyToken = (req, res, next) => {
if (req.headers && req.headers.authorization && req.headers.authorization.split(' ')[0] === 'JWT') {
jwt.verify(req.headers.authorization.split(' ')[1], process.env.API_SECRET, function (err, decode) {
if (err) req.user = undefined;
User.findOne({
_id: decode.id
})
.exec((err, user) => {
if (err) {
res.status(500)
.send({
message: err
});
} else {
req.user = user;
next();
}
})
});
} else {
req.user = undefined;
next();
}
};
module.exports = verifyToken;
In the above code we are importing jsonwebtoken module and user model defined above.
In given middleware we are checking headers of request.
Here we are looking at the authorization header which is in form JWT [JWT_TOKEN], so we are splitting it and then verifying it. If we get user info then we will send the information requested, otherwise we will send the error message.
Now let’s set up a route that will utilize this code. Modify your app.js file in routes folder as given:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
var express = require("express"),
router = express.Router(),
verifyToken = require('../middlewares/authJWT'),
{
signup,
signin
} = require("../controllers/auth.controller.js");
router.post("/register", signup, function (req, res) {
});
router.post("/login", signin, function (req, res) {
});
router.get("/hiddencontent", verifyToken, function (req, res) {
if (!user) {
res.status(403)
.send({
message: "Invalid JWT token"
});
}
if (req.user == "admin") {
res.status(200)
.send({
message: "Congratulations! but there is no hidden content"
});
} else {
res.status(403)
.send({
message: "Unauthorised access"
});
}
});
module.exports = router;
Here we defined a get route /hiddencontent which will check if you have a valid token. If you are admin it will send a congratulations message, otherwise it will send an unauthorised error message.
Now let’s test our API, fire up Postman and create a POST request to create a user as given below.
Now let’s test the login route. Create a POST route as given:
This shows that both register and signup are working fine.
Let’s test /hiddencontent. Create a GET request as follows,
Make sure you write the value of the authorization token as JWT [JWT_TOKEN].
You will get response as,
We successfully completed authentication and authorization with JWT.