What actually changed
You upgraded @supabase/auth-js to v2 (or pulled @supabase/ssr into your Next.js App Router project), and now your terminal fills up with this on every protected route:
Using the user object as returned from supabase.auth.getSession() or
from some supabase.auth.onAuthStateChange() events could be insecure if the
user object is used for authorization purposes. This is because the user
object is not validated on the server and can be tampered with.
Use supabase.auth.getUser() instead.
It is not a thrown error — your app keeps working — so most teams ignore it for weeks. Then a security review flags it, or a forged-cookie attack vector shows up in a pentest, and the warning becomes a P0. The issue has been the most-commented auth-js issue for over a year (supabase/auth-js#873).
The fix
The warning is telling you exactly what to do, but not where. The rule:
-
Use
getUser()for any decision that grants or denies access. Middleware, Server Component gating, Server Actions, Route Handlers. -
Use
getSession()only for optimistic client UI — rendering a username before the server has confirmed the session.
Before — the code that triggers the warning
// src/middleware.ts (or any Server Component)
import { createServerClient } from '@supabase/ssr'
const supabase = createServerClient(url, anonKey, { cookies })
// ❌ Reads the cookie, does NOT validate the JWT with the Auth server.
const { data: { session } } = await supabase.auth.getSession()
if (!session) {
return NextResponse.redirect(loginUrl) // gated on unvalidated data
}
getSession() returns whatever is in the cookie. If an attacker crafts a cookie with a fake JWT, getSession() happily hands back a session object and your redirect never fires — or worse, your protected data leaks.
After — validate server-side
// src/middleware.ts
import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'
export async function middleware(request: NextRequest) {
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll: () => request.cookies.getAll(),
setAll: (toSet) =>
toSet.forEach(({ name, value, options }) =>
request.cookies.set(name, value),
),
},
},
)
// ✅ Validates the JWT against the Supabase Auth server.
const { data: { user } } = await supabase.auth.getUser()
if (!user && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url))
}
return NextResponse.next()
}
The same swap applies to Server Components and Server Actions — replace getSession() with getUser() and destructure user, not session. The user object is what you gate on; the session is for the client.
Where getSession() is still correct
'use client'
import { createClient } from '@/lib/supabase/client'
export function HeaderAvatar() {
const supabase = createClient()
const [email, setEmail] = useState<string | null>(null)
useEffect(() => {
// ✅ Optimistic UI — fine. Not used for authorization.
supabase.auth.getSession().then(({ data: { session } }) => {
setEmail(session?.user?.email ?? null)
})
}, [])
return <span>{email ?? 'Not signed in'}</span>
}
This is the only place getSession() belongs. If a fake cookie fools it, the worst outcome is a wrong email shown for a second until the real session loads. No data is exposed.
Verifying the fix
-
grep -rn "auth.getSession" src/— every hit outside a'use client'file must change togetUser(). - Run your dev server and load a protected route. The warning should stop printing.
- Forge a cookie manually (set
sb-access-tokento a random string in DevTools → Application → Cookies) and reload. WithgetUser(), the route redirects to/login. With the oldgetSession()code, it would have let you through.
The deeper background on why this distinction matters for the entire request lifecycle — middleware refresh, Server Component reads, Client Component sync — is in the Next.js + Supabase SSR session management deep dive, which walks through the cookie refresh chain that makes getUser() safe without doubling your latency.
Related Incidents
-
Supabase getUser vs getSession vs getClaims: which to call server-side — the same trap, framed around the newer
getClaims()helper and when it beatsgetUser(). -
Handle Supabase auth errors in Next.js middleware — what to do when
getUser()itself throwsAuthSessionMissingErrororAuthInvalidTokenError. - Supabase auth redirect not working on Vercel preview deployments — a sibling symptom: the session looks fine locally but vanishes in production because the redirect URL is wrong, not the session check.
- Supabase OAuth access_token hash Stuck in URL
-
Next.js
cookies()must be awaited in 15 and 16 — if yourgetUser()call silently returnsnullafter a Next.js upgrade, this is why. Thecookies()helper now returns a Promise, and an un-awaited call yields an empty cookie store, which looks identical to an auth bug.
Originally published at https://www.iloveblogs.blog
Top comments (0)