Overview
Implement authorization with role-based access control (RBAC) and scope-based isolation. Always verify permissions before allowing access to resources.
What it is
Permission checking utilities for roles (admin, member, viewer) and scopes (user, program, project) with ownership verification.
Why we use it
Prevent unauthorized access, enforce principle of least privilege, and maintain data isolation between programs.
When to use
Any operation that modifies data or accesses sensitive resources. Always check permissions after authentication.
Key Features
- Role-based access (admin, member, viewer)
- Scope isolation (user, program, project)
- Resource ownership verification
- Permission checking utilities
Quick Start
Permission Check
Basic authorization check in an API route.
// Basic permission check in API route
import { auth } from '@/lib/middleware/auth';
import { AuthorizationError } from '@/lib/errors';
export async function DELETE(
request: Request,
{ params }: { params: { id: string } }
) {
const session = await auth();
if (!session) throw new AuthenticationError();
// Check if user owns the resource
const item = await getItem(params.id);
if (item.userId !== session.user.id) {
throw new AuthorizationError('You cannot delete this item');
}
await deleteItem(params.id);
return NextResponse.json({ ok: true, data: null });
}Patterns
Role-Based Access
Check user roles for operations.
// Role-based access control
type Role = 'admin' | 'member' | 'viewer';
function hasRole(user: User, requiredRole: Role): boolean {
const roleHierarchy: Record<Role, number> = {
viewer: 1,
member: 2,
admin: 3,
};
return roleHierarchy[user.role] >= roleHierarchy[requiredRole];
}
export async function POST(request: Request) {
const session = await auth();
if (!hasRole(session.user, 'admin')) {
throw new AuthorizationError('Admin access required');
}
// Admin-only operation
await createUser(body);
}Scope-Based Access
Verify program/project access.
// Scope-based access control
import { checkProgramAccess } from '@/features/programs';
export async function GET(
request: Request,
{ params }: { params: { programId: string } }
) {
const session = await auth();
// Check if user has access to this program
const hasAccess = await checkProgramAccess(
session.user.id,
params.programId
);
if (!hasAccess) {
throw new AuthorizationError('Access denied to this program');
}
const data = await getProgramData(params.programId);
return NextResponse.json({ ok: true, data });
}Ownership Verification
Check resource ownership.
// Resource ownership verification
export async function PUT(
request: Request,
{ params }: { params: { id: string } }
) {
const session = await auth();
const body = await request.json();
// Fetch resource and verify ownership
const resource = await getResource(params.id);
if (!resource) {
throw new NotFoundError('Resource not found');
}
// Check ownership OR admin role
const isOwner = resource.createdById === session.user.id;
const isAdmin = session.user.role === 'admin';
if (!isOwner && !isAdmin) {
throw new AuthorizationError('You cannot modify this resource');
}
const updated = await updateResource(params.id, body);
return NextResponse.json({ ok: true, data: updated });
}Page Access Control
Centralized access denial for page routes that redirects to /access-denied with context for debugging.
// Page-level access control with denyAccess
import { denyAccess } from '@/lib/auth/deny-access';
import { resolveScope, getScopeAccess } from '@/lib/scope';
export default async function SettingsPage({ params }) {
const { programSlug, projectSlug } = await params;
const scope = await resolveScope({ programSlug, projectSlug });
const access = getScopeAccess(scope);
// Redirects to /access-denied with context
if (!access.isOwner) {
denyAccess({
scope: 'project',
programSlug,
projectSlug,
reason: 'owner_required',
});
}
return <SettingsForm />;
}Watch Out
IDOR vulnerabilities (accessing other users' data)
Don't
// IDOR vulnerability - no ownership check
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
// Anyone can access any user's data!
const data = await getUserData(params.id);
return NextResponse.json({ ok: true, data });
}Do
// Proper ownership verification
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const session = await auth();
// Only allow accessing own data
if (params.id !== session.user.id) {
throw new AuthorizationError('Access denied');
}
const data = await getUserData(params.id);
return NextResponse.json({ ok: true, data });
}Overly permissive default permissions
Don't
// Overly permissive default
function canAccess(user: User, resource: Resource) {
// Default to allowing access - dangerous!
if (!resource.permissions) return true;
return resource.permissions.includes(user.role);
}Do
// Restrictive default (principle of least privilege)
function canAccess(user: User, resource: Resource) {
// Default to denying access
if (!resource.permissions) return false;
return resource.permissions.includes(user.role);
}- Missing scope checks on queries
- Client-side only authorization checks