← All posts
JWTrefresh tokenssecurityvibe codingNext.js

How to Secure JWT Refresh Tokens in Vibe-Coded Apps

O

OverMCP Team

Quick answer

To secure JWT refresh tokens in vibe-coded apps, you must store them in httpOnly cookies, implement rotation and reuse detection, and set short expiration times. If you're building with AI tools, you likely have refresh tokens stored in localStorage or without rotation—both are critical vulnerabilities that can lead to account takeover. This guide walks you through the exact steps to fix these issues, even if you didn't write the code yourself.

What to check first

Before you start fixing, audit your current implementation. Most vibe-coded apps skip security by default, so you'll likely find at least one of these issues:

  • Storage: Are refresh tokens stored in localStorage or sessionStorage? If yes, they're exposed to XSS attacks.
  • Rotation: Is a new refresh token issued on every refresh? If not, a stolen token works forever.
  • Reuse detection: If a stolen token is used, does your app revoke the entire session? If not, attackers can replay tokens.
  • Expiration: Are refresh tokens long-lived (e.g., 30 days)? Shorter is better.
  • Transmission: Are tokens sent via Authorization header? This is fine, but only if you're not using localStorage.
  • Use a free AI app security scanner to quickly identify these issues and others like leaked secrets or misconfigurations.

    Step-by-step fix

    1. Move refresh tokens to httpOnly cookies

    The most secure way to store refresh tokens is in an httpOnly cookie. This prevents JavaScript from accessing them, mitigating XSS attacks. Here's how to set it in a Next.js API route:

    // app/api/auth/refresh/route.ts
    import { serialize } from 'cookie';
    
    export async function POST(request: Request) {
      const body = await request.json();
      // Verify the refresh token (e.g., with jose or jsonwebtoken)
      const user = await verifyRefreshToken(body.refreshToken);
      if (!user) {
        return new Response('Unauthorized', { status: 401 });
      }
    
      // Generate new tokens
      const accessToken = generateAccessToken(user);
      const newRefreshToken = generateRefreshToken(user);
    
      // Set refresh token as httpOnly cookie
      const cookie = serialize('refresh_token', newRefreshToken, {
        httpOnly: true,
        secure: process.env.NODE_ENV === 'production',
        sameSite: 'strict',
        path: '/',
        maxAge: 60 * 60 * 24 * 7 // 7 days
      });
    
      return new Response(JSON.stringify({ accessToken }), {
        headers: { 'Set-Cookie': cookie }
      });
    }

    Note the httpOnly: true and sameSite: 'strict' options. For more on security headers, check our security headers checker.

    2. Implement token rotation and reuse detection

    Token rotation means every time you use a refresh token, you issue a new one. This limits the window of opportunity for a stolen token. Reuse detection means if an old (already rotated) token is presented, you revoke the entire session. Here's a simple implementation using a database:

    -- Track refresh tokens with a version and revoked flag
    CREATE TABLE refresh_tokens (
      id SERIAL PRIMARY KEY,
      user_id INT NOT NULL,
      token_hash TEXT NOT NULL,
      version INT DEFAULT 1,
      revoked BOOLEAN DEFAULT false,
      created_at TIMESTAMP DEFAULT now()
    );

    In your refresh endpoint:

    // Verify token hash from cookie
    const { user, tokenRecord } = await verifyRefreshTokenFromCookie();
    if (!tokenRecord || tokenRecord.revoked) {
      return new Response('Unauthorized', { status: 401 });
    }
    
    // Check if token is already used (reuse detection)
    if (tokenRecord.used_at) {
      // Revoke all tokens for this user
      await revokeAllUserTokens(user.id);
      return new Response('Session revoked', { status: 401 });
    }
    
    // Mark current token as used
    await markTokenUsed(tokenRecord.id);
    
    // Issue new refresh token and update version
    const newToken = generateRefreshToken(user);
    await storeToken(user.id, newToken, tokenRecord.version + 1);

    This ensures that if an attacker and the legitimate user both try to use the same token, only one succeeds and the session is terminated.

    3. Set short expiration times

    Access tokens should expire in 10-15 minutes. Refresh tokens can be longer, but not indefinite. 7 days is a good starting point. If your app needs more, consider implementing absolute timeout or sliding expiration.

    4. Use HTTPS everywhere

    This is non-negotiable. Cookies with secure: true only work over HTTPS. If you're on Vercel, you get HTTPS by default. Run our SSL certificate checker to verify.

    5. Monitor for token leaks

    Use a secret leak scanner to ensure you haven't accidentally committed refresh token secrets to GitHub. Also, check your client-side code for any hardcoded secrets.

    Common mistakes

    Vibe-coded apps often get these wrong:

  • Storing refresh tokens in localStorage: This is the #1 mistake. Any XSS vulnerability can lead to token theft. Even with the best input sanitization, you're one library bug away from disaster.
  • No rotation: Many AI-generated apps issue a refresh token once and never rotate it. This means a stolen token works forever.
  • No reuse detection: Without it, attackers can replay a stolen token even after the user has refreshed.
  • Long-lived tokens: Some apps set refresh tokens to 30 days or more. The longer the lifetime, the bigger the risk.
  • Ignoring `sameSite` attribute: sameSite prevents CSRF attacks on your auth endpoints. Use strict or lax.
  • Using access tokens in cookies too: Access tokens should be in memory (e.g., React state) to avoid CSRF issues. Use cookies only for refresh tokens.
  • How OverMCP can help

    OverMCP is a continuous security monitoring platform designed for vibe-coded apps. It scans your code, dependencies, and configuration to catch vulnerabilities like the ones above before attackers do. Connect your Vercel project and get real-time alerts on security issues. Start with a free AI app security scanner to see where you stand.

    FAQ

    Why is localStorage insecure for refresh tokens?

    LocalStorage is accessible by any JavaScript running on your domain. If an attacker can inject a script (XSS), they can read the token and send it to their server. httpOnly cookies prevent JavaScript access, so the token is safe even if XSS exists.

    How often should I rotate refresh tokens?

    Rotate on every refresh—each time you issue a new access token, also issue a new refresh token. This ensures that a stolen token becomes useless after the next use.

    What should I do if I suspect a refresh token leak?

    Immediately revoke all refresh tokens for the affected user. Implement reuse detection to automatically revoke sessions when a token is used twice. Also, rotate any secrets that might be compromised. Run a secret leak scanner to check your codebase.

    ---

    Secure your vibe-coded app today. OverMCP provides automated security scanning for AI-built applications. Start free at [overmcp.com](https://www.overmcp.com/).

    Is your app secure?

    Free scan in 30 seconds. No signup needed.

    Scan My App Free