Introduction
Authentication and authorization are crucial concepts in securing access to resources and data in any application. In this comprehensive guide, we will learn how to implement user authentication and authorization in a Node.js application using JSON Web Tokens (JWT).
We will build a simple Node.js API with user registration, login and restricted routes for authenticated users only. By the end, you will have a solid understanding of how to properly structure authentication in a Node.js app.
What is Authentication and Authorization?
Before we dive into the implementation, let‘s briefly go over the core concepts.
Authentication is the process of verifying a user‘s identity. It is usually done by submitting credentials like a username/email and password. The server then checks if the credentials match a registered user in the database. If valid, the user is authenticated.
Authorization controls access to resources and data. Even if a user is authenticated, they should only be authorized to access resources they have permission for. For example, admins can access admin-only pages while regular users cannot.
Authentication verifies identity, while authorization verifies permissions.
Why JWT?
There are different ways to implement authentication in Node.js:
-
Sessions – Store user data on the server, often in a database. The client receives a session ID cookie to identify their server-side session.
-
JSON Web Tokens (JWT) – Stateless authentication mechanism. The server generates a token that is stored client-side, often in localStorage or cookies. The token verifies the user identity on future requests.
JWTs have some advantages over sessions:
-
Stateless – No session data is stored server-side. The token contains all the user data encoded in it.
-
Decoupled – Separates authentication from authorization, handling them independently.
-
Mobile/SPA friendly – Easy to store tokens on the client side like in localStorage.
-
Scalable – Stateless architecture scales better for distributed systems.
For these reasons, JWTs are commonly used for authentication in modern applications.
JWT Structure
A JWT consists of three parts:
-
Header – Metadata about the token type and the signing algorithm.
-
Payload – Contains claims which are statements about the entity (user). Can contain custom claims.
-
Signature – Generated by encoding the header and payload together with a secret key. Ensures the token is not altered client-side.
This results in a JWT that looks like:
xxxxx.yyyyy.zzzzz
The three parts are URL safe base64 strings separated by dots. The signature ensures the JWT can be verified as authentic by the server.
Some standard claims for the payload include:
- iss – Issuer
- exp – Expiration time
- sub – Subject (user ID)
Project Setup
We will build a simple Node.js app with Express and MongoDB.
To follow along, create a new directory and initialize a Node project:
npm init -y
Install the required packages:
npm install express mongoose jsonwebtoken bcryptjs dotenv
- express – web framework
- mongoose – MongoDB ODM
- jsonwebtoken – generate JWTs
- bcryptjs – hash passwords
- dotenv – load environment variables
User Model
Start by defining a Mongoose model for the User.
In models/User.js:
const mongoose = require(‘mongoose‘);
const UserSchema = new mongoose.Schema({
name: {
type: String,
required: true
},
email: {
type: String,
required: true,
unique: true
},
password: {
type: String,
required: true
}
});
module.exports = mongoose.model(‘User‘, UserSchema);
This will represent a user with name, email, and password properties.
The email field is set to be unique so duplicate emails cannot be added.
Connect to MongoDB
In config/db.js, connect to a MongoDB database:
const mongoose = require(‘mongoose‘);
const connectDB = async () => {
try {
const conn = await mongoose.connect(process.env.MONGO_URI);
console.log(`MongoDB Connected: ${conn.connection.host}`);
} catch (error) {
console.log(error);
process.exit(1);
}
}
module.exports = connectDB;
This will connect using a MongoDB connection string from the environment variables.
Load the .env file to access the variables:
MONGO_URI=mongodb://localhost:27017/nodeauth
Then call connectDB() in the main app.js file.
Register User
The first API route we need is a user registration endpoint.
In routes/auth.js:
const router = require(‘express‘).Router();
const User = require(‘../models/User‘);
const bcrypt = require(‘bcryptjs‘);
// Register route
router.post(‘/register‘, async (req, res) => {
try {
// Destructure name, email and password from request body
const { name, email, password } = req.body;
// Check if user exists
const userExists = await User.findOne({ email });
if (userExists) {
return res.status(400).json({ msg: ‘User already exists‘ });
}
// Create new user
const user = new User({
name,
email,
password
});
// Hash password
const salt = await bcrypt.genSalt(10);
user.password = await bcrypt.hash(password, salt);
// Save to DB
await user.save();
res.json({ msg: ‘User created successfully‘ });
} catch (error) {
console.log(error);
res.status(500).json({ msg: ‘Server error‘ });
}
});
module.exports = router;
This will:
- Check if user already exists with given email
- Create a new User object with name, email and plaintext password
- Generate a salt and hash the password
- Save the user to the database
We hash the passwords for security before storing them.
Login User
To log a user in, we need to validate the login credentials they submit.
In routes/auth.js:
// Login route
router.post(‘/login‘, async (req, res) => {
try {
// Destructure email and password from request body
const { email, password } = req.body;
// Check if user doesn‘t exist
const user = await User.findOne({ email });
if (!user) {
return res.status(400).json({ msg: ‘User does not exist‘ });
}
// Check password match
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
return res.status(400).json({ msg: ‘Invalid credentials‘ });
}
res.json({ msg: ‘Login successful‘ });
} catch (error) {
console.log(error);
res.status(500).json({ msg: ‘Server error‘ });
}
});
This will:
- Check if user exists with given email
- Compare the submitted password with the hashed password in DB
- If valid credentials, return a success response
This confirms the user has provided valid credentials.
Generate JWT
After authenticating the user, we want to generate a JWT to identify authenticated requests.
In routes/auth.js, import jwt:
const jwt = require(‘jsonwebtoken‘);
Define a function to generate a JWT:
const generateToken = (id) => {
return jwt.sign({ id }, process.env.JWT_SECRET, {
expiresIn: ‘1d‘
});
}
This signs a new JWT, passing the user id as the payload.
The token expires in 1 day. The secret key is set in the .env file:
JWT_SECRET=abc123
Update the login route to return the JWT on success:
// Login route
router.post(‘/login‘, async (req, res) => {
try {
// Authenticate user
const token = generateToken(user._id);
res.json({
token,
msg: ‘Login successful‘
});
} catch (error) {
// rest of code
}
});
The client can now save the token locally or in cookies and use it for authenticated requests.
Protect Routes
To authorize based on authentication, we need middleware to verify the JWT before accessing protected routes.
In middleware/auth.js:
const jwt = require(‘jsonwebtoken‘);
const auth = (req, res, next) => {
try {
// Get token from header
const token = req.header(‘x-auth-token‘);
if (!token) {
return res.status(401).json({ msg: ‘No token, authorization denied‘ });
}
// Verify token
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// Add user from payload
req.user = decoded.user;
next();
} catch (e) {
res.status(400).json({ msg: ‘Token is not valid‘ });
}
};
module.exports = auth;
This middleware will:
- Get the JWT token from the request headers
- Verify the token using the secret key
- Decode the payload to get the user data
- Attach the user to the request object
- Call next() to execute the route handler
Now we can protect any routes that require authentication.
// Protected route
router.get(‘/profile‘, auth, (req, res) => {
// Access user from req.user
res.json(req.user);
});
The auth middleware will check the token before the handler.
If no token or an invalid one is sent, it will return an error response.
Conclusion
And that‘s it! We have a basic Node.js authentication flow using JSON Web Tokens.
Main points:
-
Install required packages like express, mongoose, bcryptjs, jsonwebtoken
-
Create a Mongoose model and schema for the User
-
Hash passwords before saving new users to the database
-
Implement registration and login logic with input validation
-
Use JWTs to authenticate logged in users
-
Protect routes by verifying the token
JWTs provide a great way to handle authentication in Node.js. They reduce boilerplate authentication code and seamlessly work with MongoDB.
For production apps, you would also want to implement:
- Password reset flow
- Email verification during signup
- Better error handling
- Refresh tokens to get a new JWT
This was a simple example to demonstrate the core concepts. You can build on it to support any additional features you need.
I hope this guide gives you a good foundation on implementing authentication with Node.js and JWT! Let me know if you have any other questions.