Server-Side Auth for Protected Routes
Cross-Module · AUTH-06, AUTH-11, ADM-01, ADM-02 · AUTH-04 · Priority: P0
Why It Matters
Client-side authentication is not authentication. It's a UI hint. Any user with browser DevTools, a REST client, or a simple curl command can bypass client-side route guards and access protected pages, API endpoints, and admin functions directly.
In independent security testing, authorization logic was the most common critical failure across all 5 AI coding tools tested — with 69 vulnerabilities found across just 15 apps (Tenzai, December 2025). A security researcher found inverted authentication logic in Lovable apps — the app blocked legitimate users while allowing unauthenticated access.
OWASP ranks Broken Access Control as the #1 web application risk, found in 100% of tested applications (OWASP Top 10:2025).
Priority: P0 — Any route that handles user data, payments, or admin functions must verify auth on the server.
Affected Stack: Next.js, Supabase, any framework with client/server separation
AUTH-04: Server-Side Auth on Protected Routes
The Problem
AI code generators create a client-side auth context (e.g., useAuth() hook) and use it to conditionally render pages. This gives the appearance of protection — but the underlying API routes and server-side data fetching have no auth checks.
// ❌ What AI tools typically generate — client-side only
export default function DashboardPage() {
const { user } = useAuth();
if (!user) return <Redirect to="/login" />;
// Page renders... but the API call has no server-side check
const data = await fetch('/api/user-data');
return <Dashboard data={data} />;
}
The API route /api/user-data returns data for anyone who calls it — no auth verification.
The Fix
Every server-side handler (API route, Server Action, getServerSideProps) must independently verify the user's session using getUser().
// ✅ Production-safe pattern
import { createServerClient } from '@supabase/ssr';
import { cookies } from 'next/headers';
export async function GET(req: Request) {
const supabase = createServerClient(/* cookie config */);
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
return new Response('Unauthorized', { status: 401 });
}
const data = await supabase
.from('user_data')
.select('*')
.eq('user_id', user.id);
return Response.json(data);
}
Key principle: The client-side check is a UX convenience. The server-side check is the actual security boundary.
AUTH-06: Protected Routes Redirect Unauthenticated Users
Middleware-level redirects ensure that unauthenticated users hitting protected URLs are sent to the login page before any server-side rendering or data fetching occurs.
// ✅ Next.js middleware for protected routes
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const session = request.cookies.get('sb-access-token');
if (!session && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}
}
AUTH-11: Client/Server Auth Separation
AI tools frequently create a single Supabase client instance shared between client and server code. This blurs the security boundary — server-side operations may accidentally use client-side credentials, and client-side code may import server-only utilities.
The fix: Maintain separate client instances. Use createBrowserClient() for client components and createServerClient() for server components, API routes, and middleware.
ADM-01 & ADM-02: Admin Endpoints and Routes Require Auth
Admin routes need the same server-side verification — plus role checks. In a security test of an AI-built app, researchers at Infinum found that changing a single field from "user" to "admin" granted full administrative access. No server-side validation existed.
// ✅ Admin API route with role check
export async function GET(req: Request) {
const supabase = createServerClient(/* ... */);
const { data: { user } } = await supabase.auth.getUser();
if (!user) return new Response('Unauthorized', { status: 401 });
const { data: profile } = await supabase
.from('profiles')
.select('role')
.eq('id', user.id)
.single();
if (profile?.role !== 'admin') {
return new Response('Forbidden', { status: 403 });
}
// Admin-only logic...
}
References
- OWASP: Broken Access Control (A01:2021)
- Supabase: Server-Side Auth with Next.js
- CWE-863: Incorrect Authorization
Related Checks
- Supabase RLS Safety — AUTH-02, AUTH-03, AUTH-17
- getUser() vs getSession() — AUTH-13
- No Client-Side-Only Role Checks — ADM-03
- MFA for Admin Roles — ADM-13
Is your app safe? Run Free Scan →