Using Cookies to Manage Authentication Tokens in Next.js

In web development, managing user sessions efficiently and securely is crucial. One of the reliable ways to handle session data is by using cookies. Cookies are small pieces of data sent from a server and stored on the user's browser while the user is browsing a website. This blog post explores how cookies can be used to store authentication tokens in a Next.js application using TypeScript, ensuring a secure and efficient user authentication experience.

What Are Cookies?

Cookies serve as a method for servers to remember information about users or to record their browsing activity. They are primarily used to manage user sessions, store user preferences, and track user behaviour for targeted advertising.

Why Use Cookies for Tokens?

Cookies are particularly useful for storing authentication tokens because:

  • Persistence: Unlike local storage which is accessible via client-side scripts (potentially exposing it to XSS attacks), cookies persist across sessions and can be made secure against script access when properly configured.

  • Security Features: Cookies come with attributes like HttpOnly, Secure, and SameSite that help enhance security by restricting access from client scripts and ensuring cookies are sent only in secure contexts and in strict cross-site scenarios.

Security Best Practices for Cookies

When using cookies to store sensitive data such as tokens, setting the following attributes is essential:

  • HttpOnly: This attribute prevents access to the cookie via client-side JavaScript, mitigating the risk of XSS attacks.

  • Secure: This attribute ensures that cookies are sent over secure protocols like HTTPS, which encrypts the data during transmission.

  • SameSite: This attribute prevents the browser from sending this cookie along with cross-site requests, which helps protect against CSRF attacks. It can be set to Strict, Lax, or None.

Let's walk through an example of setting and retrieving JWTs (JSON Web Tokens) in a Next.js application using TypeScript. We will set a cookie after a user logs in and read it in subsequent requests to verify the user's session.

First, here's how to set a cookie containing a JWT when a user logs in:

import { NextApiRequest, NextApiResponse } from 'next';
import cookie from 'cookie';

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  // Assume user authentication is successful and a JWT is generated
  const token = 'your_jwt_token_here';

  // Set the cookie with the JWT
  res.setHeader('Set-Cookie', cookie.serialize('auth_token', token, {
    httpOnly: true,
    secure: process.env.NODE_ENV !== 'development',
    sameSite: 'strict',
    path: '/',
    maxAge: 3600 // 1 hour expiration
  }));

  res.status(200).json({ message: 'Logged in successfully!' });
}

Next, how to read and verify the token from the cookie in an API route:

import { NextApiRequest, NextApiResponse } from 'next';
import cookie from 'cookie';

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  // Extract the cookie from the request
  const cookies = cookie.parse(req.headers.cookie || '');
  const token = cookies.auth_token;

  // Check for the token's existence
  if (!token) {
    return res.status(401).json({ message: 'Authentication failed! No token provided.' });
  }

  // Here, add logic to verify the token's validity
  res.status(200).json({ message: 'Token verified successfully!', token });
}

Other Information about Cookies.

Aside from the HttpOnly, Secure, and SameSite attributes mentioned previously, there are other attributes that you might find useful:

  • Expires: This attribute sets a specific date and time when the cookie will expire and be deleted. Unlike Max-Age, which is relative (in seconds), Expires is an absolute timestamp.

  • Domain: Specifies the domain for which the cookie is valid. Using this attribute, a cookie can be made accessible across subdomains.

  • Path: Defines the path within the domain for which the cookie is valid. This can be used to restrict the cookie to a specific part of the site.

Size Limits and Performance Considerations

Cookies are limited in size (typically around 4KB per cookie), and each domain typically cannot have more than 20-50 cookies (the exact number depends on the browser). Because cookies are sent with every HTTP request to the domain they belong to, having too many or too large cookies can negatively impact performance due to increased load times.

Alternatives to Cookies

In situations where the limitations of cookies (size, quantity, or security concerns) pose a problem, alternative storage solutions like localStorage, sessionStorage, or server-side storage might be more appropriate:

  • localStorage: Provides a way to store data persistently on the client's browser, accessible via JavaScript but limited to the same origin.

  • sessionStorage: Similar to localStorage but is cleared when the page session ends (when the tab is closed).

In light of privacy regulations such as GDPR in Europe or CCPA in California, handling cookies often requires user consent, especially for tracking cookies used for marketing purposes. Implementing a consent management platform (CMP) can help comply with these regulations.

Securely Handling Sensitive Information

While HttpOnly and Secure attributes make cookies safer, sensitive information (like personal identifiers or financial data) should ideally not be stored directly in cookies. Encrypting cookie values can add an extra layer of security.

Conclusion

Using cookies to manage authentication tokens in a Next.js application enhances security and user experience. By correctly configuring cookie attributes such as HttpOnly, Secure, and SameSite, developers can safeguard user data against common web vulnerabilities like XSS and CSRF. This method not only ensures secure data transmission but also aligns with best practices for modern web application development.