How to store JWT token as an HTTPOnly Cookie
— JWT, Web Development, NodeJS, Axios, FetchAPI — 5 min read
In this tutorial, we'll learn how to manage HTTPOnly cookies from the server/backend/API using the Set-Cookie
HTTP Response header. We'll also learn how to use HTTPOnly cookies to store JWT tokens and how we can configure the front-end code to automatically send them with every request.
HTTPOnly cookies are created and managed by the server and are not accessible by client-side JavaScript code. Also, if enabled, the browser automatically sends these cookies in the Cookie
HTTP header with every request to the server. This makes HTTPOnly cookies a good option to store JWT tokens obtained after successful authentication so that these tokens can be sent with every subsequent API request to the server automatically without needing any additional code to send it explicitly.
Since cookies work only in the browser, this strategy for storing JWT tokens in an HTTPOnly cookie only works for web applications. For mobile applications, we have to store JWT tokens differently and send it explicitly in the
Authorization
HTTP Request header for every API request.
Tutorial Project Setup
We'll be making use of a Node.js and ExpressJS based API server and a Vite-powered vanilla JavaScript frontend.
This tech stack is just for demonstration purposes. The concepts taught in this tutorial can be used with any language, framework or library of your choice.
We'll be making our AJAX calls using the native Fetch API and the very popular 3rd-party library Axios.
You can download or clone the tutorial project setup from this github repo.
Once you have the starter files, open your terminal go to the /client
and /server
folders and run npm i
on both to install the respective NPM dependencies.
Next, again in your terminal go to /client
and run npm run dev
and for /server
run npm run start
. This should start a client localhost server at http://localhost:5173/
and a back-end localhost server at http://localhost:3000/
( provided you don't have anything else running on that port ).
If you open /server/index.js
, you'll notice we have included the cors
NPM package which will be used to configure the server to allow cross-domain requests from our client.
We have also included the cookie-parser
NPM package and middleware which is used to read cookies from Cookie
HTTP Request header into req.cookies
.
Now if you open /client/main.js
, you'll notice we have included the axios
package.
Alright! That's it for the setup and we're good to go. Lets dive straight into the tutorial.
Configure CORS
The configuration for the cors()
middleware looks like this:
const corsOptions = { origin: "http://localhost:5173", credentials: true};app.use( cors(corsOptions) );
We're allowing requests to originate from the our client localhost server. Also the credentials
property sets the Access-Control-Allow-Credentials
HTTP Response header to true
which tells browsers that the response should be exposed to the front-end JavaScript code.
Add the Set-Cookie
Header to the API Response
We have a single API service on the root endpoint. Pay special attention to the declaration of the options
object where we specify the configuration for the HTTPOnly cookie which we'll set later using res.cookie()
.
app.get( "/", ( req, res ) => { // Our `token` cookie will be parsed into `req.cookies.token` console.log( "🍪", req.cookies ); // Configure the `token` HTTPOnly cookie let options = { maxAge: 1000 * 60 * 15, // expire after 15 minutes httpOnly: true, // Cookie will not be exposed to client side code sameSite: "none", // If client and server origins are different secure: true // use with HTTPS only }
const token = "abcd.123456.xyz"; // dummy JWT token res.cookie( "token", token, options ); res.send( "Cookie has been set!" );});
Make AJAX call using Fetch API
Now lets turn out attention to the front-end code. Open the /client/main.js
. Our first AJAX call is implemented using the Fetch API. Again pay special attention to the options that we pass to fetch()
. We pass the credentials
property and set it to "include"
. This is required to send HTTPOnly cookies automatically in every request as a part of the Cookie
HTTP request header.
fetch( "http://localhost:3000/", { method: "get", credentials: "include"}) .then( response => response.text()) .then( data => console.log( "Response using Fetch: ", data ) );
Make AJAX call using Axios
We have a second AJAX call implemented using Axios. Again the thing to note here is that we are telling the browser to send HTTPOnly Cookies automatically with every request by setting the withCredentials
property to true
.
axios( "http://localhost:3000/", { withCredentials: true}) .then( response => response.data ) .then( data => console.log( "Response using Axios: ", data ) );
Ok we're all set to test these changes out so lets go ahead and run both the servers using npm run dev
on the client and npm run start
on the server.
Open the client URL in the browser, open Dev Tools and look at the Console. You should see logs from the fetch and axios calls. This means both calls worked as expected.
Now head over to the Application/Storage tab in the Dev Tools and you should also see the token
HTTPOnly cookie. Awesome! Seems like our API script was successfully able to create this cookie using res.cookie()
.
Go ahead and refresh the page with the Dev Tools still open. Go to the Network tab. Analyse both the API requests that are sent from the browser specifically the Request Headers. Notice that the Cookie
request header has been sent along with the request and it contains our token
cookie.
If you check out the server console, you should see the token cookie object printed as well. This confirms that not only are we able to set cookies, but it is also automatically being sent in every request from the browser to the API. Woo hoo! 🎉
Hope this helps!🙏