Next.js Lesson 19 – Authorization | Dataplexa
Lesson 19

Authorization

Build role-based access control and permission systems to secure different areas of NewsWave based on user roles and permissions.
Authorization controls what authenticated users can do. While authentication answers "Who are you?", authorization answers "What can you do?". The NewsWave editorial team needs different permission levels — editors can publish articles, writers can draft them, and subscribers can only read premium content. Authorization happens after authentication. You know who the user is, now you decide what they can access. GitHub uses this pattern perfectly — you can view public repos (no auth needed), contribute to repos you have access to (authorization), or create private repos (paid authorization). Modern web apps use role-based access control (RBAC). Users get roles like "admin", "editor", or "user". Each role has specific permissions. The system checks these permissions before showing content or processing requests.

Understanding Authorization vs Authentication

Authentication and authorization work together but solve different problems. Authentication verifies identity — like showing your driver's license at a bar. Authorization checks permissions — like the bouncer deciding if you can enter the VIP section based on your membership level. Many developers confuse these concepts. You can be authenticated but not authorized. Think about Vercel's dashboard — you can log in (authenticated) but only see projects you own or have been invited to (authorized). The system knows who you are, but limits what you can do. Authorization happens at multiple layers. Frontend authorization hides UI elements users shouldn't see. Backend authorization protects API routes and database operations. Both layers are essential — never rely on frontend-only authorization for security.
// Frontend authorization - hide UI elements
function ArticleActions({ user, article }) {
  // Only show edit button if user can edit this article
  const canEdit = user.role === 'editor' || user.id === article.authorId;
  
  return (
    <div>
      {canEdit && <EditButton />}
      <ShareButton />
    </div>
  );
}
localhost:3000 — NewsWave
What just happened?
The component checks user permissions before rendering the edit button. Editors see edit buttons on all articles, while writers only see them on articles they authored. The share button appears for everyone since sharing is a universal permission.

Role-Based Access Control (RBAC)

RBAC organizes permissions around roles instead of individual users. You assign roles to users, and roles contain specific permissions. This scales better than managing permissions per user — imagine GitHub giving repository access to each developer individually instead of using team roles. Roles form a hierarchy in most systems. An "admin" role might include all "editor" permissions, plus additional admin-only features. This inheritance reduces complexity and makes permission management predictable. The NewsWave team needs different roles for their editorial workflow. Subscribers read content, writers create drafts, editors publish articles, and admins manage the entire system. Each role builds upon the previous one's permissions.
// Define roles and their permissions for NewsWave
const ROLES = {
  subscriber: {
    name: 'Subscriber',
    permissions: ['read:articles', 'read:comments'] // Basic reading access
  },
  writer: {
    name: 'Writer', 
    permissions: ['read:articles', 'create:drafts', 'edit:own_drafts'] // Can write
  },
  editor: {
    name: 'Editor',
    permissions: ['read:articles', 'create:articles', 'edit:all_articles', 'publish:articles'] // Can publish
  }
};
Terminal
$ mkdir lib/auth && touch lib/auth/permissions.js
Creating authorization utilities...
✓ Permission system files created
Permission naming follows a pattern: action:resource format. "read:articles" means reading articles, "edit:own_drafts" means editing drafts you created. This makes permissions self-documenting and easy to understand.
// Check if user has specific permission
function hasPermission(user, permission) {
  // Get user's role from the ROLES object
  const role = ROLES[user.role];
  if (!role) return false; // Role doesn't exist
  
  // Check if role includes this permission
  return role.permissions.includes(permission);
}
localhost:3000 — NewsWave
What just happened?
The permission system checks each user's role against required permissions. Subscribers can only read, writers can also create drafts, and editors have full publishing rights. The visual checkmarks show exactly what each role can access.

Protecting API Routes

API route protection happens on the server where users can't manipulate it. Frontend authorization improves user experience, but backend authorization provides real security. Attackers can bypass frontend checks, but they can't fake server-side authentication tokens. Next.js API routes check authorization before processing requests. Extract the user's session, verify their role, then decide whether to continue or return an error. This pattern protects every backend operation. The NewsWave publishing system needs secure API endpoints. Only editors should publish articles, only writers should create drafts, and only admins should delete content. Each endpoint validates permissions before executing.
// API route with role-based authorization
import { getServerSession } from 'next-auth/next';
import { authOptions } from '../auth/[...nextauth]';

