4 min read

Node JWT Authentication Process

Last Update: 25 April, 2022

Handling authentication and authorisation can be very daunting especially if you are a beginner. I know it was for me. With this post you can get a basic understanding of the jwt auth process with passport.js. We won't be using any frontend framework, gonna keep it nice and simple.

§The Packages We Will Need

We will be hashing our password and create a user token to store in local storage. Using a database will also be necessary for this little project, my personal choice was the mongodb atlas. It's very easy to use and part of my favourite stack. I used mongoose to interact with my database, makes it really easy. With "nodemon" you don't need to stop and start your project after every change.

Initiate an npm package and just skip the questions with "-y":

npm init -y

Also go ahead and install the necessary packages for our project:

npm i bcrypt crypto dotenv express fs jsonwebtoken mongoose passport nodemon

Don't forget to add a script for nodemon, if you don't wanna write the command on the console again and again, mine is "dev": "nodemon server.js".

Full package.json file here:

{ "name": "auth-project-post", "version": "1.0.0", "description": "", "main": "server.js", "scripts": { "dev": "nodemon server.js", "test": "echo \"Error: no test specified\" && exit 1", "start": "node server.js" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "bcrypt": "^5.0.1", "crypto": "^1.0.1", "dotenv": "^16.0.0", "express": "^4.17.3", "fs": "^0.0.1-security", "jsonwebtoken": "^8.5.1", "mongoose": "^6.3.0", "passport": "^0.5.2" }, "devDependencies": { "nodemon": "^2.0.15" } }

§Setting The Server File

Our "npm run dev" command will tell nodemon to run the server.js file in the project root, which is pretty basic. The comments will be explaining the logic.

const express = require('express'); // we are going to get our url for database connection from .env file require('dotenv').config(); const mongoose = require('mongoose'); // It's always good to separate your code up const routes = require('./routes/index.js') const secretRoutes = require('./secret-routes/index.js') const app = express(); // we will be using body to send data app.use(express.json()) app.use(express.urlencoded({"extended":true})) // Tell express where your static files are located app.use('/', express.static(process.cwd() + '/public')); app.use('/', routes); app.use('/secret', secretRoutes); const port = process.env.PORT || 5000; // Not to risk anything we are connecting and then running the listen command mongoose.connect(process.env.URI, { useNewUrlParser:true, useUnifiedTopology: true, } ) .then(() => app.listen(port, () => console.log(`App is listening on port: ${port}`))) .catch((error) => console.log(error))

§Handling The Database Connection and The User Schema

The database I'm using for this project is Mongodb Atlas. It offers a free plan that you can use for your projects. You can find a guide to creating your own database here. Mongoose package makes it much easier to connect and interact with your database. I will be using mongoose in this project as well. I'm also keeping my Mongodb URL in a .env file in my project root.

You can test your connection by running the "npm run dev" command in console. If everything is alright you should be seeing "App is listening on port: 5000".

Ok let's create our user schema now. Think of the schema as a way of letting the database know how you wanna interact with it. You are basically describing what kind of data you wanna save. In your project root create a "models" folder, in that folder we can create a model. For our project we need a "user.js" file. The code is here:

const mongoose = require('mongoose'); const { Schema } = mongoose; const UserSchema = new Schema({ email: { type: String, required: true }, password: { type: String, required: true } }) const User = mongoose.model("User", UserSchema); module.exports = User

§Creating Our Keys

In the root the project create a file for generating the public and private keys. Private key will be used to sign the tokens. Your client shouldn't know about this key. Public key will be used to verify the user for later auth activities. So go ahead and create your file, I named mine genKeyPair.js. This is the full code:

