First, install bcrypt
and jsonwebtoken
as dependencies and @types/bcrypt
and @types/jsonwebtoken
as dev dependencies in your Express.js app. We'll use bcrypt
to hash passwords and jsonwebtoken
to create secure tokens in our Express server.
Once installed, run openssl rand -base64 32
in your terminal to create a random string, copy the string, and create an environment variable named JWT_SECRET
in your project. After that, within your /lib/utils
directory, create a setCookie
function with the following code:
import { Types } from 'mongoose';
import { Response } from 'express';
import jwt from 'jsonwebtoken';
export function setCookie(res: Response, _id: Types.ObjectId) {
const jwtToken = jwt.sign({ _id }, process.env.JWT_SECRET as string, {
expiresIn: '7d',
});
res.cookie('token', jwtToken, {
path: '/',
httpOnly: true,
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000, // 1 week
secure: process.env.NODE_ENV !== 'development',
});
}
The above function accepts two parameters. The res
parameter is the Express Response
object, and the _id
parameter is the user ID which we'll store in the JSON web token.
Within the function, we create a jwtToken
variable by using the jwt.sign
method. This method accepts three parameters. The first parameter is the information we want to store in the token, the second parameter is the JWT secret, and the third parameter is an options object. In the options object, we set expiresIn
to '7d', which expires the token in a week.
After that, we use the res.cookie
method to set the cookie to the response header. This method accepts three parameters. The first parameter is the cookie name, the second parameter is the cookie content, and the third parameter is an options object.
The options object is important as we set the security parameters here. Within this object, we set the value of path
to /
, which defines the scope of the cookie. Next, we set the value of httpOnly
to true
, which makes the cookie inaccessible by client-side JavaScript. After that, we set the value of sameSite
to strict
, which means that the browser only sends the cookie with requests from the cookie's origin server. Next, we set the value of maxAge
to seven days to match our token expiration. Finally, we set the value of secure
to true
when the Node Environment is 'production'.
Now, within your routes
directory, create a user.ts
file with the following code:
import { Router } from 'express';
const router = Router();
export default router;
Then, navigate to your app.ts
file, import the user
router, and add it as the middleware like the following code:
import User from './routes/user';
app.use('/users', User);
Now, get back to the user
router and create a /register
route by adding the following code:
import bcrypt from 'bcrypt';
import User from '../models/user';
import { setCookie } from '../lib/utils';
router.post('/register', async (req, res) => {
const { name, email, password } = req.body;
if (!name || !email || !password) {
console.log('Name, email, or password is missing');
res.status(400);
throw new Error('Name, email, or password is missing');
}
try {
const salt = await bcrypt.genSalt(10);
const hashedPassword = await bcrypt.hash(password, salt);
const response = await User.create({
name,
email,
password: hashedPassword,
});
const user = response.toObject();
setCookie(res, user._id);
res.status(201).json(user);
} catch (err) {
console.log(err);
throw err;
}
});
In this code, within the controller function, we destructure the name
, email
, and password
from the request body and check if all three fields are provided.
After that, we create a salt
which is required to hash the password. Next, we hash the password by using the bcrypt.hash
method. After that, we create a user in the database, convert the response to an object, set the cookie to the response header, and send the user with the response.
Now, let's create a /login
route with the following code:
router.post('/login', async (req, res) => {
const { email, password } = req.body;
if (!email || !password) {
console.log('Email or password is missing');
res.status(400);
throw new Error('Email or password is missing');
}
try {
const user = await User.findOne({ email });
if (!user) {
console.log('Invalid credentials');
res.status(403);
throw new Error('Invalid credentials');
}
const isCorrectPassword = await bcrypt.compare(password, user.password);
if (!isCorrectPassword) {
console.log('Invalid credentials');
res.status(403);
throw new Error('Invalid credentials');
}
setCookie(res, user._id);
res.status(200).json(user);
} catch (err) {
console.log('Invalid credentials');
res.status(403);
throw new Error('Invalid credentials');
}
});
In the above code, within the controller function, we destructure email
and password
from the request body and check if both fields are provided. Next, we do a database query to find the user with the provided email address. If no user is found, we throw an error. Otherwise, we compare the provided password with the hashed password using the bcrypt.compare
method. If the passwords don't match, we throw an error. Otherwise, we use the setCookie
function to set the cookie to the response header and send the user with the response.
Finally, we need a route to log out a user. So, let's create a /logout
route with the following code:
router.post('/logout', async (req, res) => {
res
.clearCookie('token', {
path: '/',
maxAge: 0,
httpOnly: true,
sameSite: 'strict',
secure: process.env.NODE_ENV !== 'development',
})
.end();
});
In the above code, we use the res.clearCookie
method to remove a cookie from the response header. This method accepts two parameters. The first parameter is the name of the cookie you want to remove. The second parameter is an options object. The options object must be identical to the options object we've used in the setCookie
function. Except, we set the value of the maxAge
to '0' here which invalidates the cookie. Finally, we chain the end
method to end the response.
That's it! Your Express app now has a secure authentication system. Check out the next article of this series, Protect API routes using the auth middleware in Express.js to learn about creating an auth middleware to parse cookies and protecting API routes in your Express server. Cheers!