ASAASA Standard
Active Phase 1Production Foundation

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


Related Checks


Is your app safe? Run Free Scan →