Implementing Secure Route Middleware with Cookies in Next.js and React.js

blog thumbnail
nextreact
04 November 2024
In any web application, ensuring secure access to certain pages is essential. With Next.js, middleware provides a powerful way to handle authentication and route protection, especially when combined with cookies for token-based authentication. In this guide, we’ll explore a simple middleware setup to secure routes based on user login status, allowing for protected paths and managing redirections effectively.

Understanding the Middleware Logic in Neaxt.js

This code snippet defines a Next.js middleware that: Redirects logged-in users who try to access the /login page to /dashboard. Redirects unauthenticated users attempting to access /dashboard back to the home page (/). This setup helps create a smooth and secure user experience, ensuring that authenticated users don’t see the login page and that only logged-in users can access the dashboard.
Code
1import { NextResponse, NextRequest } from 'next/server'; 2 3export function middleware(request: NextRequest) { 4 const path = request.nextUrl.pathname; 5 const isPublicPath = path === '/login'; 6 7 const token = request.cookies.get('token')?.value || ''; 8 console.log(isPublicPath); 9 10 // Redirect logged-in users away from the login page 11 if (isPublicPath && token) { 12 return NextResponse.redirect(new URL('/dashboard', request.nextUrl)); 13 } 14 15 // Redirect unauthenticated users trying to access the dashboard 16 if (!isPublicPath && !token) { 17 return NextResponse.redirect(new URL('/', request.nextUrl)); 18 } 19} 20 21export const config = { 22 matcher: [ 23 '/dashboard', // Protected route 24 '/login', // Public route 25 ], 26};

Managing User Authentication in React.js with Redux Toolkit and Cookies

In modern web applications, user authentication is a critical component. Using Redux Toolkit along with cookies for token storage allows us to create a robust authentication system in our React.js applications. In this blog, we will explore how to set up a Redux slice to manage authentication state, along with methods to check authentication status and log users out securely.

1. Creating the Redux Slice

Create a new file called authSlice.js in the src/features directory. This file will contain our authentication logic using Redux Toolkit.
Code
1// src/features/authSlice.js 2import { createSlice } from '@reduxjs/toolkit'; 3import Cookies from 'js-cookie'; 4 5const initialState = { 6 isAuth: false, 7 authToken: null, 8}; 9 10export const authSlice = createSlice({ 11 name: 'auth', 12 initialState, 13 reducers: { 14 setAuth: (state, action) => { 15 state.isAuth = action.payload; 16 }, 17 checkAuthStatus(state) { 18 const cookie = Cookies.get('authToken'); 19 const token = JSON.parse(!!cookie); 20 state.isAuth = !!token?.jwt; 21 state.authToken = token?.jwt || null; 22 }, 23 logout(state) { 24 state.isAuth = false; 25 state.authToken = null; 26 Cookies.remove('authToken'); 27 }, 28 }, 29}); 30 31export const { setAuth, checkAuthStatus, logout } = authSlice.actions; 32 33export default authSlice.reducer;

2. Configuring the Redux Store

Next, set up the Redux store to include the authentication slice. Create a file named store.js in the src directory:
Code
1// src/store.js 2import { configureStore } from '@reduxjs/toolkit'; 3import authReducer from './features/authSlice'; 4 5const store = configureStore({ 6 reducer: { 7 auth: authReducer, 8 }, 9}); 10 11export default store;

3. Integrating Redux with the React Application

Now, wrap your application with the Redux Provider to make the store available throughout your component tree. Modify src/index.js:
Code
1// src/index.js 2import React from 'react'; 3import ReactDOM from 'react-dom'; 4import { Provider } from 'react-redux'; 5import store from './store'; 6import App from './App'; 7 8ReactDOM.render( 9 <Provider store={store}> 10 <App /> 11 </Provider>, 12 document.getElementById('root') 13);

4. Implementing Authentication Logic in Components

Now let’s create a simple login component that uses the Redux actions to manage authentication. Create a Login.js component in the src/components directory:
Code
1// src/components/Login.js 2import React, { useState } from 'react'; 3import { useDispatch } from 'react-redux'; 4import { setAuth } from '../features/authSlice'; 5import Cookies from 'js-cookie'; 6 7const Login = () => { 8 const dispatch = useDispatch(); 9 const [email, setEmail] = useState(''); 10 const [password, setPassword] = useState(''); 11 12 const handleLogin = (e) => { 13 e.preventDefault(); 14 15 // Simulate API call 16 if (email === 'user@example.com' && password === 'password') { 17 const token = JSON.stringify({ jwt: 'secureTokenExample' }); 18 Cookies.set('authToken', token, { path: '/' }); // Store token in cookie 19 dispatch(setAuth(true)); // Update Redux state 20 } else { 21 alert('Invalid credentials'); 22 } 23 }; 24 25 return ( 26 <form onSubmit={handleLogin}> 27 <input 28 type="email" 29 value={email} 30 onChange={(e) => setEmail(e.target.value)} 31 placeholder="Email" 32 required 33 /> 34 <input 35 type="password" 36 value={password} 37 onChange={(e) => setPassword(e.target.value)} 38 placeholder="Password" 39 required 40 /> 41 <button type="submit">Login</button> 42 </form> 43 ); 44}; 45 46export default Login;

5. Creating a Dashboard Component

Next, create a protected Dashboard.js component that only authenticated users can access:
Code
1// src/components/Dashboard.js 2import React from 'react'; 3import { useDispatch, useSelector } from 'react-redux'; 4import { logout } from '../features/authSlice'; 5 6const Dashboard = () => { 7 const dispatch = useDispatch(); 8 const isAuthenticated = useSelector((state) => state.auth.isAuth); 9 10 const handleLogout = () => { 11 dispatch(logout()); // Logout action 12 }; 13 14 return ( 15 <div> 16 <h1>Welcome to the Dashboard!</h1> 17 {isAuthenticated && ( 18 <button onClick={handleLogout}>Logout</button> 19 )} 20 </div> 21 ); 22}; 23 24export default Dashboard;

6. Update Your App.js Component

Now, you need to integrate the ProtectedRoute component in your existing App.js. Your implementation is almost complete; you just need to ensure the routes are set up correctly. Here's the updated App.js:
Code
1import Header from '../components/header/Header' 2import Footer from '../components/footer/Footer' 3import HomePage from './homePage/HomePage' 4import CategoryName from './categoryName/CategoryName' 5import ProductDetail from './productDetail/ProductDetail' 6import CartPage from './cartPage/CartPage' 7import { Route, Routes, BrowserRouter } from 'react-router-dom' 8import ProtectedRoute from '../components/ProtectedRoute' 9function App() { 10 return ( 11 <BrowserRouter> 12 <Header /> 13 <Routes> 14 <Route path='/' element={<HomePage />} /> 15 <Route path='/:categoryName' element={<CategoryName /> } /> 16 <Route path='/:product/:id' element={<ProductDetail /> } /> 17 <Route 18 path="/cart" 19 element={<ProtectedRoute element={<CartPage />} />} 20 /> 21 </Routes> 22 <Footer /> 23 </BrowserRouter> 24 ) 25} 26 27export default App

7. Create the ProtectedRoute Component

Create a new file named ProtectedRoute.js in the components directory. This component will handle the authentication check.
Code
1import React from 'react'; 2import { Navigate, useLocation } from "react-router-dom"; 3import { useSelector } from 'react-redux'; 4 5const ProtectedRoute = ({ element }) => { 6 const isAuth = useSelector((state) => state.auth.isAuth); 7 let location = useLocation(); 8 9 return isAuth ? element : <Navigate to="/" state={{ from: location }} replace />; 10}; 11 12export default ProtectedRoute; 13