Data Access Layer Pattern for Next.js 16 Authentication Security
Critical Security Issue: CVE-2025-29927
Next.js 16 has a critical authentication bypass vulnerability in middleware. Middleware NextResponse.redirect() and NextResponse.rewrite() DO NOT terminate execution, allowing unauthorized access to protected resources.
The Problem
export function middleware(request: NextRequest) {
const session = request.cookies.get('session');
if (!session) {
return NextResponse.redirect(new URL('/login', request.url));
}
}
This middleware appears to protect routes, but code after the return statement still executes. Attackers can bypass authentication by manipulating requests.
Why This Matters
- •Middleware-only authentication is completely broken in Next.js 16
- •Protected routes, server actions, and API routes are all vulnerable
- •Data breaches, unauthorized access, and privilege escalation are possible
The Solution: Data Access Layer (DAL)
Implement a multi-layer security strategy with authentication verification at every access point.
Core Pattern: verifySession()
For comprehensive input validation patterns to use alongside authentication, use the sanitizing-user-inputs skill from the typescript plugin.
Create a centralized Data Access Layer that verifies authentication before ANY data access:
import 'server-only';
import { cookies } from 'next/headers';
import { decrypt } from '@/lib/session';
import { cache } from 'react';
export const verifySession = cache(async () => {
const cookie = (await cookies()).get('session')?.value;
const session = await decrypt(cookie);
if (!session?.userId) {
throw new Error('Unauthorized');
}
return { isAuth: true, userId: session.userId };
});
Key features:
- •Uses
cache()for request-level memoization (single verification per request) - •Throws error if unauthorized (fails fast)
- •Returns typed session data for use in application logic
- •Server-only code that cannot leak to client
Three-Layer Security Architecture
- •Route Protection - Basic UX (redirect unauthorized users)
- •Data Access Layer - Core security (verify before data access)
- •Server Actions - Action-level verification (verify before mutations)
Layer 1: Route Protection (UX Only)
export default async function DashboardLayout({ children }) {
const session = await verifySession();
if (!session.isAuth) {
redirect('/login');
}
return <>{children}</>;
}
Verify session in layouts and pages to redirect unauthorized users. This is UX only, not security.
Layer 2: Data Access Layer (Security)
export async function getUser() {
const session = await verifySession();
const data = await db.query.users.findMany({
where: eq(users.id, session.userId),
});
return data;
}
Always verify session before database queries. This is your actual security boundary.
For type-safe database access patterns, use the ensuring-query-type-safety skill from prisma-6 to prevent type errors and runtime failures in your data access functions.
Layer 3: Server Actions (Mutation Security)
'use server';
export async function updateProfile(formData: FormData) {
const session = await verifySession();
const name = formData.get('name');
await db.update(users).set({ name }).where(eq(users.id, session.userId));
revalidatePath('/profile');
}
Verify session at the start of every server action that modifies data.
Implementation Checklist
When working with authenticated features:
- • Create
lib/dal.tswithverifySession()function - • Use
verifySession()in ALL data fetching functions - • Use
verifySession()in ALL server actions - • Add route protection to layouts/pages for UX (optional but recommended)
- • Never rely on middleware alone for authentication
- • Import 'server-only' in DAL to prevent client leaks
- • Use React
cache()for request-level memoization
Common Patterns
Authorization (Role-Based Access)
export async function verifyAdmin() {
const session = await verifySession();
const user = await db.query.users.findFirst({
where: eq(users.id, session.userId),
});
if (user?.role !== 'admin') {
throw new Error('Forbidden');
}
return { userId: session.userId, role: user.role };
}
Resource Ownership
export async function getPost(postId: string) {
const session = await verifySession();
const post = await db.query.posts.findFirst({
where: eq(posts.id, postId),
});
if (post.authorId !== session.userId) {
throw new Error('Forbidden');
}
return post;
}
Multi-Step Atomic Operations
If setting up PrismaClient with singleton pattern in Next.js, use the creating-client-singletons skill from prisma-6 for proper instantiation preventing connection pool exhaustion.
If implementing authenticated operations requiring atomicity (e.g., creating a post with tags, transferring ownership), use the using-interactive-transactions skill from prisma-6 for database-specific transaction patterns.
Public + Private Data
export async function getProfile(username: string) {
const session = await verifySession().catch(() => null);
const profile = await db.query.users.findFirst({
where: eq(users.username, username),
columns: {
username: true,
bio: true,
email: session ? true : false,
},
});
return profile;
}
Key Takeaways
- •CVE-2025-29927 makes middleware authentication unsafe - redirects don't stop execution
- •Data Access Layer is mandatory - verify session before every data access
- •Multi-layer security - route protection (UX) + DAL (security) + server actions (mutations)
- •verifySession() everywhere - make it a habit to call this first
- •Use React cache() - prevents multiple verification calls per request
References
See the references/ directory for:
- •
dal-example.md- Complete working example with full implementation - •
cve-2025-29927.md- Detailed vulnerability analysis and exploitation examples
When to Use This Skill
Apply this pattern when:
- •Implementing authentication in Next.js 16 applications
- •Protecting routes that require user login
- •Creating server actions that modify user data
- •Building authorization systems (roles, permissions)
- •Fetching user-specific data from databases
- •Any time you see middleware being used for authentication
- •Reviewing code for security vulnerabilities
This is the most important security pattern in Next.js 16. Use it in every authenticated application.