Cookies not being changed on vercel for development and production

Cookie Handling in My App

I’m running into a problem with cookies on Vercel.


1. Signin Flow (/pages/api/auth/signin.ts)

When the user clicks Login, PKCE values are generated and stored as cookies.

import type { NextApiRequest, NextApiResponse } from 'next';
import { createPkceChallengePair, generateStateAndNoonce } from '@/lib/pkce';

const handler = async (req: NextApiRequest, res: NextApiResponse) => {
  const { codeVerifier, codeChallenge } = await createPkceChallengePair();
  const { state, noonce } = generateStateAndNoonce();

  const issuer = process.env.IAS_ISSUER_URL;  
  const clientId = process.env.IAS_CLIENT_ID;
  const redirectUri = process.env.IAS_CALLBACK_URL;
  const scope = process.env.IAS_SCOPE;

  res.setHeader('Set-Cookie', [
    `pkce_verifier=${codeVerifier}; Path=/; HttpOnly; Secure; SameSite=Lax`,
    `pkce_state=${state}; Path=/; HttpOnly; Secure; SameSite=Lax`,
    `pkce_nonce=${noonce}; Path=/; HttpOnly; Secure; SameSite=Lax`,
  ]);

  const loginUrl =
    `${issuer}/connect/authorize?` +
    new URLSearchParams({
      client_id: clientId,
      redirect_uri: redirectUri,
      response_type: 'code',
      scope,
      state,
      nonce: noonce,
      code_challenge: codeChallenge,
      code_challenge_method: 'S256',
      response_mode: 'query',
      prompt: 'login',
    }).toString();

  res.redirect(loginUrl);
};

export default handler;

:white_check_mark: Works (cookies set correctly).


2. Callback Flow (/pages/api/auth/callback.ts)

After login, the identity provider redirects here. Tokens are exchanged and stored in cookies.

import type { NextApiRequest, NextApiResponse } from 'next';
import { parseCookies } from "@/utils/parseCookies";

const handler = async (req: NextApiRequest, res: NextApiResponse) => {
  const { code, state } = req.query;

  const cookies = parseCookies(req.headers.cookie);
  const codeVerifier = cookies['pkce_verifier'];

  const issuer = process.env.IAS_ISSUER_URL;  
  const clientSecret = process.env.CLIENT_SECRET; 
  const redirectUri = process.env.IAS_CALLBACK_URL;
  const clientId = process.env.IAS_CLIENT_ID;

  const tokenRes = await fetch(`${issuer}/connect/token`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      client_id: clientId,
      client_secret: clientSecret,
      grant_type: 'authorization_code',
      code: code.toString(),
      redirect_uri: redirectUri,
      code_verifier: codeVerifier,
    }),
  });

  const tokens = await tokenRes.json();

  res.setHeader('Set-Cookie', [
    `id_token=${tokens.id_token}; Path=/; HttpOnly; Secure; SameSite=Lax`,
    `access_token=${tokens.access_token}; Path=/; HttpOnly; Secure; SameSite=Lax`,
    `refresh_token=${tokens.refresh_token}; Path=/; HttpOnly; Secure; SameSite=Lax`,
  ]);

  res.redirect('/?login=success');
};

export default handler;

:white_check_mark: Works (cookies set correctly).


3. Refresh Flow (withIasAuth.ts → refreshTokens())

When the access_token expires, this function tries to refresh tokens and replace the cookies.

import type { NextApiRequest, NextApiResponse } from "next";
import { parseCookies } from "@/utils/parseCookies";

type Tokens = {
  access_token: string;
  id_token?: string;
  refresh_token?: string;
};

function mask(token?: string) {
  if (!token) return "none";
  return `${token.slice(0, 8)}...${token.slice(-8)}`;
}

function getEnv() {
  const issuer = process.env.IAS_ISSUER_URL;
  const clientId = process.env.IAS_CLIENT_ID;
  const clientSecret = process.env.CLIENT_SECRET;
  return { issuer, clientId, clientSecret };
}

export async function refreshTokens(
  req: NextApiRequest,
  res: NextApiResponse
): Promise<Tokens | null> {
  const cookies = parseCookies(req.headers.cookie);
  const refresh_token = cookies["refresh_token"];

  if (!refresh_token) return null;

  const { issuer, clientId, clientSecret } = getEnv();

  const r = await fetch(`${issuer}/connect/token`, {
    method: "POST",
    headers: { "content-type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      grant_type: "refresh_token",
      refresh_token,
      client_id: clientId,
      client_secret: clientSecret,
    }),
  });

  if (!r.ok) return null;

  const tokens: Tokens = await r.json();

  res.setHeader("Set-Cookie", [
    `id_token=${tokens.id_token}; Path=/; HttpOnly; Secure; SameSite=Lax`,
    `access_token=${tokens.access_token}; Path=/; HttpOnly; Secure; SameSite=Lax`,
    `refresh_token=${tokens.refresh_token}; Path=/; HttpOnly; Secure; SameSite=Lax`,
  ]);

  console.log("[IAS AUTH] Cookies replaced ✅");

  return tokens;
}

:cross_mark: Fails on Vercel — cookies are not updated after refresh.
:white_check_mark: Works locally.


Key Observation

  • Cookies set in signin and callback → work fine (redirect flow).
  • Cookies set in refreshTokens → fail only on Vercel (XHR/fetch response).

This shows the issue is not in the code itself (since localhost works), but in how Vercel (and possibly Chrome) handle Set-Cookie headers during API responses.

I’ve seen similar issues online where cookies aren’t stored on Vercel but work fine on localhost. Is there something missing here? I haven’t seen anyone provide a clear resolution to this problem.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.