const fs = require('fs'); const crypto = require('crypto'); const genKeyPair = () => { const keyPair = crypto.generateKeyPairSync('rsa', { modulusLength: 4096, publicKeyEncoding: { type: 'pkcs1', format: 'pem' }, privateKeyEncoding: { type: 'pkcs1', format: 'pem' } }); // The keys will be separate files fs.writeFileSync(__dirname + '/id_rsa_pub.pem', keyPair.publicKey); fs.writeFileSync(__dirname + '/id_rsa_priv.pem', keyPair.privateKey) } genKeyPair();

Ok now run:

node genKeyPair.js

command to create the keys.

§Handling Registration

Our files will be very basic although you can do any sort of improvement you want. Just create a public folder in your project root and we can start. Inside the public folder don't forget to create a "js" folder and a style.css file. They will be necessary to handle the logic and make our app look a little better. There is a lot of room for improvement in every step, so if you feel like you can do an improvement go for it. I will be sharing with you all the boring boilerplate to get started here so don't worry.

In our home.html don't forget to reference necessary the js and css files, full code is here:

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" type="text/css" href="style.css"> <title>Document</title> </head> <body> <nav> <a href="/">HOME</a> <ul> <li><a href="register">Register</a></li> <li><a href="/login">Login</a></li> <li><a href="/secret">Secret Page</a></li> </ul> </nav> <div class="home-wrapper"> <h1>Welcome to he home page of our awesome website</h1> <p>Lorem ipsum, dolor sit amet consectetur adipisicing elit. Sint eum et quam excepturi corrupti, ipsa illum eligendi aut! Labore molestiae fuga incidunt. Recusandae, consequatur consectetur? Aliquam eligendi deserunt doloribus voluptatum. Eligendi rem repellendus dolores laboriosam dolorum quia vero nihil perferendis id odio, quas molestiae, vitae exercitationem autem voluptates! Eius illum aliquid tenetur sapiente minus quo, consequatur magni repudiandae. Quaerat, necessitatibus! Ea soluta repellendus modi ab officia quae porro dolorem cum nulla. Aspernatur dolore tenetur corporis deserunt consequuntur omnis voluptate tempora eos facilis ipsum. Aspernatur ad laudantium, harum dolor autem eius. Reprehenderit, aliquam distinctio. Vel tenetur dolore praesentium fugit nam iste totam amet nulla voluptatibus. Eaque reiciendis reprehenderit, eos suscipit omnis id at vel accusantium earum explicabo, delectus dignissimos possimus incidunt! Aperiam optio perferendis natus, vel tempore quaerat assumenda maxime voluptate voluptates rerum suscipit? Asperiores, ea deleniti! Amet similique, provident ipsam error ad nostrum corporis, eligendi ea dignissimos dicta, quia aperiam?</p> </div> </body> </html>

The css for this project is nothing but basic, full code:

* { margin: 0; padding: 0; box-sizing: border-box; } body { display: flex; flex-direction: column; align-items: center; background: rgb(94, 139, 162); color: rgb(255, 255, 255); } p { line-height: 1.7; padding-top: 20px; } .home-wrapper { width: 80%; padding: 50px; background: rgba(0,0,0,0.7); margin: 10px; border-radius: 20px; } nav { background: olivedrab; display: flex; flex-direction: row; align-items: flex-start; width: 100%; padding: 20px; } ul { margin-left: auto; display: flex; flex-direction: row; list-style: none; } li { margin-left: 10px; } a { color: azure; font-size: 1.2rem; text-decoration: none; } .login-wrapper, .register-wrapper { margin-top: 20px; background: rgba(0,0,0,0.7); padding: 20px; border-radius: 20px; } .login-form, .registery-form { display: flex; flex-direction: column; min-width: 600px; } .login-form > input, .registery-form > input { padding: 20px; font-size: 1.6rem; border-radius: 20px; } .login-submit-btn, .registry-submit-btn { margin-top: 10px; border:none; background: rgba(234, 97, 170, 0.7); transition: 0.4s; } .login-submit-btn:hover, .registry-submit-btn:hover { cursor: pointer; background: rgba(234, 97, 170, 1); } .secret-wrapper { width: 80%; margin-top: 20px; background: rgba(0,0,0,0.7); padding: 50px; border-radius: 20px; }

Now to create our register page and the logic to create a user. Most of the logic will be handled in our js files for creating and logging in a user:

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" type="text/css" href="style.css"> <script defer src="./js/registry.js"></script> <title>Document</title> </head> <body> <nav> <a href="/">HOME</a> <ul> <li><a href="register">Register</a></li> <li><a href="/login">Login</a></li> <li><a href="/secret">Secret Page</a></li> </ul> </nav> <div class="register-wrapper"> <h1>Create an account</h1> <p class="register-error"></p> <form class="registery-form"> <p>Email:</p> <input placeholder="example@gmail.com" name="email" id="femail" type="email" > <p>Password:</p> <input id="fpassword" name="password" type="password" > <p>Rewrite Password:</p> <input id="fpassword2" name="password2" type="password" > <input class="registry-submit-btn" type="submit" value="SUBMIT"> </form> </div> </body> </html>

The logic for handling the user creating form:

const regForm = document.querySelector('.registery-form'); const password = document.querySelector('#fpassword'); const password2 = document.querySelector('#fpassword2'); const email = document.querySelector('#femail'); regForm.addEventListener('submit', async (event) => { event.preventDefault() // stop the refreshing of page try { // send the user info in body to our route const response = await fetch('/register', { method: 'POST', body: JSON.stringify({ email: email.value, password: password.value, password2: password2.value }), headers: { 'Content-Type': 'application/json' }, }); const data = await response.json(); // get the token created const { expiresIn } = await data; const { token } = await data // remove the existing token localStorage.removeItem("id_token"); localStorage.removeItem("expires_at"); // the token will expire in 24 hours const expiresAt = new Date.now() + Number.parseInt(expiresIn) * 86400 * 1000; // day // we will be saving our token in local storage localStorage.setItem('id_token', token); localStorage.setItem('expires_at', JSON.stringify(expiresAt.valueOf())); // and if everything is ok redirect the user to our home page if(data){ window.location = '/'} } catch (error) { // catch any error before breaking the app console.log("error in registry ==>",error) } })

Now that we have seen the HTML side of the registration, we can go into the backend to see how we are dealing with user info. The user info sent will be used here for creating our token to be used later in user authentication.

In the root of your project create a routes folder and in that folder create index.js for handling the backend logic this is where the magic happens:

const express = require('express'); const jsonwebtoken = require('jsonwebtoken'); const router = express.Router(); const path = require('path'); const bcrypt = require('bcrypt'); const fs = require('fs'); const passport = require('passport'); const User = require('../models/user.js') // Private key is necessary for issuing the jwt const pathToKey = path.join(__dirname, "../", 'id_rsa_priv.pem') const PRIV_KEY = fs.readFileSync(pathToKey, 'utf8'); // This function is for issuing our jwt const issueJWT = (user) => { const _id = user._id; const expiresIn = '1'; const payload = { sub: _id, iat: Date.now() }; const signedToken = jsonwebtoken.sign(payload, PRIV_KEY, { expiresIn: expiresIn, algorithm: 'RS256' }); return { token: "Bearer " + signedToken, expires: expiresIn } //that simple } // This function will be used to hash the plain password given // by the user, it's not safe at all. const genPassword = async (plaintextPassword) => { const saltRounds = 10; const hash = await bcrypt.hash(plaintextPassword, saltRounds) return hash }; // well we need to serve the home page to create our navbar router.get('/', (req, res) => { res.sendFile(path.dirname(__dirname) + "/public/home.html"); }) // render the form router.get('/register', (req, res) => { res.sendFile(path.dirname(__dirname) + "/public/register.html"); }) // Our post request will be handled here router.post('/register', (req, res) => { const { email, password, password2 } = req.body; // You can check the info sent here console.log("these are recieved", email, password, password2) // If there's no user info sent for some reason if (!req.body.email) { console.log("No info sent") return } // if the passwords don't match if (password !== password2) { res.status(401).json({ error: "Passwords are not identical!" }) return } // Now we are looking for the user in our database by the email property User.findOne({ email: email}, async (err, data) => { if(err) { return res.status(400).json({ error: `an error occured: ${err}` }) } else if(data) { // if the email already exists in our database return res.status(400).json({ error: "User exists in database" }) } else { // If there's no such email in the database // we can't just save the password given by the user // we need to hash it using our fucntion above const passwordToSave = await genPassword(password); // Create the user const newUser = new User({ email: email, password: passwordToSave }); // and finally save it to our database newUser.save() .then(async user => { console.log("registered new user") // Issue the jwt with created user's id const jwt = await issueJWT(user) // Send back all the information to our frontend res.status(201).json( { user: { email: user.email, }, token: jwt.token, expiresIn: jwt.expires } ).catch(console.log) // in case of an error }) } }) }) module.exports = router;

There we go! We are creating our user and loging them in as well after creation.

§Now To Take Care of The User Login

We need to be able to login our user of course. We will start with the HTML file and create our backend after that:

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" type="text/css" href="style.css"> <script defer src="./js/login.js"></script> <title>Document</title> </head> <body> <nav> <a href="/">HOME</a> <ul> <li><a href="register">Register</a></li> <li><a href="/login">Login</a></li> <li><a href="/secret">Secret Page</a></li> </ul> </nav> <div class="login-wrapper"> <h1>Login</h1> <p class="login-error"></p> <form class="login-form"> <p>Email:</p> <input type="email" id="l-email" name="email" placeholder="example@gmail.com" > <p>Password:</p> <input type="password" id="l-password" name="password" > <input class="login-submit-btn" type="submit" value="LOGIN"> </form> </div> </body> </html>

And the logic to handle the login form submit:

const loginForm = document.querySelector('.login-form'); const email = document.querySelector('#l-email'); const password = document.querySelector('#l-password'); loginForm.addEventListener('submit', async (event) => { event.preventDefault(); try { const response = await fetch('/login', { method: 'POST', body: JSON.stringify({ email: email.value, password: password.value }), headers: { 'Content-Type': 'application/json' }, }) const data = await response.json(); const { expiresIn } = await data; const { token } = await data localStorage.removeItem("id_token"); localStorage.removeItem("expires_at"); const expiresAt = Date.now() + Number.parseInt(expiresIn) * 86400 * 1000; // day localStorage.setItem('id_token', token); localStorage.setItem('expires_at', JSON.stringify(expiresAt.valueOf())); if(data){ window.location = '/'} } catch (error) { console.log("error in login", error) } })

In our routes/index.js file we need to serve the HTML files and handle the post request. So we can just add the following lines below the previous lines we wrote for user registery:

// serving our html file of course router.get('/login', (req, res) => { res.sendFile(path.dirname(__dirname) + "/public/login.html"); }) router.post('/login', (req, res) => { // grabbing the values we want from the request body const { email,password } = req.body; if(!email) { // You can create an error message for the users for // now we will just go with logging a mesage console.log("no info sent") return } // Look for the user in the database User.findOne({ email: email}, async (err, user) => { if(err){ return res.status(400).json({ error: `an error occured: ${err}`}) } else if (!user) { // If there's no user res.status(400).json({ error: "unknown username"}) } else { // Found the user, compare the passwords const valid = await bcrypt.compare(password, user.password); // If passwords do not match if (!valid) { console.log("it's not valid") res.status(400).json({ error: "Wrong password"}) } else { // eveything works so far const tokenObj = issueJWT(user) res.status(200).json({ user: { username: user.username, email: user.email, createdAt: user.createdAt }, token: tokenObj.token, expiresIn: tokenObj.expires } ); } } }) });

Now we are able to create and login a user. We should be able to do something with all the functionality we have created so far.

§A Secret Page For Logged In Users

We can go ahead and create a secret page for our users. What I did was very basic, just a message indicating that the user's logged in.

We are going to get the information from local storage and check for validation. Feel free add whatever logic you want here:

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" type="text/css" href="style.css"> <script defer src="./js/profile.js"></script> <title>Document</title> </head> <body> <nav> <a href="/">HOME</a> <ul> <li><a href="register">Register</a></li> <li><a href="/login">Login</a></li> <li><a href="/secret">Secret Page</a></li> </ul> </nav> <div class="secret-wrapper"> <div id="user-email"></div> </div> </body> </html>

And of course the mighty javascript to make things happen:

const profileEmail = document.querySelector("#user-email"); const idToken = localStorage.getItem("id_token"); const expiresAt = localStorage.getItem("expires_at"); // Check if the token is expired if(idToken && expiresAt >= Date.now()){ const text = "The token exists and still valid, feel free to use it with the public key!" const h1 = document.createElement("h1") h1.innerText = text profileEmail.appendChild(h1) }

Ok now create a secret-routes folder in the root of our project and in that folder create an index.js file.

Here I'm only returning the .html file. There are so many things you can do here using our jwt.

You can handle the authorisation here using passport package. You can modify the app so the client sends the token in the authorisation header and in the backend, check the validity using the public key we created. I'm not including that part here because I know from experince that we only learn after we struggle to understand the logic behind the technologies we use. Give this page a look. It won't take long and trust me you will thank me afterwards.

§Conclusion and Next Steps

So far we have seen how to create a user, hash the password, create public and private keys for jwt operations. That's a lot of work, congrats!

Seeing how all these things work together will make you a better, more confident developer. It was definitely the case for me. Even though there are so many services taking care of auth processes for very reasonable prices it's always good to know what's going on behind the scenes.

Ilker Akbiyik