Building a Secure Authentication Flow Using Refresh Tokens in Node.js and Next.js

Building a Secure Authentication Flow Using Refresh Tokens in Node.js and Next.js

Authentication is a cornerstone of web development. From e-commerce to SaaS platforms, providing secure, seamless, and scalable authentication is critical. In this article, we’ll explore how to implement a secure authentication flow using refresh tokens in Node.js and Next.js, ensuring the safety of user sessions while maintaining an optimal user experience.


Understanding Access and Refresh Tokens

Tokens are widely used in modern authentication systems, especially for Single Page Applications (SPAs) and mobile apps.

Access Tokens

  • Short-lived tokens (e.g., 15 minutes) used to authenticate API requests.
  • Stored in memory or client-side storage for temporary use.
  • Automatically expire, requiring renewal.

Refresh Tokens

  • Long-lived tokens (e.g., 7 days) used to obtain new access tokens without re-authenticating the user.
  • Must be stored securely (e.g., HTTP-only cookies) to prevent exposure to client-side attacks.


Why Token Security Matters

Without proper safeguards, tokens can be exploited. Here’s why and how to secure them:

Risk of Exposure:

  • Storing refresh tokens in client-side storage (e.g., localStorage) makes them vulnerable to Cross-Site Scripting (XSS) attacks.
  • Returning refresh tokens in API responses can expose them to malicious browser extensions or client-side code.

Best Practices:

  • Use HTTP-only cookies to store refresh tokens, ensuring they are inaccessible to JavaScript.
  • Implement token rotation to render compromised tokens unusable.
  • Always use HTTPS to secure token transmission.


Implementation: Secure Authentication in Node.js and Next.js

Here’s a step-by-step guide to implementing a secure token-based authentication system:


Backend: Node.js with Express

The backend is responsible for issuing, refreshing, and validating tokens.

Key Steps:

  1. Generate short-lived access tokens and long-lived refresh tokens.
  2. Store refresh tokens in HTTP-only cookies.
  3. Rotate refresh tokens upon use to enhance security.

Backend Code:

app.post('/login', (req, res) => {
    const { username } = req.body;
    if (!username) return res.status(400).json({ message: 'Username is required' });

    const user = { username };
    const accessToken = generateAccessToken(user);
    const refreshToken = generateRefreshToken(user);

    // Store refresh token in HTTP-only cookie
    res.cookie('refreshToken', refreshToken, {
        httpOnly: true,
        secure: true, // Use true in production
        sameSite: 'strict',
    });

    res.json({ accessToken });
});        

Token Refresh Logic:

app.post('/refresh-token', (req, res) => {
    const refreshToken = req.cookies.refreshToken;
    if (!refreshToken) return res.status(403).json({ message: 'Invalid refresh token' });

    jwt.verify(refreshToken, REFRESH_TOKEN_SECRET, (err, user) => {
        if (err) return res.status(403).json({ message: 'Token expired or invalid' });

        const newAccessToken = generateAccessToken(user);
        const newRefreshToken = generateRefreshToken(user);

        // Replace old refresh token
        res.cookie('refreshToken', newRefreshToken, {
            httpOnly: true,
            secure: true,
            sameSite: 'strict',
        });

        res.json({ accessToken: newAccessToken });
    });
});        

Frontend: Next.js

The frontend handles login, token storage, and token renewal.

Login Component:

const handleLogin = async () => {
    try {
        const response = await axios.post('/login', { username });
        localStorage.setItem('accessToken', response.data.accessToken); // Temporarily store access token
        router.push('/protected');
    } catch (error) {
        console.error('Login failed:', error.response.data.message);
    }
};        

Protected Routes with Token Refresh:

const fetchProtectedData = async () => {
    try {
        const response = await axios.get('/protected', {
            headers: { Authorization: `Bearer ${localStorage.getItem('accessToken')}` },
        });
        setData(response.data.message);
    } catch (error) {
        if (error.response.status === 403) {
            // Attempt to refresh token
            const refreshResponse = await axios.post('/refresh-token');
            localStorage.setItem('accessToken', refreshResponse.data.accessToken);
            fetchProtectedData(); // Retry request
        } else {
            console.error('Access denied:', error.response.data.message);
        }
    }
};        

Key Security Features

HTTP-Only Cookies for Refresh Tokens:

  • Prevents refresh tokens from being accessed by JavaScript, mitigating XSS risks.
  • Automatically sent with requests to the backend.

Token Rotation:

  • Every time a refresh token is used, a new one is issued, and the old one is invalidated.
  • Ensures that stolen refresh tokens cannot be reused.

Short-Lived Access Tokens:

  • Limits the window of opportunity for misuse if an access token is compromised.

Logout and Token Revocation:

  • Tokens are cleared from cookies upon logout.
  • Server-side storage ensures tokens can be invalidated proactively.


Complete Authentication Workflow

Login:

  • User logs in and receives an access token (stored in memory) and a refresh token (stored in a secure cookie).

Access Token Expiry:

  • When the access token expires, the frontend requests a new one using the refresh token.

Token Rotation:

  • The backend issues a new access token and refresh token, invalidating the old refresh token.

Logout:

  • Refresh tokens are deleted from cookies, and access tokens are cleared from memory.


Conclusion

By following these practices, you can implement a secure, scalable authentication system using refresh tokens in Node.js and Next.js. The key takeaway is to minimize the exposure of sensitive tokens, leverage secure cookies, and ensure robust session management through token rotation and short lifespans.

Authentication is never one-size-fits-all, so adapt these principles based on your application’s needs and user base.

要查看或添加评论,请登录

Prashanth S的更多文章

社区洞察

其他会员也浏览了