Skip to content

Next.js Integration

Fire Shield provides seamless integration with Next.js for both App Router (Next.js 13+) and Pages Router (Next.js 12).

Installation

bash
npm install @fire-shield/next@3.1.1 @fire-shield/core@3.1.1

Setup

Initialize RBAC and Adapter

typescript
// lib/rbac.ts
import { RBAC } from '@fire-shield/core';
import { NextRBACAdapter } from '@fire-shield/next';
import { getUser } from './auth';

export const rbac = new RBAC();

// Define roles
rbac.createRole('admin', ['user:*', 'post:*']);
rbac.createRole('editor', ['post:read', 'post:write']);
rbac.createRole('viewer', ['post:read']);

// Create the adapter
export const rbacAdapter = new NextRBACAdapter(rbac, {
  getUser: (req) => getUser(req),
});

App Router (Next.js 13+)

Middleware Protection

Protect routes using Next.js middleware with the adapter's middleware method:

typescript
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { rbacAdapter } from './lib/rbac';

const adminMiddleware = rbacAdapter.middleware('admin:access');

export async function middleware(request: NextRequest) {
  // Protect admin routes
  if (request.nextUrl.pathname.startsWith('/admin')) {
    const response = await adminMiddleware(request);
    if (response) return response; // returns 403 if unauthorized
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/admin/:path*', '/api/admin/:path*'],
};

Route-Based Protection

typescript
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { rbac } from './lib/rbac';

const protectedRoutes = {
  '/admin': 'admin:access',
  '/posts/create': 'post:write',
  '/users': 'user:read',
};

export function middleware(request: NextRequest) {
  const pathname = request.nextUrl.pathname;
  const user = getUserFromRequest(request);

  for (const [route, permission] of Object.entries(protectedRoutes)) {
    if (pathname.startsWith(route)) {
      if (!rbac.hasPermission(user, permission)) {
        return NextResponse.redirect(new URL('/unauthorized', request.url));
      }
    }
  }

  return NextResponse.next();
}

Server Components

Check permissions in server components:

typescript
// app/admin/page.tsx
import { rbac } from '@/lib/rbac';
import { getUser } from '@/lib/auth';
import { redirect } from 'next/navigation';

export default async function AdminPage() {
  const user = await getUser();

  if (!rbac.hasPermission(user, 'admin:access')) {
    redirect('/unauthorized');
  }

  return (
    <div>
      <h1>Admin Dashboard</h1>
      {/* Admin content */}
    </div>
  );
}

API Routes

Protect API routes using the adapter's withPermission or withRole methods:

typescript
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { rbacAdapter } from '@/lib/rbac';
import { getUsers, createUser } from '@/lib/users';

export const GET = rbacAdapter.withPermission(
  'user:read',
  async (request: NextRequest) => {
    const users = await getUsers();
    return NextResponse.json({ users });
  }
);

export const POST = rbacAdapter.withPermission(
  'user:create',
  async (request: NextRequest) => {
    const body = await request.json();
    const newUser = await createUser(body);
    return NextResponse.json({ user: newUser });
  }
);

You can also check permissions manually:

typescript
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { rbac } from '@/lib/rbac';
import { getUser } from '@/lib/auth';

export async function GET(request: NextRequest) {
  const user = await getUser(request);

  // Check permission with detailed result
  const result = rbac.authorize(user, 'user:read');
  if (!result.allowed) {
    return NextResponse.json(
      { error: 'Forbidden', reason: result.reason },
      { status: 403 }
    );
  }

  const users = await getUsers();
  return NextResponse.json({ users });
}

API Routes with Role Check

typescript
// app/api/admin/stats/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { rbacAdapter } from '@/lib/rbac';

export const GET = rbacAdapter.withRole(
  'admin',
  async (request: NextRequest) => {
    const stats = await getAdminStats();
    return NextResponse.json({ stats });
  }
);

Server Actions

Use the adapter's requirePermission or requireRole methods in server actions:

typescript
// app/actions/users.ts
'use server';

import { rbacAdapter } from '@/lib/rbac';
import { getUser } from '@/lib/auth';
import { revalidatePath } from 'next/cache';

export async function deleteUser(userId: string) {
  const user = await getUser();

  // Throws if permission is missing
  await rbacAdapter.requirePermission(user, 'user:delete');

  await db.users.delete(userId);
  revalidatePath('/admin/users');

  return { success: true };
}

export async function updateUserRole(userId: string, newRole: string) {
  const user = await getUser();

  await rbacAdapter.requirePermission(user, 'user:update');

  await db.users.update(userId, { role: newRole });
  revalidatePath('/admin/users');

  return { success: true };
}

Client Component Protection

Hide/show UI elements based on permissions:

typescript
// components/AdminButton.tsx
'use client';

import { useUser } from '@/hooks/useUser';
import { rbac } from '@/lib/rbac';

export function AdminButton() {
  const user = useUser();

  if (!rbac.hasPermission(user, 'admin:access')) {
    return null;
  }

  return (
    <button onClick={handleAdminAction}>
      Admin Panel
    </button>
  );
}
typescript
// components/PostActions.tsx
'use client';

import { useUser } from '@/hooks/useUser';
import { rbac } from '@/lib/rbac';

export function PostActions({ postId }: { postId: string }) {
  const user = useUser();

  return (
    <div>
      {rbac.hasPermission(user, 'post:read') && (
        <button onClick={() => viewPost(postId)}>View</button>
      )}

      {rbac.hasPermission(user, 'post:write') && (
        <button onClick={() => editPost(postId)}>Edit</button>
      )}

      {rbac.hasPermission(user, 'post:delete') && (
        <button onClick={() => deletePost(postId)}>Delete</button>
      )}
    </div>
  );
}

Pages Router (Next.js 12)

API Routes

typescript
// pages/api/users.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { rbac } from '@/lib/rbac';
import { getUser } from '@/lib/auth';

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const user = await getUser(req);

  if (!rbac.hasPermission(user, 'user:read')) {
    return res.status(403).json({ error: 'Forbidden' });
  }

  const users = await getUsers();
  res.json({ users });
}

