CentercodePorygon
DittoPorygonAPI

Authorization

Role-based and scope-based access control

Safe

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

Related

Authentication

Session management

Scope Model

Scope isolation

API Architecture

API patterns