export default async function handler(req, res) {
  // Get user session from the request
  const session = await getServerSession(req, res, authOptions);
  
  if (!session) {
    return res.status(401).json({ error: 'Not authenticated' }); // Must be logged in
  }
}
Terminal
$ mkdir pages/api/articles && touch pages/api/articles/publish.js
Creating protected API endpoint...
✓ Publish endpoint ready for authorization
Session validation comes first — reject unauthenticated requests immediately. Then check specific permissions for the requested action. This two-step process ensures both identity and authorization.
// Complete API route with permission checking
export default async function publishArticle(req, res) {
  const session = await getServerSession(req, res, authOptions);
  
  if (!session) {
    return res.status(401).json({ error: 'Authentication required' });
  }
  
  // Check if user has publish permission
  if (!hasPermission(session.user, 'publish:articles')) {
    return res.status(403).json({ error: 'Insufficient permissions' }); // Forbidden
  }
  
  // User is authenticated and authorized - process the request
  const { articleId } = req.body;
  // Publish the article logic here...
  
  res.status(200).json({ success: true, message: 'Article published' });
}
Terminal
$ curl -X POST localhost:3000/api/articles/publish -d '{"articleId":123}'
{"error":"Authentication required"}
$ curl -X POST localhost:3000/api/articles/publish -H "Authorization: Bearer token" -d '{"articleId":123}'
{"success":true,"message":"Article published"}
HTTP status codes communicate authorization results clearly. 401 means "authentication required" — the user needs to log in. 403 means "forbidden" — the user is authenticated but lacks permission. 200 means success.
What just happened?
The API endpoint validates authentication first, then checks specific permissions. Unauthenticated requests get 401 errors, unauthorized requests get 403 errors, and valid requests proceed normally. This creates multiple security layers.

Middleware Authorization

Next.js middleware runs before pages load, making it perfect for route-level authorization. Instead of checking permissions in every page component, middleware handles access control centrally. This prevents unauthorized users from even seeing protected pages. Middleware executes at the edge — closer to users than your main server. This means faster authorization decisions and better user experience. Vercel's edge network runs your middleware code globally. The NewsWave admin panel needs middleware protection. Editors shouldn't see admin-only pages, and regular users shouldn't access the content management system at all. Middleware redirects unauthorized users before pages render.
// middleware.js - runs before every request
import { withAuth } from 'next-auth/middleware';

export default withAuth(
  function middleware(req) {
    // This runs after authentication is verified
    const { pathname } = req.nextUrl;
    const userRole = req.nextauth.token?.role;
    
    // Protect admin routes - only admins allowed
    if (pathname.startsWith('/admin') && userRole !== 'admin') {
      return Response.redirect(new URL('/unauthorized', req.url)); // Redirect away
    }
  }
);
Terminal
$ touch middleware.js
Creating middleware file...
✓ Middleware will protect routes automatically
The middleware matcher specifies which routes to protect. You can target specific paths, use wildcards, or exclude certain routes. This gives fine-grained control over where authorization runs.
// Configure which routes middleware should protect
export const config = {
  matcher: [
    '/admin/:path*',      // All admin routes
    '/editor/:path*',     // All editor routes  
    '/api/articles/:path*' // All article API routes
  ]
};

// Alternative: protect everything except public routes
export const config = {
  matcher: ['/((?!api/auth|_next/static|favicon.ico).*)']
};
localhost:3000 — NewsWave
What just happened?
Middleware configuration determines which routes need protection. Public routes like article pages remain open, while editor and admin routes require specific roles. The matcher patterns use wildcards to protect entire route sections efficiently.

Resource-Level Authorization

