Middleware Route Protection
Check auth once, protect routes declaratively.
When to Use This Skill
-
Need to protect multiple routes
-
Want centralized auth checking
-
Tired of repeating auth logic in every route
-
Need role-based access control
Core Concepts
-
Middleware intercepts - All requests pass through middleware
-
Declarative routes - Define protected/public routes in config
-
Session refresh - Keep sessions alive automatically
-
Consistent errors - API routes get JSON, pages get redirects
TypeScript Implementation
middleware.ts
// middleware.ts import { createServerClient } from '@supabase/ssr'; import { NextResponse, type NextRequest } from 'next/server';
// Routes that require authentication const PROTECTED_ROUTES = [ '/dashboard', '/settings', '/api/user', '/api/predictions', ];
// Routes that are always public const PUBLIC_ROUTES = [ '/', '/login', '/signup', '/api/health', '/api/public', ];
export async function middleware(request: NextRequest) { const pathname = request.nextUrl.pathname;
// Skip static files and Next.js internals if ( pathname.startsWith('/_next') || pathname.startsWith('/favicon') || pathname.match(/.(svg|png|jpg|jpeg|gif|webp|ico)$/) ) { return NextResponse.next(); }
// Skip explicitly public routes if (PUBLIC_ROUTES.some(route => pathname === route || pathname.startsWith(route + '/'))) { return NextResponse.next(); }
// Create response that we'll modify let response = NextResponse.next({ request });
// Create Supabase client with cookie handling const supabase = createServerClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, { cookies: { getAll() { return request.cookies.getAll(); }, setAll(cookiesToSet) { cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value) ); response = NextResponse.next({ request }); cookiesToSet.forEach(({ name, value, options }) => response.cookies.set(name, value, options) ); }, }, } );
// Refresh session (important for SSR) const { data: { user } } = await supabase.auth.getUser();
// Check if route requires auth const requiresAuth = PROTECTED_ROUTES.some(route => pathname === route || pathname.startsWith(route + '/') );
if (requiresAuth && !user) { // API routes: return 401 JSON if (pathname.startsWith('/api/')) { return NextResponse.json( { error: 'Authentication required', code: 'AUTH_REQUIRED', loginUrl: '/login', }, { status: 401 } ); }
// Pages: redirect to login with return URL
const url = request.nextUrl.clone();
url.pathname = '/login';
url.searchParams.set('redirectTo', pathname);
return NextResponse.redirect(url);
}
// Add user ID to headers for downstream use if (user) { response.headers.set('x-user-id', user.id); }
return response; }
export const config = { matcher: [ '/((?!_next/static|_next/image|favicon.ico|.\.(?:svg|png|jpg|jpeg|gif|webp)$).)', ], };
Using User ID in API Routes
// app/api/user/route.ts import { NextRequest, NextResponse } from 'next/server'; import { createServerSupabaseClient } from '@/lib/supabase-server';
export async function GET(request: NextRequest) { // User ID was added by middleware const userId = request.headers.get('x-user-id');
if (!userId) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); }
const supabase = await createServerSupabaseClient();
const { data: profile } = await supabase .from('user_profiles') .select('*') .eq('id', userId) .single();
return NextResponse.json({ profile }); }
Role-Based Protection
// middleware.ts (extended)
const ROUTE_ROLES: Record<string, string[]> = { '/admin': ['admin'], '/dashboard': ['user', 'admin'], '/api/admin': ['admin'], };
// After getting user, check role if (user) { const userRole = user.user_metadata?.role || 'user';
const requiredRoles = Object.entries(ROUTE_ROLES) .find(([route]) => pathname.startsWith(route))?.[1];
if (requiredRoles && !requiredRoles.includes(userRole)) { if (pathname.startsWith('/api/')) { return NextResponse.json( { error: 'Forbidden', code: 'FORBIDDEN' }, { status: 403 } ); } return NextResponse.redirect(new URL('/unauthorized', request.url)); } }
Pattern-Based Routes
// For more complex route matching const PROTECTED_PATTERNS = [ /^/dashboard(/.)?$/, /^/api/user/.$/, /^/settings$/, /^/api/v\d+/private/./, // /api/v1/private/, /api/v2/private/* ];
const requiresAuth = PROTECTED_PATTERNS.some(pattern => pattern.test(pathname) );
Python Implementation (FastAPI)
middleware/auth.py
from fastapi import Request, HTTPException from fastapi.responses import RedirectResponse from starlette.middleware.base import BaseHTTPMiddleware from typing import Set
PROTECTED_ROUTES: Set[str] = { "/dashboard", "/settings", "/api/user", }
PUBLIC_ROUTES: Set[str] = { "/", "/login", "/signup", "/api/health", }
class AuthMiddleware(BaseHTTPMiddleware): async def dispatch(self, request: Request, call_next): path = request.url.path
# Skip public routes
if path in PUBLIC_ROUTES or any(path.startswith(r + "/") for r in PUBLIC_ROUTES):
return await call_next(request)
# Check if route requires auth
requires_auth = path in PROTECTED_ROUTES or any(
path.startswith(r + "/") for r in PROTECTED_ROUTES
)
if not requires_auth:
return await call_next(request)
# Get user from session/token
user = await get_user_from_request(request)
if not user:
if path.startswith("/api/"):
raise HTTPException(
status_code=401,
detail={
"error": "Authentication required",
"code": "AUTH_REQUIRED",
"login_url": "/login",
}
)
return RedirectResponse(f"/login?redirectTo={path}")
# Add user to request state
request.state.user = user
request.state.user_id = user.id
return await call_next(request)
async def get_user_from_request(request: Request): """Extract and validate user from request.""" token = request.cookies.get("session") or request.headers.get("Authorization") if not token: return None # Validate token and return user return await validate_session(token)
Using in routes
from fastapi import Request, Depends
@app.get("/api/user/profile") async def get_profile(request: Request): user_id = request.state.user_id profile = await db.get_profile(user_id) return {"profile": profile}
Error Response Format
// Consistent error format for API routes interface AuthError { error: string; code: 'AUTH_REQUIRED' | 'SESSION_EXPIRED' | 'FORBIDDEN'; message?: string; loginUrl: string; }
// 401 - Not authenticated { "error": "Authentication required", "code": "AUTH_REQUIRED", "loginUrl": "/login" }
// 403 - Authenticated but not authorized { "error": "Forbidden", "code": "FORBIDDEN", "message": "Admin access required" }
Best Practices
-
Refresh sessions - Call getUser() in middleware to refresh
-
Pass user ID - Add to headers for downstream routes
-
JSON for APIs - Never redirect API routes
-
Return URL - Include redirectTo param for login redirects
-
Skip static - Don't process static files
Common Mistakes
-
Redirecting API routes (should return 401 JSON)
-
Not refreshing session in middleware
-
Processing static files through auth
-
Missing return URL on login redirect
-
Not handling role-based access
Related Skills
-
Supabase Auth
-
JWT Auth
-
Row Level Security