Implementation of Secure Route in ReactJS

Approach using React Router v6 along with some custom hooks for handling authentication

  1. Create a PrivateRoute component: This will be a higher-order component (HOC) that checks if a user is authenticated.
  2. Admin-specific checks: Extend the PrivateRoute for admin routes to check if the user has admin privileges.
  3. Mock Authentication Context: For simplicity, use a mock authentication context. In a real app, you’d replace this with actual authentication logic.

You will need a context that holds the user’s authentication state.

// AuthContext.js
import React, { createContext, useContext, useState } from 'react';

const AuthContext = createContext(null);

export const AuthProvider = ({ children }) => {
	const [user, setUser] = useState(null);

	const login = (userData) => setUser(userData);
	const logout = () => setUser(null);

	const isAuthenticated = !!user;

	return (
		<AuthContext.Provider value={{ user, login, logout, isAuthenticated }}>
			{children}
		</AuthContext.Provider>
	);
};

export const useAuth = () => useContext(AuthContext);
// PrivateRoute.js
import { Navigate } from 'react-router-dom';
import { useAuth } from './AuthContext';

export const PrivateRoute = ({ children }) => {
	const { isAuthenticated } = useAuth();

	return isAuthenticated ? children : <Navigate to="/login" />;
};

export const AdminRoute = ({ children }) => {
	const { user } = useAuth();

	return user?.role === 'admin' ? children : <Navigate to="/login" />;
};
import { Route, Routes } from 'react-router-dom';
import Footer from './components/Footer';
import NavBar from './components/NavBar';
import * as Pages from './pages/_index';
import * as Admin from './admin/_index';
import { useTranslation } from 'react-i18next';
import { useEffect } from 'react';
import { PrivateRoute, AdminRoute } from './PrivateRoute';
import { AuthProvider } from './AuthContext';

export default function App() {
	const { i18n } = useTranslation();
	useEffect(() => {
		let lang;

		if (localStorage.i18nextLng !== undefined) {
			lang = localStorage.i18nextLng;
			console.log(`Sprache '${lang}' von localStorage`);
		} else if (document.location.host.endsWith('.com')) {
			lang = 'en';
			console.log(`Sprache '${lang}' von document.location.host`);
		} else if (navigator.language !== undefined) {
			lang = navigator.language.substring(0, 2);
			console.log(`Sprache '${lang}' von navigator.language`);
		} else {
			i18n.changeLanguage('en');
			console.log(`Default Sprache '${lang}'`);
		}

		localStorage.i18nextLng = lang;
		i18n.changeLanguage(lang);
	}, []);

	return (
		<AuthProvider>
			<NavBar />
			<Routes>
				<Route path='/' element={<Pages.Home />} />
				<Route path='services' element={<Pages.Services />} />
				<Route path='pricing' element={<Pages.Pricing />} />
				<Route path='about' element={<Pages.About />} />
				<Route path='login' element={<Pages.Login />} />
				<Route path='register' element={<Pages.Register />} />
				<Route path='contact' element={<Pages.Contact />} />
				<Route path='policy' element={<Pages.Policy />} />
				<Route path='*' element={<Pages.NotFound />} />

				{/* Private Route for Authenticated Users */}
				<Route
					path='dashboard'
					element={
						<PrivateRoute>
							<Admin.Dashboard />
						</PrivateRoute>
					}
				/>

				{/* Admin-only Route */}
				<Route
					path='admin/dashboard'
					element={
						<AdminRoute>
							<Admin.AdminDashboard />
						</AdminRoute>
					}
				/>
			</Routes>
			<Footer />
		</AuthProvider>
	);
}
  1. AuthProvider: This component provides authentication state to the rest of the app. The state includes user information and authentication status.
  2. PrivateRoute: Wraps routes that require authentication. If the user is not authenticated, they are redirected to the login page.
  3. AdminRoute: Wraps routes that require the user to be an admin. If the user does not have the appropriate role, they are redirected to the login page.
  4. Usage in App: Both PrivateRoute and AdminRoute are used to protect specific routes. For example, the dashboard route is protected by PrivateRoute, while the admin/dashboard route is protected by AdminRoute.
// AuthContext.js
import React, { createContext, useContext, useState } from 'react';

const AuthContext = createContext(null);

export const AuthProvider = ({ children }) => {
	const [user, setUser] = useState(null);

	const login = (userData) => setUser(userData);
	const logout = () => setUser(null);

	const isAuthenticated = !!user;

	return (
		<AuthContext.Provider value={{ user, login, logout, isAuthenticated }}>
			{children}
		</AuthContext.Provider>
	);
};