Resource-level authorization controls access to specific items, not just pages or features. A writer might have permission to edit articles, but only articles they created. This granular control prevents users from modifying other people's content. GitHub demonstrates this perfectly — you can edit files in repositories you own or have write access to, but not in other people's private repos. The system checks both your general permissions and your relationship to each specific resource. NewsWave needs resource-level controls for article management. Writers should edit their own drafts but not published articles by other authors. Editors can modify any article, but regular subscribers can't edit anything.
// Check if user can access specific resource
function canAccessResource(user, resource, action) {
  // Check general permission first
  if (!hasPermission(user, `${action}:articles`)) {
    return false; // User doesn't have base permission
  }
  
  // Check ownership for certain actions
  if (action === 'edit' && user.role === 'writer') {
    return user.id === resource.authorId; // Writers can only edit own articles
  }
  
  return true; // Editors and admins can access any resource
}
Terminal
$ touch lib/auth/resource-permissions.js
Creating resource-level authorization...
✓ Resource access control ready
Database queries should include authorization filters. Instead of fetching all articles then filtering in code, add WHERE clauses that limit results based on user permissions. This prevents sensitive data from leaving the database.
// Database query with authorization built in
async function getArticlesForUser(user) {
  if (user.role === 'editor' || user.role === 'admin') {
    // Editors see all articles
    return await db.articles.findMany();
  }
  
  if (user.role === 'writer') {
    // Writers only see their own articles
    return await db.articles.findMany({
      where: { authorId: user.id } // Filter by ownership
    });
  }
  
  // Subscribers see only published articles
  return await db.articles.findMany({
    where: { status: 'published' } // Filter by publication status
  });
}
localhost:3000 — NewsWave
What just happened?
Each user sees different articles based on their role and ownership. Editors see everything, writers only see their own content, and subscribers see only published articles. The authorization happens at the data level, not just in the UI.

Authorization Context and Hooks

React Context provides authorization data throughout your component tree without prop drilling. Create an authorization context that combines user data with permission checking functions. Components can then access authorization state anywhere in the app. Custom hooks wrap authorization logic in reusable functions. Instead of duplicating permission checks across components, create hooks like usePermission or useCanEdit. This centralizes authorization logic and makes components cleaner. The NewsWave frontend needs consistent authorization throughout the interface. Navigation menus, article lists, and action buttons all need to check user permissions. Context and hooks provide this seamlessly.
// Authorization context for the entire app
import { createContext, useContext } from 'react';

const AuthContext = createContext();

export function AuthProvider({ children, user }) {
  // Permission checking function available everywhere
  const hasPermission = (permission) => {
    const role = ROLES[user?.role];
    return role?.permissions.includes(permission) || false;
  };
  
  return (
    <AuthContext.Provider value={{ user, hasPermission }}>
      {children}
    </AuthContext.Provider>
  );
}
Terminal
$ mkdir context && touch context/AuthContext.js
Setting up authorization context...
✓ Global authorization state ready
Custom hooks make authorization checks simple and consistent. Components call the hook instead of accessing context directly, making the code more readable and maintainable.
// Custom hooks for common authorization patterns
export function useAuth() {
  const context = useContext(AuthContext);
  if (!context) throw new Error('useAuth must be used within AuthProvider');
  return context; // Returns user and hasPermission
}

export function usePermission(permission) {
  const { hasPermission } = useAuth();
  return hasPermission(permission); // Returns true/false
}

export function useCanEdit(resource) {
  const { user } = useAuth();
  return canAccessResource(user, resource, 'edit'); // Returns true/false
}
localhost:3000 — NewsWave
What just happened?
The authorization context provides permission checking throughout the app. Custom hooks make it easy for components to check specific permissions. Writers only see basic actions, while editors get additional publish and edit buttons. Switch between roles to see the difference.
Security Best Practices
Never rely on frontend authorization alone — it can be bypassed. Always validate permissions on the server. Use HTTPS for all authentication tokens. Implement rate limiting on sensitive endpoints. Log authorization failures for security monitoring. Store minimal user data in frontend sessions.
Authorization builds upon authentication to create secure, role-based access control. Frontend authorization improves user experience by hiding inappropriate controls, while backend authorization provides real security through API route protection and database filtering. Middleware handles route-level access, and React Context makes authorization state available throughout your app. Remember that authorization is about controlling what authenticated users can do, not who they are — that's authentication's job.

Quiz

1. Why is backend authorization more secure than frontend authorization in NewsWave?


2. What should a NewsWave API route do after verifying user authentication?


3. How does Next.js middleware help protect NewsWave's admin routes?


Up Next: Environment Variables

Secure your NewsWave app configuration with environment variables for API keys, database URLs, and deployment settings across development and production environments.