Stop the Supabase getSession() Security Warning
The Supabase "using getSession() is insecure" warning floods your logs. Here is why getUser() fixes it and where getSession() stays safe.
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.
The other half of the warning: onAuthStateChange#
The warning text quoted at the top of this fix actually names two sources, not one:
"the user object as returned from supabase.auth.getSession() or from some
supabase.auth.onAuthStateChange() events". Swapping getSession() for getUser() in
middleware and Server Components fixes the first source. It does nothing for the second,
because onAuthStateChange is a client-side listener, not a call you replace — it fires
automatically whenever the auth state changes (sign-in, sign-out, token refresh), and
the session object it hands your callback comes from the same unvalidated cookie read
as getSession().
'use client'
useEffect(() => {
const { data: { subscription } } = supabase.auth.onAuthStateChange(
(event, session) => {
// ❌ session.user here is NOT validated against the Auth server
setCurrentUser(session?.user ?? null)
},
)
return () => subscription.unsubscribe()
}, [])If that callback only updates UI state — a header avatar, a "signed in as" label — this
is the same safe optimistic-UI case as getSession() above, and no change is needed.
The warning starts to matter the moment the callback does anything that gates access:
redirecting the user away on a sign-out event, revealing a client-side route, or passing session.user.id
into a call that trusts it for authorization. In that case, call getUser() inside the
handler before acting on the event, rather than trusting the session argument directly:
const { data: { subscription } } = supabase.auth.onAuthStateChange(
async (event) => {
if (event === 'SIGNED_OUT') {
router.push('/login')
return
}
// ✅ validate before trusting the identity for anything sensitive
const { data: { user } } = await supabase.auth.getUser()
setCurrentUser(user)
},
)This distinction is easy to miss because the fix for getSession() (swap the call) does
not transfer directly to onAuthStateChange (the listener itself stays, only what you do
inside it changes) — a mechanical find-and-replace across the codebase won't catch it,
which is why grep -rn "auth.getSession" from the verification steps above won't surface
this half of the warning at all; you need a second pass specifically for
onAuthStateChange call sites. The same event-driven listener is also the source of a
separate, unrelated failure mode — a callback that never resolves because it awaits
another Supabase call from inside itself — covered in
Supabase Hangs After onAuthStateChange: The Deadlock,
worth checking if your listener callback is doing both jobs (validating the user and
making other Supabase calls) and appears to freeze rather than just log the warning.
Further reading: Supabase Auth + Middleware: Session Guide for Next.js 15.
Still stuck? Paste your error.
Paste the error message or stack trace — we check it against every fix and tool on the site.
Related fixes & guides
- Fix: Multiple GoTrueClient instances detected (Supabase)
- Supabase Edge Function BOOT_ERROR: Fix "Failed to Bootstrap"
- Supabase OAuth access_token hash Stuck in URL
- Complete Guide to Building SaaS with Next.js and Supabase
- Supabase Auth vs Clerk in 2026: The Production Verdict
- Supabase + Google OAuth on Next.js 15: Working Guide (2026)
- Supabase getClaims() vs getSession(): The Silent Auth Bug
- Supabase bucket RLS policy for table objects fix
- Supabase vs Firebase in 2026: The Honest Comparison
- Supabase AuthSessionMissingError in Middleware: 5 Fixes