Pages Router with Adapter HOC

The adapter provides withPermissionPagesRouter and withRolePagesRouter for Pages Router API routes:

typescript
// pages/api/admin/users.ts
import { rbacAdapter } from '@/lib/rbac';

export default rbacAdapter.withPermissionPagesRouter(
  'user:read',
  async (req, res) => {
    const users = await getUsers();
    res.json({ users });
  }
);
typescript
// pages/api/admin/stats.ts
import { rbacAdapter } from '@/lib/rbac';

export default rbacAdapter.withRolePagesRouter(
  'admin',
  async (req, res) => {
    const stats = await getAdminStats();
    res.json({ stats });
  }
);

HOC for Page Protection

typescript
// lib/withPermission.ts
import { GetServerSideProps, GetServerSidePropsContext } from 'next';
import { rbac } from './rbac';
import { getUser } from './auth';

export function withPermission(
  permission: string,
  getServerSidePropsFunc?: GetServerSideProps
): GetServerSideProps {
  return async (context: GetServerSidePropsContext) => {
    const user = await getUser(context);

    if (!rbac.hasPermission(user, permission)) {
      return {
        redirect: {
          destination: '/unauthorized',
          permanent: false,
        },
      };
    }

    if (getServerSidePropsFunc) {
      return getServerSidePropsFunc(context);
    }

    return { props: {} };
  };
}

Usage:

typescript
// pages/admin/users.tsx
import { withPermission } from '@/lib/withPermission';

export const getServerSideProps = withPermission('user:read', async () => {
  const users = await getUsers();
  return { props: { users } };
});

export default function UsersPage({ users }) {
  return (
    <div>
      <h1>Users</h1>
      {/* Render users */}
    </div>
  );
}

API Reference

new NextRBACAdapter(rbac, options?)

Creates a new Next.js adapter instance.

Options:

  • getUser?: (req) => RBACUser | Promise<RBACUser> - Extract user from request
  • onUnauthorized?: (result, req, res?) => Response | void - Custom unauthorized handler
  • onError?: (error, req, res?) => Response | void - Custom error handler

Methods

adapter.middleware(permission)

Returns an async function (request: NextRequest) => Promise<Response | undefined>. Use in middleware.ts for route-level protection.

adapter.withPermission(permission, handler)

HOC for App Router route handlers. Wraps a handler with a permission check.

typescript
export const GET = rbacAdapter.withPermission('user:read', async (req) => {
  return NextResponse.json({ users: await getUsers() });
});

adapter.withRole(role, handler)

HOC for App Router route handlers. Wraps a handler with a role check.

typescript
export const GET = rbacAdapter.withRole('admin', async (req) => {
  return NextResponse.json({ stats: await getAdminStats() });
});

adapter.withPermissionPagesRouter(permission, handler)

HOC for Pages Router API routes.

adapter.withRolePagesRouter(role, handler)

HOC for Pages Router API routes with role check.

adapter.checkPermission(user, permission)

Returns Promise<AuthorizationResult>. Use in Server Components or Server Actions.

adapter.requirePermission(user, permission)

Throws an error if the user lacks the permission. Use in Server Actions.

adapter.requireRole(user, role)

Throws an error if the user lacks the role. Use in Server Actions.

adapter.withNotDenied(permission, handler)

HOC that blocks access if the given permission is explicitly denied for the user.

adapter.denyPermission(request, permission)

Explicitly deny a permission for the user found in the request.

adapter.allowPermission(request, permission)

Remove an explicit deny for the user found in the request.

adapter.isDenied(request, permission)

Returns Promise<boolean> indicating whether the permission is explicitly denied.

createPermissionChecker(rbac, getUser)

Standalone helper that creates a reusable permission-checking function:

typescript
import { createPermissionChecker } from '@fire-shield/next';

const checkPermission = createPermissionChecker(rbac, getUser);

// In a route or component:
const allowed = await checkPermission(request, 'post:write');

createRoleChecker(getUser)

Standalone helper that creates a reusable role-checking function:

