Part 3/3: How to Implement Refresh Tokens through Http-Only Cookie in NestJS and React
Hello everyone,
Welcome to the final episode of our three-part series on token management in a NestJS + React application. In the first two posts, we walked through the process of implementing refresh token logic by storing tokens in local storage. Today, we’ll advance to a more secure method by implementing HTTP-only cookies for refresh tokens.
Why Use HTTP-Only Cookies?
HTTP-only cookies allow us to store sensitive data, such as refresh tokens, in a way that cannot be accessed by JavaScript. This means that even if there are vulnerabilities in your code or third-party libraries, a hacker won't be able to retrieve the refresh token.
However, even with HTTP-only cookies, there's still a risk of requests being made on behalf of the user through XSS attacks. The key advantage is that hackers will not be able to access the value of the refresh token directly; they would need to execute targeted XSS attacks on the application's endpoints, which requires prior knowledge of the system.
Why Not Store Both Access and Refresh Tokens in HTTP-Only Cookies?
By keeping the access token out of cookies, we protect against CSRF (Cross-Site Request Forgery) attacks. To mitigate XSS attacks, we set a short expiry time on the access token, limiting the damage even if it’s compromised.
Should We Worry About CSRF on the /refresh-tokens Endpoint?
Not really. Since this endpoint doesn’t compromise the system or user data, CSRF attacks here are less of a concern.
Enhancing Security: Hashing Refresh Tokens
To further improve security, we also hash the refresh tokens in the database. We use a hashing algorithm without salt, allowing us to reproduce the same hash for previously used tokens to check if they have been blacklisted. With this improvement, in case of database leaks, the hacker will not be able to read any refresh tokens!
Getting Started
To begin, ensure you've completed the guides in Part 1 and Part 2 for app installation. Afterward, follow these steps to implement the necessary changes.
If you want to jump straight into the code, you can check out the repository here on the "part-3" branch.
Step 1: Define Cookie Configuration and Helper Function
First, set up a cookie configuration and create a helper function to extract the refresh token from cookies:
Step 2: Enable CORS to Accept Cookies
Ensure the CORS policy allows credentials so that our backend can receive cookies from the frontend:
Step 3: Update the Token Generation Method
Modify the generateTokenPair
method to set the refresh token as an HTTP-only cookie whenever it's called:
Step 4: Update JWT Strategy to Use Cookies
Modify the refresh JWT strategy to retrieve the token from cookies instead of using bearer token authentication:
Step 5: Adjust Token Handling and Add Cookie Clearing Endpoint
When calling the /refresh-tokens
endpoint, the token should now be extracted from the HTTP-only cookie, replacing the previous method of using Bearer authorization.
Additionally, we must implement a /clear-auth-cookie
endpoint to remove the cookie. This endpoint should be invoked during the logout action on the frontend. The reason for this is that HTTP-only cookies cannot be cleared programatically from the frontend, so this ensures the user is properly logged out.
Step 6: Update Frontend Logic
Now regarding the frontend project, we will actually simplify the logic a little bit.
- Remove Refresh Token from Local Storage:
Clear any references to the refresh token in the AuthClientStore
class:
- Update Request Methods:
Remove the refreshToken
variable from the sendProtectedRequest
method, as it's no longer needed.
- Include Credentials in Requests:
Very important: Update the login
and refresh token
API integration methods to include credentials, ensuring that cookies can be set and sent correctly.
- Define Clear Auth Cookie API Call:
Implement the clearAuthCookie
API call:
- Call
clearAuthCookie
on Logout:
Ensure the auth cookie is cleared on logout:
And that's it! With these steps, the refresh token logic is now integrated into an HTTP-only cookie. You can proceed to Part 2 of the tutorial to test the solution.
If you'd like me to cover more interesting topics about the node.js ecosystem, feel free to leave your suggestions in the comments section. Don't forget to subscribe to my newsletter on rabbitbyte.club for updates!