I ran into this issue today while implementing user authentication.
This was my set up:
I had a login form, a Server Action and a Route Handler.
Here is the code:
"use client"
import { useActionState } from "react"
import { Icons } from "@/components/icons"
import { signInWithGoogle } from "@/app/actions"
export function SignInGoogleForm() {
const [formState, formAction, isPending] = useActionState(
signInWithGoogle,
undefined
)
return (
<form action={formAction} className="mx-auto grid max-w-[320px] gap-2">
{formState !== undefined && formState.error && (
<div className="text-pretty py-4 text-sm text-red-600">
{formState.error}
</div>
)}
<button
type="submit"
disabled={isPending}
className="border-input text-secondary-foreground hover:bg-accent w-full rounded-md border py-2 text-sm font-medium shadow-sm transition-all active:scale-[0.98]"
>
<Icons.google className="mr-2 inline-block size-5" />
Sign in with Google
</button>
</form>
)
}
import { redirect } from "next/navigation"
export async function signInWithGoogle() {
try {
// Making request to the Route Handler
const response = await fetch("/api/auth/signin/google")
if (response.status === 307) {
const redirectUrl = response.headers.get("location")
if (redirectUrl) {
redirect(redirectUrl)
}
}
return {
error: true,
message: "Something went wrong. Please try again.",
}
} catch {
return {
error: true,
message: "Something went wrong. Please try again.",
}
}
}
import { cookies } from "next/headers"
import { NextResponse } from "next/server"
import { google } from "@/lib/oauth2/auth"
import { generateCodeVerifier } from "@/lib/oauth2/pkce"
import { generateState } from "@/lib/oauth2/utils"
export async function GET() {
try {
const state = generateState()
const codeVerifier = generateCodeVerifier()
const url = await google.createAuthorizationURLWithPKCE(
state,
codeVerifier,
["openid", "profile"]
)
// Setting cookies
const cookieStore = await cookies()
cookieStore.set("google_oauth_state", state, {
path: "/",
httpOnly: true,
secure: process.env.NODE_ENV === "production",
maxAge: 60 * 10, // 10 minutes
sameSite: "lax",
})
cookieStore.set("google_code_verifier", codeVerifier, {
path: "/",
httpOnly: true,
secure: process.env.NODE_ENV === "production",
maxAge: 60 * 10, // 10 minutes
sameSite: "lax",
})
return NextResponse.redirect(url.toString())
} catch (error) {
return new NextResponse(null, { status: 500 })
}
}
The flow was simple:
- I submit the form.
- Form submission triggers the Server Action.
- The Server Action makes a request to the Route Handler.
- Inside the Route Handler, I set cookies.
However, when I tested the flow, the cookie was not being set in the browser.
After doing some research, I realized what was going on — the key was understanding where the code was actually executing.
Remember that Route Handlers and Server Actions are executed on the server.
When I submitted the form, it made a POST
request to the Server Action. The Server Action then made a GET
request to the Route Handler. The communication between the Server Action and the Route Handler was server-to-server.
The Route Handler then set the cookies and sent its response (which included the Set-Cookie
header) back to the Server Action (the server). The cookie never reached the browser.
Note that browsers only set cookies when they directly receive an HTTP response containing the Set-Cookie
header. In this case, since the communication happened internally (server-to-server), the browser was completely unaware of that response, and therefore, no cookie was stored.
To solve this issue, I simply moved my cookie-setting logic from the Route Handler to the Server Action:
"use server"
import { cookies } from "next/headers"
import { redirect } from "next/navigation"
import { google } from "@/lib/oauth2/auth"
import { generateCodeVerifier } from "@/lib/oauth2/pkce"
import { generateState } from "@/lib/oauth2/utils"
export async function signInWithGoogle() {
const state = generateState()
const codeVerifier = generateCodeVerifier()
const url = await google.createAuthorizationURLWithPKCE(state, codeVerifier, [
"openid",
"email",
"profile",
])
// Setting cookies
const cookieStore = await cookies()
cookieStore.set("google_oauth_state", state, {
path: "/",
httpOnly: true,
secure: process.env.NODE_ENV === "production",
maxAge: 60 * 10, // 10 minutes
sameSite: "lax",
})
cookieStore.set("google_code_verifier", codeVerifier, {
path: "/",
httpOnly: true,
secure: process.env.NODE_ENV === "production",
maxAge: 60 * 10, // 10 minutes
sameSite: "lax",
})
redirect(url.toString())
}
Now, when I submit the form, the browser sends the request directly to the Server Action. The Server Action then sets the cookies and sends the response (which includes the Set-Cookie
header) straight back to the browser. The communication is client-to-server:
As a result, the cookie is successfully stored on the client.