typescript
import { createRoleChecker } from '@fire-shield/next';

const checkRole = createRoleChecker(getUser);

const isAdmin = await checkRole(request, 'admin');

Authentication Integration

With NextAuth.js

typescript
// lib/auth.ts
import { getServerSession } from 'next-auth/next';
import { authOptions } from './authOptions';
import type { RBACUser } from '@fire-shield/core';

export async function getUser(): Promise<RBACUser | null> {
  const session = await getServerSession(authOptions);

  if (!session?.user) {
    return null;
  }

  return {
    id: session.user.id,
    roles: session.user.roles || [],
  };
}
typescript
// middleware.ts
import { getToken } from 'next-auth/jwt';
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { rbac } from './lib/rbac';

export async function middleware(request: NextRequest) {
  const token = await getToken({
    req: request,
    secret: process.env.NEXTAUTH_SECRET,
  });

  const user = token
    ? { id: token.sub as string, roles: token.roles as string[] }
    : null;

  if (!user || !rbac.hasPermission(user, 'admin:access')) {
    return NextResponse.redirect(new URL('/unauthorized', request.url));
  }

  return NextResponse.next();
}

export const config = {
  matcher: '/admin/:path*',
};

Audit Logging

Database Audit Logger

typescript
// lib/auditLogger.ts
import { BufferedAuditLogger } from '@fire-shield/core';
import { db } from '@/lib/database';

export const auditLogger = new BufferedAuditLogger(
  async (events) => {
    await db.auditLogs.insertMany(
      events.map((event) => ({
        type: event.type,
        userId: event.userId,
        permission: event.permission,
        allowed: event.allowed,
        reason: event.reason,
        context: event.context,
        createdAt: new Date(event.timestamp),
      }))
    );
  },
  {
    maxBufferSize: 100,
    flushIntervalMs: 5000,
  }
);
typescript
// lib/rbac.ts
import { RBAC } from '@fire-shield/core';
import { auditLogger } from './auditLogger';

export const rbac = new RBAC({ auditLogger });

API Route for Audit Logs

typescript
// app/api/audit-logs/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { rbacAdapter } from '@/lib/rbac';
import { db } from '@/lib/database';

export const GET = rbacAdapter.withPermission(
  'audit:read',
  async (request: NextRequest) => {
    const logs = await db.auditLogs.find().sort({ createdAt: -1 }).limit(100);
    return NextResponse.json({ logs });
  }
);

Dynamic Permissions

Check permissions based on resource ownership:

typescript
// app/api/posts/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { rbac } from '@/lib/rbac';
import { getUser } from '@/lib/auth';

export async function DELETE(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  const user = await getUser(request);
  const post = await getPost(params.id);

  // Check if user owns the post
  const isOwner = post.authorId === user.id;

  // Check permission with ownership
  const canDelete =
    rbac.hasPermission(user, 'post:delete:any') ||
    (isOwner && rbac.hasPermission(user, 'post:delete:own'));

  if (!canDelete) {
    return NextResponse.json(
      { error: 'You do not have permission to delete this post' },
      { status: 403 }
    );
  }

  await deletePost(params.id);
  return NextResponse.json({ success: true });
}

Best Practices

1. Centralize Permission Checks

typescript
// lib/permissions.ts
import { rbac } from './rbac';
import type { RBACUser } from '@fire-shield/core';

export const permissions = {
  canManageUsers: (user: RBACUser) => rbac.hasPermission(user, 'user:manage'),
  canEditPost: (user: RBACUser, post: Post) =>
    rbac.hasPermission(user, 'post:edit:any') ||
    (post.authorId === user.id && rbac.hasPermission(user, 'post:edit:own')),
  canDeletePost: (user: RBACUser, post: Post) =>
    rbac.hasPermission(user, 'post:delete:any') ||
    (post.authorId === user.id && rbac.hasPermission(user, 'post:delete:own')),
};

2. Use TypeScript for Type Safety

typescript
// types/permissions.ts
export type Permission =
  | 'user:read'
  | 'user:write'
  | 'user:delete'
  | 'post:read'
  | 'post:write'
  | 'post:delete'
  | 'admin:access';

export type Role = 'admin' | 'editor' | 'viewer';

3. Create Reusable Middleware

typescript
// lib/middleware/requirePermission.ts
import { NextRequest, NextResponse } from 'next/server';
import { rbac } from '../rbac';
import { getUser } from '../auth';

export function requirePermission(permission: string) {
  return async (request: NextRequest) => {
    const user = await getUser(request);

    if (!rbac.hasPermission(user, permission)) {
      return NextResponse.json(
        { error: 'Forbidden', required: permission },
        { status: 403 }
      );
    }

    return NextResponse.next();
  };
}

TypeScript Support

typescript
import type { RBACUser } from '@fire-shield/core';

interface User extends RBACUser {
  email: string;
  name: string;
}

const user: User = {
  id: 'user-123',
  roles: ['editor'],
  email: 'user@example.com',
  name: 'John Doe',
};

rbac.hasPermission(user, 'post:write');

Next Steps