Published in javascript
Nodejs JWT Authentication With HTTP Only Cookie
By Atakan Demircioğlu
Fullstack Developer
What is HTTP Only Cookie?
What is HTTP Only Cookie?
- HttpOnly is an additional flag included in a Set-Cookie HTTP response header.
- If the HttpOnly flag is included in the HTTP response header, the cookie cannot be accessed through the client-side script (if the browser supports it).
- It is not accessible with document. cookie with JS and you can just send it to the server. (if the browser supports it).
- As a result, even if a cross-site scripting (XSS) flaw exists, and a user accidentally accesses a link that exploits this flaw, the browser will not reveal the cookie to a third party.
- If a browser does not support HttpOnly and a website attempts to set an HttpOnly cookie, the HttpOnly flag will be ignored by the browser, thus creating a traditional, script-accessible cookie.
What is JWT? (JSON Web Token)
- JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object.
- This information can be verified and trusted because it is signed digitally.
- JWT tokens can be signed with a secret.
- You can use JWT for Authorization or Information Exchange.
Real Word Node.js Application
Let's assume you have a basic login page and you are sending a username and password to an endpoint named as /auth or etc.
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
import { PrismaClient } from '@prisma/client';
import HTTPMethod from 'http-method-enum';
import HTTP_STATUS_CODES from 'http-status-enum';
import { serialize } from 'cookie';
const prisma = new PrismaClient();
const KEY = process.env.JWT_KEY;
export default async function authenticate(req, res) {
const { method } = req;
try {
switch (method) {
case HTTPMethod.POST:
const { email, password } = req.body;
if (!email || !password) {
return res.status(HTTP_STATUS_CODES.BAD_REQUEST).json({
status: 'error',
error: 'Request missing email or password',
});
}
const user = await prisma.user.findUnique({
where: {
email: email,
},
});
if (!user) {
return res
.status(HTTP_STATUS_CODES.BAD_REQUEST)
.json({ status: 'error', error: 'User Not Found' });
}
bcrypt.compare(password, user.password).then((isMatch) => {
if (isMatch) {
const payload = {
id: user.id,
email: user.email,
createdAt: user.createdAt,
username: user.username,
fullname: user.fullname,
};
jwt.sign(
payload,
KEY,
{
expiresIn: 60 * 60 * 24 * 30,
},
(_err, token) => {
const serialized = serialize('token', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 60 * 60 * 24 * 30,
path: '/',
});
res.setHeader('Set-Cookie', serialized);
res.status(HTTP_STATUS_CODES.OK).json({
success: true,
user: {
email: payload.email,
username: payload.username,
fullname: payload.fullname,
},
});
},
);
} else {
res.status(HTTP_STATUS_CODES.BAD_REQUEST).json({
status: 'error',
error: 'Password and email does not match.',
});
}
});
break;
}
} catch (error) {
console.log(error);
res.status(HTTP_STATUS_CODES.INTERNAL_SERVER_ERROR).json({
status: 'error',
error: 'Internal Server Error',
});
}
}
- You can create the file under /api/ named as auth.js
- I am just prepared a basic example at it includes Prisma (ORM), bcrypt and etc. (maybe you don’t need it)
- Line 46: We are signing the payload with our secret (stored in the .env file)
- Line 53: We serialize the token with some settings.
- httpOnly means this is a HTTP only cookie, secure means HTTPS (in development just use HTTP, maxAge is for giving an expiration time)
- sameSite: ‘strict’ : Cookies will only be sent in a first-party context and not be sent along with requests initiated by third-party websites.
- Line 60: Setting HTTP-only cookie with HTTP response header
So how about the logout process?
import { serialize } from 'cookie';
export default function logout(req, res) {
const { cookies } = req;
const jwt = cookies.token;
if (!jwt) {
return res.status(401).json({
status: 'error',
error: 'Unauthorized',
});
}
const serialized = serialize('token', null, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: -1,
path: '/',
});
res.setHeader('Set-Cookie', serialized);
res.status(200).json({
status: 'success',
message: 'Logged out',
});
}
It is a little different but the logic is the same. We just need an endpoint like /logout and just need to set up maxAge -1 or just 0.
I just tried to explain the logic with a basic example. You can improve the security part or the logic whatever you want.
You can also my article about the usage of next-js middleware.