Protecting routes
~19 minSecuring routes protects sensitive data and keeps parts of the application to signed-in users. Pick the router your application uses, or build your own guard from the SDK primitives.
Set up route protection
The official `@thunderid/react-router` package provides a `ProtectedRoute` component that guards a route and sends unauthenticated visitors to sign-in.
Installation
npm install @thunderid/react-router
Basic Setup with ProtectedRoute
function App() {
return (
<ThunderIDProvider
baseUrl="https://localhost:8090"
clientId="your-client-id"
>
<BrowserRouter>
<Routes>
<Route path="/" element={<div>Public Home Page</div>} />
<Route path="/signin" element={<SignIn />} />
<Route
path="/dashboard"
element={
<ProtectedRoute redirectTo="/signin">
<Dashboard />
</ProtectedRoute>
}
/>
<Route
path="/profile"
element={
<ProtectedRoute redirectTo="/signin">
<Profile />
</ProtectedRoute>
}
/>
</Routes>
</BrowserRouter>
</ThunderIDProvider>
)
}
export default AppCustom Fallback and Loading States
You can customize the behavior of `ProtectedRoute` with custom fallback components and loading states:
// Redirect to custom login page
<Route
path="/dashboard"
element={
<ProtectedRoute redirectTo="/login">
<Dashboard />
</ProtectedRoute>
}
/>
// Custom fallback component
<Route
path="/dashboard"
element={
<ProtectedRoute
fallback={
<div className="auth-required">
<h2>Please sign in</h2>
<p>You need to be signed in to access this page.</p>
</div>
}
>
<Dashboard />
</ProtectedRoute>
}
/>
// Custom loading state
<Route
path="/dashboard"
element={
<ProtectedRoute redirectTo="/signin" loader={<div className="spinner">Loading...</div>}>
<Dashboard />
</ProtectedRoute>
}
/>Integration with Layouts
Protect multiple routes using a shared layout:
function App() {
return (
<BrowserRouter>
<Routes>
{/* Public routes */}
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/signin" element={<SignIn />} />
{/* Protected routes with layout */}
<Route path="/app" element={<AppLayout />}>
<Route
path="dashboard"
element={
<ProtectedRoute redirectTo="/signin">
<Dashboard />
</ProtectedRoute>
}
/>
<Route
path="profile"
element={
<ProtectedRoute redirectTo="/signin">
<Profile />
</ProtectedRoute>
}
/>
<Route
path="settings"
element={
<ProtectedRoute redirectTo="/signin">
<Settings />
</ProtectedRoute>
}
/>
</Route>
</Routes>
</BrowserRouter>
)
}Installation
npm install @thunderid/tanstack-router
Basic Setup with ProtectedRoute
const rootRoute = createRootRoute({
component: () => <div>Root Layout</div>,
})
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
component: () => <div>Public Home Page</div>,
})
const signinRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/signin',
component: SignIn,
})
const dashboardRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/dashboard',
component: () => (
<ProtectedRoute redirectTo="/signin">
<Dashboard />
</ProtectedRoute>
),
})
const profileRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/profile',
component: () => (
<ProtectedRoute redirectTo="/signin">
<Profile />
</ProtectedRoute>
),
})
const routeTree = rootRoute.addChildren([indexRoute, signinRoute, dashboardRoute, profileRoute])
const router = createRouter({ routeTree })
function App() {
return (
<ThunderIDProvider
baseUrl="https://localhost:8090"
clientId="your-client-id"
>
<RouterProvider router={router} />
</ThunderIDProvider>
)
}
export default AppCustom Fallback and Loading States
// Redirect to custom login page
const dashboardRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/dashboard',
component: () => (
<ProtectedRoute redirectTo="/login">
<Dashboard />
</ProtectedRoute>
),
})
// Custom fallback component
const dashboardRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/dashboard',
component: () => (
<ProtectedRoute
fallback={
<div className="auth-required">
<h2>Please sign in</h2>
<p>You need to be signed in to access this page.</p>
</div>
}
>
<Dashboard />
</ProtectedRoute>
),
})
// Custom loading state
const dashboardRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/dashboard',
component: () => (
<ProtectedRoute redirectTo="/signin" loader={<div className="spinner">Loading...</div>}>
<Dashboard />
</ProtectedRoute>
),
})Integration with Layouts
const rootRoute = createRootRoute({
component: () => <div>Root Layout</div>,
})
// Public routes
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
component: Home,
})
const aboutRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/about',
component: About,
})
const signinRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/signin',
component: SignIn,
})
// Protected routes with layout
const appLayoutRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/app',
component: AppLayout,
})
const appDashboardRoute = createRoute({
getParentRoute: () => appLayoutRoute,
path: '/dashboard',
component: () => (
<ProtectedRoute redirectTo="/signin">
<Dashboard />
</ProtectedRoute>
),
})
const appProfileRoute = createRoute({
getParentRoute: () => appLayoutRoute,
path: '/profile',
component: () => (
<ProtectedRoute redirectTo="/signin">
<Profile />
</ProtectedRoute>
),
})
const appSettingsRoute = createRoute({
getParentRoute: () => appLayoutRoute,
path: '/settings',
component: () => (
<ProtectedRoute redirectTo="/signin">
<Settings />
</ProtectedRoute>
),
})
const routeTree = rootRoute.addChildren([
indexRoute,
aboutRoute,
signinRoute,
appLayoutRoute.addChildren([appDashboardRoute, appProfileRoute, appSettingsRoute]),
])
const router = createRouter({ routeTree })
function App() {
return (
<ThunderIDProvider
baseUrl="https://localhost:8090"
clientId="your-client-id"
>
<RouterProvider router={router} />
</ThunderIDProvider>
)
}
export default AppBasic Custom Route Guard
const ProtectedRoute = ({ children }: { children: ReactNode }) => {
const { isSignedIn } = useThunderID()
if (!isSignedIn) {
return <Unauthenticated />
}
return children
}
const App = () => {
return (
<Routes>
<Route
path="/contact"
element={
<ProtectedRoute>
<Contact />
</ProtectedRoute>
}
/>
<Route path="/" element={<Home />} />
<Route path="*" element={<PageNotFound />} />
</Routes>
)
}
export default AppAdvanced Custom Implementation
You can extend the basic pattern to include loading states, redirects, and custom logic:
interface ProtectedRouteProps {
children: ReactNode
redirectTo?: string
fallback?: ReactNode
requireRole?: string
}
export const ProtectedRoute = ({
children,
redirectTo = '/signin',
fallback,
requireRole,
}: ProtectedRouteProps) => {
const { isSignedIn, user, loading } = useThunderID()
// Show loading state while authentication is being checked
if (loading) {
return <div>Loading...</div>
}
// Redirect to sign-in if not authenticated
if (!isSignedIn) {
if (fallback) {
return <>{fallback}</>
}
return <Navigate to={redirectTo} replace />
}
// Check for required role if specified
if (requireRole && user?.roles && !user.roles.includes(requireRole)) {
return <div>You don't have permission to access this page</div>
}
return <>{children}</>
}