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.