An end to end application which demonstrates how we can implement authentication in an application using Node.js and JSON Web Tokens and some good practices.
An end to end application which demonstrates how we can implement authentication in an application using Node.js and JSON Web Tokens and some good practices.
Authentication is the primary requirement of any application (web/mobile). To start using an application today authentication is the first step we need to pass. It is the most important step in any application as it keeps the app secure by ensuring the users are verified. In this project, we will first implement the most basic form of authentication i.e. user-id and password based authentication. Whenever we log in to any application by giving our credentials (user-id & password) we are allowed to access the different pages of the application without us re-entering our credentials in each and every page.
Think this over - If HTTP is a stateless protocol (i.e. every request to the server is independent of the previous request and server doesn't know anything about client) then how will the server know that the client is requesting for something is valid (i.e. authenticity of the user and the request)?
Example - Let's say we are using Gmail. To access our emails via our Gmail account we need to give our credentials (email-id & password) to get past the security steps. Here what happens is that a request is sent to the server and since the request contains the credentials (in some form), the server can verify the authenticity of our request, and give us access to our Gmail accounts. But let's suppose we again type 'gmail.com' in the browser after sometime i.e. we are again sending a request to the server for our account access, and as before the server doesn't know anything about us. So, logically the server will ask you again for your credentials (i.e. email-id & password) but do we need to re-enter our credentials again? We don't. Server will give us the required access again this time too without us re-entering our credentials.
But how this is possible because the second time also the server doesn't know anything about the user. How did it give access to the Gmail account again without verifying who the user is?
In this project you will understand this by implementing the authentication system using Node.js. You will learn about different methods of authentication. Implement them, evaluate and identify their downsides and then look at ways to improve them.
We will build the following -
Node.js
. Specifically, some public and 1 private API.Node.js
and learn npm
Express.js
framework and simplifying REST APIsJWT
and improving our authentication logicAn end to end application which demonstrates how we can implement authentication in an application using Node.js and JSON Web Tokens and some good practices.
Authentication is the primary requirement of any application (web/mobile). To start using an application today authentication is the first step we need to pass. It is the most important step in any application as it keeps the app secure by ensuring the users are verified. In this project, we will first implement the most basic form of authentication i.e. user-id and password based authentication. Whenever we log in to any application by giving our credentials (user-id & password) we are allowed to access the different pages of the application without us re-entering our credentials in each and every page.
Think this over - If HTTP is a stateless protocol (i.e. every request to the server is independent of the previous request and server doesn't know anything about client) then how will the server know that the client is requesting for something is valid (i.e. authenticity of the user and the request)?
Example - Let's say we are using Gmail. To access our emails via our Gmail account we need to give our credentials (email-id & password) to get past the security steps. Here what happens is that a request is sent to the server and since the request contains the credentials (in some form), the server can verify the authenticity of our request, and give us access to our Gmail accounts. But let's suppose we again type 'gmail.com' in the browser after sometime i.e. we are again sending a request to the server for our account access, and as before the server doesn't know anything about us. So, logically the server will ask you again for your credentials (i.e. email-id & password) but do we need to re-enter our credentials again? We don't. Server will give us the required access again this time too without us re-entering our credentials.
But how this is possible because the second time also the server doesn't know anything about the user. How did it give access to the Gmail account again without verifying who the user is?
In this project you will understand this by implementing the authentication system using Node.js. You will learn about different methods of authentication. Implement them, evaluate and identify their downsides and then look at ways to improve them.
We will build the following -
Node.js
. Specifically, some public and 1 private API.Node.js
and learn npm
Express.js
framework and simplifying REST APIsJWT
and improving our authentication logicIn this module we will learn about Node.js
. Then we will install all the required things and initialize an empty project.
Node.js
on your machine from here. This will automatically install npm.npm
and how it works from here.Node.js
and how it works. This video will help you.npm
commands and try them on your own. This is a good article to get started with it. After that -
node_modules
folder can be re-built using the package.json
file. So, you don't need to push the node_modules
folder to your git repo(as it is usually big). So, add the node_modules
folder to '.gitignore' file.ctrl + `
.npm
-commands. Use the following commands to reach the stage (refer expected outcome section) I am currently in -npm init --yes
npm install --save express jsonwebtoken dotenv
node_modules
folder and figure out how you can rebuild it.Node.js
, open your terminal and type node --version
. If you will get your version in return, you are good to go.
package.json
file should be like this (w.r.t. the dependencies section) -{
"name": "basic-authentication",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"dotenv": "^8.2.0",
"express": "^4.17.1",
"jsonwebtoken": "^8.5.1"
}
}
node_modules
folder hereTo get an understanding of the backend of an application we need to understand a few concepts like HTTP module, servers and using them to create APIs. In this task you will learn about web app servers, connecting your app to a target server and run the app on localhost to test out the app. Also the concept of API will be briefed here.
Node.js
by going through this video.Node.js
. This video should be a good start. After that try to go through the official documentation.7050
./hello
. This API should output "Hello World!" in the web browser on the specific port that the server is run on. Check the output using Postman API or at the URL- http://127.0.0.1:7050
.HTTP
module? - HTTP request methodHTTP
module? - You can access the entire uri path using request.url
. And after that try to think of some way to access query parameters by splitting the string after "?".Endpoint | Response | Description |
---|---|---|
GET /hello |
Hello World from 'GET' | |
POST /hello |
Hello World from 'POST' | |
GET /me?name=<YOUR_NAME> |
<YOUR_NAME> | name is a query parameter |
GET /me/<YOUR_NAME> |
<YOUR_NAME> | name is a path parameter |
GET /me/hello?name=<YOUR_NAME> |
Hello <YOUR_NAME> | name is a query parameter |
GET /me/hello/<YOUR_NAME> |
Hello <YOUR_NAME> | name is a path parameter |
name
query at /mename
query at /me/helloExpress.js
framework of Node.js
. This video helps you to get started with it. Also go through the official documentation.Express.js
framework to understand how well the code can be written using this framework.Endpoint | Response | Description |
---|---|---|
GET /hello |
Hello World from 'GET' | |
POST /hello |
Hello World from 'POST' | |
GET /me?name=<YOUR_NAME> |
<YOUR_NAME> | name is a query parameter |
GET /me/<YOUR_NAME> |
<YOUR_NAME> | name is a path parameter |
GET /me/hello?name=<YOUR_NAME> |
Hello <YOUR_NAME> | name is a query parameter |
GET /me/hello/<YOUR_NAME> |
Hello <YOUR_NAME> | name is a path parameter |
We will be building a small part of Linkedin where a user can register themselves and everyone on the platform will be able to see the profile of other users and the user will be able to update their details as well. A few points to keep in mind -
Register API with following specifications -
username
, password
, name
, college
, year-of-graduation
and register the userusername
is already taken by some other user, this should return a 400
error responseAPI to get list of all the users registered with us
Your response should look something like this -
[
{
"username": "subham301477",
"name": "Subham Agarwal",
"college": "Jadavpur University",
"year-of-graduation": 2020
},
{
"username": "deepak97",
"name": "Deepak Baid",
"college": "Regent Education",
"year-of-graduation": 2021
}
]
API to update the user-details
username
and password
along with the information they want to update.{
"username": "subham301477",
"password": "my_password",
"college": "Updated College Details",
"name": "Subham Agarwal",
"year-of-graduation": 2019
}
401
status code in case username/password
is incorrect otherwise it will return 200
response. const usersData = [];
app.post("/register", (req, res) => {
// Do the necessary stuffs to check whether we have everything required or not in the request
....
// Do the necessary checks (like whether username already taken by anyone else)
....
const currentUserData = {
"username": ..., // fill this value by taking from the request
"password": ...,
"name": ...,
"college": ....,
"year-of-graduation": ...
};
// push the data to the global array, so that it is visible (and can be used) by other APIs as well
usersData.push(currentUserData);
res.send({message: "Successfully registered!"});
})
const usersData = [];
app.post("/register", (req, res) => {.........});
app.get("/profiles", (req, res) => {
// clone the 'usersData' so that we don't change the global data
const usersDataCopy = JSON.parse(JSON.stringify(usersData));
/*
* Write the code to remove the 'password' field from each of the user.
* As this API is for public use and we don't want to expose 'password' publicly
*/
....
res.json(usersDataCopy);
})
const usersData = [];
app.put("/profile", (req, res) => {
// Check whether all the information required is present in the request or not
....
// Check whether the mentioned username and password exists in our list of registered users
let isValid = false;
let requestedUserIndexInGlobalArray = -1;
for (let i = 0; i < usersData.lenth; i ++) {
const currentUser = usersData[i];
// If the currentUser's username and password matches with the provided username and password
// then set the 'isValid' flag and 'requestedUserIndexInGlobalArray' and break from the loop
...
}
if (!isValid) {
res.status(401).json({message: "Invalid username or password!"});
}
else {
// update the user's details corresponding to the found user
usersData[requestedUserIndexInGlobalArray].name = .....
....
}
});
JAVA
or C++
code again, we start from fresh, all the previously allocated memory are now lost.After you have implemented the project till here, your app should function like this -
Now that we know a way legitimate users can update their profile i.e. we have implemented one private API. But, one issue here is that every time a user wants to update his profile, he needs to enter their credentials again and again. This is not at all a good user experience (think how will you feel when every time you type gmail.com
we have to re-enter your credentials every time). In this module we will try to fix this behaviour.
The idea is that instead of sending username
and password
with each request, we will send username
and password
in the first request. The server then sends us a token which is valid for a particular duration. Now, every time we need to send a request to a private API, we will send the token instead and if the token is valid, the server will know that the user requesting the info is legitimate.
JWT
in Node.js
by going through this article.jsonwebtoken
package and use it's different methods. You can use this website to encrypt or decrypt your JWT
tokens.
JWT
and what information they contain. You can again use this go get a clear picture of it's different sections and how they work together./login
API which takes username
& password
and returns JWT
(valid for an hr) in the response header with the key named auth-token
PUT /profiles
API so that it authenticates using JWT
sent in the request headerIt is a good idea to always send JWT
in headers. It will help in separating the logical request with the authentication part.
To test whether the JWT
really expires or not, you can set the expiry to 1 minutes and test the same instead of waiting for it to expire after 1 hr.
Remember, the information encoded with JWT
is publicly accessible. So, never include sensitive information in your token.
It is recommended to use the 3rd optional argument in JWT
library instead of manually setting the expiry in the payload.
jwt.sign({
data: 'foobar'
}, 'secret', { expiresIn: 60 * 60 });
Though at the end it will add a few arguments in the payload only. It has a few advantages -
logically
here. Because at the end that argument will be setting some extra fields in the payload we are encrypting./login
API?const secretKeyForJWT = "FDaFdsFDafsFdasfFDSAsd";
app.post("/login", (req, res) => {
// Sanity check - does the request contains the required parameters or not
...
// Check whether the given username and password exists in our collection
...
// returns 400 status in case the username or password is incorrect
....
// build the JWT which is valid for 1 hr
// Remember to include enough information to uniquely identify the user with token only
// In our case 'username' is unique across all user, so
const dataToSign = {
"username": ...,
};
const token = jwt.sign(dataToSign, secretKeyForJWT);
// send the JWT in the header
...
});
PUT /profile
API?app.put("/profile", (req, res) => {
// Sanity check - does the request contains the required parameters or not
...
// Extract JWT from the header and check whether it is valid or not
...
// returns 401 status in case JWT is invalid or expired
....
// decrypt the JWT data to get 'username'
....
// update the user information corresponding to 'username' fetched in the previous step
...
});
After you have successfully implemented the APIs, the app behaviour will look something like below -
We have almost finished our backend APIs. A few things to solve for -
In this module we will primarily fix these 2 issues.
Node.js
you can watch the first 2 minutes of this video.add .env file to the .gitignore file
, so that we don't push it to our publicly available git repo by mistake.const authenticationLogin = (req, res, next) => {
// Extract JWT from the header and check whether it is valid or not
...
// returns 401 status in case JWT is invalid or expired
....
// token is valid and verified.
// We will then decrypt the token and put the value into some field in 'req'
// so that it is accessed afterwards by any other middleware/the request handler
req.userInfo = decodedInfo;
};
app.put("/profile", authenticationLogic, (req, res) => {
// Sanity check - does the request contains the required parameters or not
...
// extract the 'username' from decrypted json token information from the 'userInfo' field of the 'req'
const username = req.userInfo.username;
// update the user information corresponding to 'username' fetched in the previous step
...
})
The app should behave like before after you have successfully implemented the APIs -
You might have seen several services which send you an email whenever you login or perform some actions on the application. Suppose you want to build similar functionality for your application. As you might have noticed, we might need to send email in a lot of use cases. So, let's build a separate API which just sends an email. Everyone should not be able to access this internal API. That API should be accessed only by us and by our other APIs. How do you prevent other users accessing this API?
In this module you will learn the basic idea of building an API which can only be accessed internally or by us.
POST /send-email
which will take the email content in the request body. For simplicity, just use console.log
inside the API instead of actually sending an email./send-email
API by using shared-secret by checking the value of the key received from the request with the defined shared-secret.
secret-key
in the headers of the request. Feel free to choose any headers of your choice, I will be using x-private-api-key
header.x-private-api-key
header is not found, send 400 response.x-private-api-key
is invalid, return 401 responseshared-secret-key
to environment variables./login
and /register
API such that it will call /send-email
API with all the details, everytime someone calls them.
/send-mail
API either synchronously or asynchronously./send-mail
API will be able to authenticate your call.add .env file to the .gitignore file
, so that we don't push it to our publicly available git repo by mistake./send-mail
API?const sharedSecretKeyForPrivateAPIs = process.env.SHARED_SECRET_KEY_FOR_PRIVATE_API;
app.post("/send-mail", (req, res) => {
const authenticationToken = <FETCH_THE_'x-private-api-key'_FROM_THE_REQUEST_HEADERS>;
// check whether the authenticationToken is present in the header or not
if (authenticationToken === undefined || authenticationToken === null) {
// send 400 response to the client
....
}
// check whether the authenticationToken received from the request is valid or not
if (authenticationToken !== sharedSecretKeyForPrivateAPIs) {
// send 401 response to the client
....
}
// send the email (simply logging in this case)
const { emailBody, emailSubject } = req.body;
console.log(emailBody, emailSubject);
});
/login
API?const sharedSecretKeyForPrivateAPIs = process.env.SHARED_SECRET_KEY_FOR_PRIVATE_API;
const secretKeyForJWT = "FDaFdsFDafsFdasfFDSAsd";
app.post("/login", (req, res) => {
// Sanity check - does the request contains the required parameters or not
...
// Check whether the given username and password exists in our collection
...
// returns 400 status in case the username or password is incorrect
....
// build the JWT which is valid for 1 hr
// Remember to include enough information to uniquely identify the user with token only
// In our case 'username' is unique across all user, so
const dataToSign = {
"username": ...,
};
const token = jwt.sign(dataToSign, secretKeyForJWT);
// attach the JWT in the header
...
// call the email sending API (we can either wait for it's completion or send the response to the user and it will complete in background
// i.e. asynchronous / synchronous
axios.post("/send-mail", {emailSubject: "Someone just logged in!", "emailBody": `Here are the details - ${JSON.stringify(req.body)}`});
res.json({ message: "Successfully logged in!" });
});
/send-mail
API using Postman and see if you are able to access it./send-mail
API and verify if you are able to call it now.Everytime you call /login
or /register
API, the request will be logged in the console.