auth

Role-Based Access Control (RBAC) in React Applications

Marcus Weber20 min10 Feb 2024

  <h2>What is RBAC?</h2>
  <p>Role-Based Access Control (RBAC) is a method of regulating access to resources based on the roles of individual users. Instead of assigning permissions to each user, you assign them to roles, and users inherit permissions from their roles.</p>
  
  <h2>RBAC Concepts</h2>
  <ul>
    <li><strong>Users:</strong> Individual people using your application</li>
    <li><strong>Roles:</strong> Job functions (admin, editor, viewer)</li>
    <li><strong>Permissions:</strong> Actions users can perform (read, write, delete)</li>
    <li><strong>Resources:</strong> What permissions apply to (articles, users, settings)</li>
  </ul>
  
  <h2>Designing Your Permission System</h2>
  <p>Start by mapping out your application's roles and permissions:</p>
  
  <pre><code>// Define roles and permissions

export const ROLES = { ADMIN: 'admin', EDITOR: 'editor', AUTHOR: 'author', VIEWER: 'viewer', } as const;

export const PERMISSIONS = { // Article permissions ARTICLE_CREATE: 'article:create', ARTICLE_READ: 'article:read', ARTICLE_UPDATE: 'article:update', ARTICLE_DELETE: 'article:delete', ARTICLE_PUBLISH: 'article:publish',

// User permissions USER_CREATE: 'user:create', USER_READ: 'user:read', USER_UPDATE: 'user:update', USER_DELETE: 'user:delete', } as const;

// Map roles to permissions export const ROLE_PERMISSIONS = { [ROLES.ADMIN]: Object.values(PERMISSIONS), [ROLES.EDITOR]: [ PERMISSIONS.ARTICLE_CREATE, PERMISSIONS.ARTICLE_READ, PERMISSIONS.ARTICLE_UPDATE, PERMISSIONS.ARTICLE_PUBLISH, PERMISSIONS.USER_READ, ], [ROLES.AUTHOR]: [ PERMISSIONS.ARTICLE_CREATE, PERMISSIONS.ARTICLE_READ, PERMISSIONS.ARTICLE_UPDATE, ], [ROLES.VIEWER]: [ PERMISSIONS.ARTICLE_READ, ], };

  <h2>Implementing RBAC in React</h2>
  
  <h3>Permission Hook</h3>
  <pre><code>// hooks/usePermissions.ts

import { useSession } from 'next-auth/react';

export function usePermissions() { const { data: session } = useSession(); const userRole = session?.user?.role; const permissions = ROLE_PERMISSIONS[userRole] || [];

const hasPermission = (permission: string) => { return permissions.includes(permission); };

const hasAnyPermission = (requiredPermissions: string[]) => { return requiredPermissions.some(p => permissions.includes(p)); };

const hasAllPermissions = (requiredPermissions: string[]) => { return requiredPermissions.every(p => permissions.includes(p)); };

return { hasPermission, hasAnyPermission, hasAllPermissions, permissions }; }

  <h3>Protected Component</h3>
  <pre><code>// components/ProtectedComponent.tsx

import { usePermissions } from '@/hooks/usePermissions';

export function DeleteButton({ articleId }: { articleId: string }) { const { hasPermission } = usePermissions();

if (!hasPermission(PERMISSIONS.ARTICLE_DELETE)) { return null; // Hide button if no permission }

return ( <button onClick={() => deleteArticle(articleId)}> Delete Article ); }

  <h2>Route Protection</h2>
  <p>Protect entire routes based on permissions:</p>
  
  <pre><code>// middleware.ts

import { getToken } from 'next-auth/jwt'; import { NextResponse } from 'next/server';

export async function middleware(req: NextRequest) { const token = await getToken({ req }); const path = req.nextUrl.pathname;

// Admin routes if (path.startsWith('/admin')) { if (!token || token.role !== 'admin') { return NextResponse.redirect(new URL('/unauthorized', req.url)); } }

// Editor routes if (path.startsWith('/editor')) { if (!token || !['admin', 'editor'].includes(token.role)) { return NextResponse.redirect(new URL('/unauthorized', req.url)); } }

return NextResponse.next(); }

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

  <h2>API Route Protection</h2>
  <pre><code>// app/api/articles/[id]/route.ts

export async function DELETE(req: Request, { params }: { params: { id: string } }) { const session = await getServerSession(authOptions);

if (!session) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); }

const permissions = ROLE_PERMISSIONS[session.user.role] || []; if (!permissions.includes(PERMISSIONS.ARTICLE_DELETE)) { return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); }

// User has permission, proceed with deletion await Article.findByIdAndDelete(params.id); return NextResponse.json({ message: 'Article deleted' }); }

  <h2>Testing RBAC</h2>
  <p>Test all permission combinations:</p>
  <ul>
    <li>Test each role can access what they should</li>
    <li>Test each role is blocked from what they shouldn't</li>
    <li>Test edge cases (no role, invalid role)</li>
    <li>Test permission changes take effect immediately</li>
  </ul>
  
  <h2>Conclusion</h2>
  <p>RBAC provides a scalable way to manage permissions. By defining clear roles and permissions, you can build secure applications that scale from 10 to 10,000 users. The key is starting with a well-designed permission system and enforcing it consistently across your application.</p>

About the Author
Marcus Weber
Marcus Weber

Lead QA Engineer

Built testing frameworks used by 50+ companies, 12+ years in QA

QA Engineer and testing advocate. Built testing frameworks for Fortune 500 companies. Passionate about making testing accessible and effective for all developers.

Expertise:
JestPlaywrightCypressTesting StrategyCI/CDTest Automation

Get notified of updates

Subscribe to receive an email when this article is updated with new information.

We'll only email you about updates to this specific article. Unsubscribe anytime.
Share this article

Tagged with

rbacpermissionsauthorizationaccess-controlreactrolessecurity