Logo
Published on

Route Guards

How React Router Route Guards Control Access

React Router Route Guards protect routes by controlling access based on authentication, authorization, and user permissions. They enable secure navigation flows and prevent unauthorized access to sensitive pages. Teams implementing proper route guards report better security and user experience.

TL;DR

  • Use higher-order components or custom hooks to protect routes
  • Implement authentication checks before rendering components
  • Redirect unauthorized users to login or error pages
  • Perfect for admin panels, user dashboards, and protected content
const result = process(data)

The Access Control Challenge

Your application exposes sensitive pages to unauthorized users, creating security vulnerabilities. Authentication checks are scattered across components, making it difficult to maintain consistent access control. Users can manually navigate to protected URLs and access restricted content.

// Problematic: No access control on sensitive routes
const React = require('react')
function AdminPanel() {
  const adminData = {
    users: 1500,
    revenue: 45000,
    settings: ['payment', 'security', 'database'],
  }
  console.log('Admin panel accessed - no authentication check!')
  console.log('Sensitive data exposed:', Object.keys(adminData))
  return {
    message: 'Admin Dashboard',
    data: adminData,
    accessible: true, // Anyone can access!
  }
}

React Router Route Guards provide secure, declarative access control with proper authentication:

// React Router: Protected routes with authentication guards
const React = require('react')
const { Navigate } = require('react-router-dom')
function ProtectedRoute({ children, requiredRole = 'user' }) {
  const user = getCurrentUser()
  const hasAccess = user && user.roles.includes(requiredRole)

  if (!user) {
    console.log('Redirecting to login - no authentication')
    return React.createElement(Navigate, { to: '/login', replace: true })
  }
  if (!hasAccess) {
    return React.createElement(Navigate, { to: '/unauthorized' })
  }
  return children
}

Best Practises

Use route guards when:

  • ✅ Protecting admin panels and sensitive user data
  • ✅ Implementing role-based access control (RBAC)
  • ✅ Securing payment pages and financial transactions
  • ✅ Creating user-specific dashboards and private content

Avoid when:

  • 🚩 All content is public and requires no authentication
  • 🚩 Simple marketing websites with static content
  • 🚩 Server-side authentication handles all security
  • 🚩 Performance is critical and auth checks add latency

System Design Trade-offs

AspectRoute GuardsManual Auth Checks
SecurityExcellent - centralized controlPoor - scattered and inconsistent
User ExperienceGood - clear redirectsPoor - broken navigation flows
MaintainabilityHigh - reusable componentsLow - duplicate auth logic
TestingEasy - isolated guard logicHard - auth mixed with UI
Role ManagementBuilt-in - hierarchical permissionsManual - custom role checking
Development SpeedFast - declarative protectionSlow - repetitive auth code

More Code Examples

❌ Manual authentication
// Manual authentication checks scattered throughout components
const React = require('react')
function UserDashboard() {
  const user = JSON.parse(localStorage.getItem('user') || '{}')
  if (!user.id) {
    console.log('Dashboard: No user found, should redirect')
    window.location.href = '/login' // Hard redirect!
    return null
  }
  console.log('Dashboard: User authenticated')
  return { content: 'User Dashboard', user: user.name }
}

function AdminSettings() {
  const user = JSON.parse(localStorage.getItem('user') || '{}')
  const isAdmin = user.role === 'admin' // Different auth logic!
  if (!user.id || !isAdmin) {
    console.log('Settings: Access denied')
    alert('Access denied!') // Inconsistent UX!
    history.back()
    return null
  }
  return { content: 'Admin Settings' }
}

function PaymentPage() {
  const authToken = localStorage.getItem('authToken')
  const userRole = localStorage.getItem('userRole')
  if (!authToken) {
    // Yet another auth pattern!
    console.log('Payment: Not logged in')
    return { redirect: '/login' }
  }
  return { content: 'Payment Processing' }
}

console.log('Each component implements different auth logic')
console.log('Inconsistent redirects and error handling')
✅ Centralized route guard system
// Centralized route guard system with role-based access
const React = require('react')
const { Navigate, createBrowserRouter } = require('react-router-dom')
function createRouteGuard(allowedRoles = []) {
  return function RouteGuard({ children }) {
    const user = useAuth() // Custom hook for auth state
    const hasPermission = user && allowedRoles.some((role) => user.roles.includes(role))

    console.log('Route guard check:', {
      user: user?.name,
      required: allowedRoles,
      granted: hasPermission,
    })

    if (!user) {
      return React.createElement(Navigate, {
        to: '/login',
        state: { from: location.pathname },
      })
    }
    if (!hasPermission) {
      return React.createElement(Navigate, { to: '/unauthorized' })
    }
    return children
  }
}

// Reusable guard components for different access levels
const AuthGuard = createRouteGuard(['user', 'admin'])
const AdminGuard = createRouteGuard(['admin'])
const PaymentGuard = createRouteGuard(['user', 'premium'])

const protectedRoutes = [
  { path: '/dashboard', element: wrapWithGuard(AuthGuard, 'UserDashboard') },
  { path: '/admin', element: wrapWithGuard(AdminGuard, 'AdminSettings') },
  { path: '/payment', element: wrapWithGuard(PaymentGuard, 'PaymentPage') },
]

function wrapWithGuard(Guard, Component) {
  return React.createElement(Guard, null, Component)
}

const useAuth = () => ({ name: 'John', roles: ['user', 'admin'] })
console.log('Consistent authentication logic across all routes')

Technical Trivia

The Twitter Security Incident of 2020: Twitter discovered that their mobile app's route guards had a critical flaw allowing unauthorized users to access admin endpoints. The vulnerability let attackers bypass authentication checks on certain routes, exposing sensitive user data and administrative functions.

Why manual auth checks failed: Different teams implemented authentication differently across the application. Some routes checked JWT tokens, others used session cookies, and a few had no protection at all. The inconsistency created gaps that attackers exploited through direct URL manipulation.

Centralized route guards solved the issue: Implementing a unified route guard system with consistent authentication logic eliminated the vulnerability. All protected routes now use the same authentication flow, with proper redirects and error handling that can't be bypassed through client-side manipulation.


Master Route Guards: Implementation Strategy

Create reusable guard components that wrap your protected routes and handle authentication consistently. Use higher-order components or custom hooks to centralize auth logic. Always redirect to appropriate pages (login, unauthorized) rather than showing error messages. Consider implementing route-level permissions that can be easily updated as your application's security requirements evolve.