export const useAuth = () => useContext(AuthContext);
// PrivateRoute.js
import { Navigate } from 'react-router-dom';
import { useAuth } from './AuthContext';

export const PrivateRoute = ({ children }) => {
	const { isAuthenticated } = useAuth();

	return isAuthenticated ? children : <Navigate to="/login" />;
};

export const AdminRoute = ({ children }) => {
	const { user } = useAuth();

	return user?.role === 'admin' ? children : <Navigate to="/login" />;
};

export const VolunteerRoute = ({ children }) => {
	const { user } = useAuth();

	return user?.role === 'volunteer' ? children : <Navigate to="/login" />;
};

export const OrganizerRoute = ({ children }) => {
	const { user } = useAuth();

	return user?.role === 'organizer' ? children : <Navigate to="/login" />;
};
import { Route, Routes } from 'react-router-dom';
import Footer from './components/Footer';
import NavBar from './components/NavBar';
import * as Pages from './pages/_index';
import * as Admin from './admin/_index';
import * as Volunteer from './volunteer/_index';
import * as Organizer from './organizer/_index';
import { useTranslation } from 'react-i18next';
import { useEffect } from 'react';
import { PrivateRoute, AdminRoute, VolunteerRoute, OrganizerRoute } from './PrivateRoute';
import { AuthProvider } from './AuthContext';

export default function App() {
	const { i18n } = useTranslation();
	useEffect(() => {
		let lang;

		if (localStorage.i18nextLng !== undefined) {
			lang = localStorage.i18nextLng;
			console.log(`Sprache '${lang}' von localStorage`);
		} else if (document.location.host.endsWith('.com')) {
			lang = 'en';
			console.log(`Sprache '${lang}' von document.location.host`);
		} else if (navigator.language !== undefined) {
			lang = navigator.language.substring(0, 2);
			console.log(`Sprache '${lang}' von navigator.language`);
		} else {
			i18n.changeLanguage('en');
			console.log(`Default Sprache '${lang}'`);
		}

		localStorage.i18nextLng = lang;
		i18n.changeLanguage(lang);
	}, []);

	return (
		<AuthProvider>
			<NavBar />
			<Routes>
				<Route path='/' element={<Pages.Home />} />
				<Route path='services' element={<Pages.Services />} />
				<Route path='pricing' element={<Pages.Pricing />} />
				<Route path='about' element={<Pages.About />} />
				<Route path='login' element={<Pages.Login />} />
				<Route path='register' element={<Pages.Register />} />
				<Route path='contact' element={<Pages.Contact />} />
				<Route path='policy' element={<Pages.Policy />} />
				<Route path='*' element={<Pages.NotFound />} />

				{/* Private Route for Authenticated Users */}
				<Route
					path='dashboard'
					element={
						<PrivateRoute>
							<Pages.Dashboard />
						</PrivateRoute>
					}
				/>

				{/* Admin-only Route */}
				<Route
					path='admin/dashboard'
					element={
						<AdminRoute>
							<Admin.AdminDashboard />
						</AdminRoute>
					}
				/>

				{/* Volunteer-only Route */}
				<Route
					path='volunteer/dashboard'
					element={
						<VolunteerRoute>
							<Volunteer.VolunteerDashboard />
						</VolunteerRoute>
					}
				/>

				{/* Organizer-only Route */}
				<Route
					path='organizer/dashboard'
					element={
						<OrganizerRoute>
							<Organizer.OrganizerDashboard />
						</OrganizerRoute>
					}
				/>
			</Routes>
			<Footer />
		</AuthProvider>
	);
}

  1. AuthProvider: The context provider now allows managing the user’s role. You can add a role field to the user object when a user logs in.
  2. Role-based Private Routes: Each role (admin, volunteer, and organizer) has a separate route guard (AdminRoute, VolunteerRoute, OrganizerRoute) that checks if the user has the required role.
  3. Secure Routes: In the App component, you wrap the routes that are specific to admin, volunteer, and organizer roles with their corresponding role-based route guards.
  4. Fallback to Login: If a user tries to access a protected route without the correct role, they will be redirected to the login page.

When logging in a user, you can set the user object with the required role. For example:

// Example login function in AuthContext.js
const login = (userData) => setUser({ ...userData, role: 'volunteer' });

This ensures that each user has a role, and your route guards will check against that role to allow or deny access to specific routes.

  1. Authorization Logic: In a real application, you would fetch the user’s role and permissions from a server or use a library like Firebase, Auth0, or similar.
  2. Session Management: Make sure to handle user sessions properly and securely, including token management if you are using JWT for authentication.