Instruction file imported from jpdacunha/training-cursor-flashycardyapp (
.cursor/rules/security-authentication-authorization.mdc). Copyright stays with the author.
Authentication & Authorization Security
Core Security Principle
CRITICAL: All authentication is handled by Clerk. Users must ONLY be able to access their own data and must NEVER be able to access data belonging to other users.
Authentication Provider
This application uses Clerk for authentication. All auth-related operations must use Clerk's APIs and helpers.
Mandatory Security Checks
1. API Routes Pattern
EVERY API route must follow this security pattern:
import { auth } from '@clerk/nextjs/server';
import { NextResponse } from 'next/server';
export async function GET() {
// STEP 1: Get authenticated user ID
const { userId } = await auth();
// STEP 2: Check authentication
if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
// STEP 3: Query ONLY data belonging to this user
const userDecks = await db
.select()
.from(decksTable)
.where(eq(decksTable.userId, userId)); // ✅ CRITICAL: Filter by userId
return NextResponse.json({ decks: userDecks });
}
2. Database Query Pattern
EVERY database query that retrieves user-specific data must filter by userId:
// ✅ CORRECT: Filter by authenticated user ID
const decks = await db
.select()
.from(decksTable)
.where(eq(decksTable.userId, userId));
// ❌ WRONG: Returns ALL decks from ALL users
const decks = await db.select().from(decksTable);
3. Resource Access Validation
When accessing a specific resource by ID (e.g., deck by deckId, card by cardId), you must:
- Get the authenticated
userIdfrom Clerk - Verify the resource belongs to the authenticated user
- Return 404 (not 403) if resource doesn't exist or doesn't belong to user
export async function GET(
request: Request,
{ params }: { params: { deckId: string } }
) {
const { userId } = await auth();
if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { deckId } = await params;
// ✅ CORRECT: Verify deck belongs to user
const [deck] = await db
.select()
.from(decksTable)
.where(
and(
eq(decksTable.id, parseInt(deckId)),
eq(decksTable.userId, userId) // CRITICAL: Check ownership
)
)
.limit(1);
if (!deck) {
return NextResponse.json({ error: 'Deck not found' }, { status: 404 });
}
return NextResponse.json({ deck });
}
4. Nested Resource Access
For nested resources (e.g., cards within a deck), verify ownership at the parent level:
// When accessing /api/decks/[deckId]/cards
export async function GET(
request: Request,
{ params }: { params: { deckId: string } }
) {
const { userId } = await auth();
if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { deckId } = await params;
// STEP 1: Verify deck belongs to user
const [deck] = await db
.select()
.from(decksTable)
.where(
and(
eq(decksTable.id, parseInt(deckId)),
eq(decksTable.userId, userId)
)
)
.limit(1);
if (!deck) {
return NextResponse.json({ error: 'Deck not found' }, { status: 404 });
}
// STEP 2: Get cards for this deck
const cards = await db
.select()
.from(cardsTable)
.where(eq(cardsTable.deckId, parseInt(deckId)));
return NextResponse.json({ cards });
}
Security Checklist
Before completing ANY API route or data access code, verify:
-
auth()is called to getuserId -
userIdis checked (return 401 if not authenticated) - Database queries filter by
userIdor verify resource ownership - No query returns data from other users
- Resource IDs from params/body are validated
- Error responses don't leak information about other users' data
- Server actions (if used) follow the same security pattern
Common Vulnerabilities to AVOID
❌ VULNERABILITY 1: Missing User Filter
// ❌ DANGER: Returns ALL users' decks
export async function GET() {
const decks = await db.select().from(decksTable);
return NextResponse.json({ decks });
}
❌ VULNERABILITY 2: Trusting Client-Provided User IDs
// ❌ DANGER: Client can pass any userId
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const userId = searchParams.get('userId'); // ❌ Never trust client!
const decks = await db
.select()
.from(decksTable)
.where(eq(decksTable.userId, userId));
return NextResponse.json({ decks });
}
❌ VULNERABILITY 3: Missing Ownership Verification
// ❌ DANGER: Any authenticated user can access any deck
export async function DELETE(
request: Request,
{ params }: { params: { deckId: string } }
) {
const { userId } = await auth();
if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { deckId } = await params;
// ❌ Missing ownership check!
await db.delete(decksTable).where(eq(decksTable.id, parseInt(deckId)));
return NextResponse.json({ success: true });
}
❌ VULNERABILITY 4: Insecure Joins
// ❌ DANGER: Could expose other users' data if not careful
const results = await db
.select()
.from(decksTable)
.leftJoin(cardsTable, eq(decksTable.id, cardsTable.deckId));
// ❌ Missing userId filter on decksTable!
Server Components
For server components that need user data:
import { auth } from '@clerk/nextjs/server';
import { redirect } from 'next/navigation';
export default async function DashboardPage() {
const { userId } = await auth();
if (!userId) {
redirect('/'); // Redirect to homepage where sign-in/sign-up buttons are located
}
// ✅ CORRECT: Filter by userId
const decks = await db
.select()
.from(decksTable)
.where(eq(decksTable.userId, userId));
return <div>{/* Render user's decks */}</div>;
}
Middleware Protection
Protect routes using Clerk middleware in middleware.ts:
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
const isProtectedRoute = createRouteMatcher([
'/dashboard(.*)',
'/decks(.*)',
'/api/decks(.*)',
'/api/cards(.*)',
]);
export default clerkMiddleware(async (auth, req) => {
if (isProtectedRoute(req)) await auth.protect();
});
Database Schema Requirements
All user-owned tables must have a userId column that references Clerk's user ID:
export const decksTable = pgTable("decks", {
id: serial("id").primaryKey(),
userId: text("user_id").notNull(), // ✅ CRITICAL: Required for ownership
title: text("title").notNull(),
// ... other fields
});
Testing Security
When testing, verify:
- Unauthenticated requests return 401
- Authenticated users can only see their own data
- Users cannot access/modify other users' resources by ID manipulation
- Joins and nested queries maintain user isolation
Error Handling
- Return 401 Unauthorized when
userIdis missing/invalid - Return 404 Not Found when resource doesn't exist OR doesn't belong to user
- Never return 403 Forbidden - it leaks information about resource existence
- Don't include sensitive details in error messages
References
- Database schema: src/db/schema.ts
- Database connection: lib/db.ts
- Clerk documentation: https://clerk.com/docs
Summary
The Golden Rule: Every data access operation must be scoped to the authenticated user's userId obtained from Clerk's auth() function. No exceptions.