Cookie Handling in My App
I’m running into a problem with cookies on Vercel.
- On localhost: everything works.
- On Vercel:
- Cookies set in signin and callback work fine.
- Cookies set in withIasAuth (refreshTokens) are not applied.
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;✅ 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;✅ 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;}❌ Fails on Vercel — cookies are not updated after refresh. ✅ 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.