System Designmedium

Implement role-based route guards on the client

Design a React route-guard pattern for an admin console with multiple roles (admin, analyst, viewer). Cover the security caveats — what the client guard actually buys you and what it does not.

Asked at CrowdStrike, Stripe

#react#auth#rbac#security

Answer

// First, the honest part:
// Client-side guards are a UX layer, NOT a security boundary.
// Every protected action MUST be re-checked on the server.
// The client guard exists to:
//  1) Hide controls a user cannot use (less confusing).
//  2) Avoid wasted requests that will 403 anyway.
//  3) Redirect unauth'd users to login before they hit a broken UI.

import { Navigate, Outlet, useLocation } from 'react-router-dom'

type Role = 'admin' | 'analyst' | 'viewer'

interface Session {
  userId: string
  roles: Role[]
}

const SessionCtx = createContext<Session | null>(null)
export const useSession = () => useContext(SessionCtx)

export function hasAny(session: Session | null, allowed: Role[]): boolean {
  if (!session) return false
  return session.roles.some((r) => allowed.includes(r))
}

export function RequireRole({ allowed }: { allowed: Role[] }) {
  const session = useSession()
  const location = useLocation()

  if (!session) {
    return <Navigate to="/login" replace state={{ from: location }} />
  }
  if (!hasAny(session, allowed)) {
    return <Navigate to="/403" replace />
  }
  return <Outlet />
}

// Usage with React Router
<Routes>
  <Route element={<RequireRole allowed={['admin', 'analyst', 'viewer']} />}>
    <Route path="/dashboard" element={<Dashboard />} />
  </Route>
  <Route element={<RequireRole allowed={['admin', 'analyst']} />}>
    <Route path="/incidents" element={<Incidents />} />
  </Route>
  <Route element={<RequireRole allowed={['admin']} />}>
    <Route path="/admin/users" element={<UserAdmin />} />
  </Route>
</Routes>

// Per-control gating
function DangerButton() {
  const session = useSession()
  if (!hasAny(session, ['admin'])) return null
  return <button onClick={deleteEverything}>Delete</button>
}

// Pitfalls to mention in the interview:
// - Don't ship admin-only code splits to non-admins (use route-level lazy loading
//   so the JS bundle isn't even fetched).
// - Refresh the session on focus/visibility — roles can change.
// - Treat `session` as untrusted input; never let it grant access to data the
//   server wouldn't return anyway.
// - For deep-link sharing, distinguish "not logged in" vs "logged in but forbidden"
//   so users get the right next action.

Practise more System Design questions →