Home > Back-end >  How do I protect routes on the server side from this RequireAuth component using JWT
How do I protect routes on the server side from this RequireAuth component using JWT

Time:12-16

I have read that authorisation should be completed by the server side, not just the client side. But I am having trouble seeing what this looks like. Here is the code I have so far.

App.js

This shows that before a page is rendered, it goes through the RequireAuth component.

import { useContext} from 'react'
import './style.scss'
import Register from './pages/Register/Register'
import TeacherLogin from './pages/Login/TeacherLogin/TeacherLogin'
import StudentLogin from './pages/Login/StudentLogin/StudentLogin'
import ForgotPassword from './pages/ForgotPassword/ForgotPassword'
import PageNotFound from './pages/PageNotFound/PageNotFound'
import Home from './pages/Home/Home'
import AccountType from './pages/AccountType/AccountType'
import Users from './pages/Students/Students'
import SOTW from './pages/SOTW/SOTW'
import Landing from './pages/Landing/Landing'
import Add from './pages/Add/Add'
import Test from './pages/Test/Test'
import Edit from './pages/Edit/Edit'
import ClassStats from './pages/ClassStats/ClassStats'
import {
  BrowserRouter,
  Routes,
  Route,
} from "react-router-dom";
import { DarkModeContext } from './Hooks/context/darkModeContext'
import Profile from './StudentPages/Profile/Profile'
import RequireAuth from './components/RequireAuth/RequireAuth'

function App() {

  const {darkMode} = useContext(DarkModeContext)

  return (
    <div className={`theme-${darkMode ? "dark" : "light"}`}>
      <BrowserRouter>
        <Routes>
            <Route path='/' element={<Landing />} />
            <Route path="/register" element={<Register/>}/>
            <Route path="forgot-password" element={<ForgotPassword/>} />
            <Route path="test" element={<Test/>} />
            <Route path="/auth">
              <Route index element={<AccountType/>} />
              <Route path ="teacher" element={<TeacherLogin/>} />
              <Route path="student" element={<StudentLogin/>} />
            </Route>
              <Route path="/auth/teacher" element={<RequireAuth allowedRoles={['teacher']}/>}>
                    <Route path='dashboard' element={<Home/>} />
                    <Route path="SOTW" element={<SOTW/>}/>
                    <Route path="users">
                      <Route index element={<Users />} />
                      <Route path="add" element={<Add title='Add Student' button='Add' />} />
                      <Route path="edit" element={<Edit title='Edit Student' button='Update' />} />
                  </Route>
            </Route>
              <Route path="auth/student" element={<RequireAuth allowedRoles={['student', 'teacher']}/>}>
                  <Route path='profile/:id' element={<Profile/>} />
              </Route>
              <Route path="*" element={<PageNotFound/>}/>
          </Routes> 
      </BrowserRouter>
    </div>
  );
}

export default App;

RequireAuth

This is the component which checks the local storage for a user. This works, but it is not safe.

import { Navigate, useLocation, Outlet } from 'react-router-dom';
import { AuthContext } from '../../Hooks/context/AuthContext';
import { useContext, useEffect, useState } from 'react';
import axios from 'axios';

const RequireAuth = ({ allowedRoles }) => {
  const { testUser, setTestUser, user } = useContext(AuthContext);
  const [statusAuth, setStatusAuth] = useState(false);
  const location = useLocation();

  console.log(user)

  useEffect(() => {
    const fetch = async () => {
      const res = await axios.get('/api/auth/protected')
      console.log(res) // THIS IS WHERE I AM STUCK. WHAT DO I DO HERE?
    }
    fetch()
  },[])
  
  return allowedRoles.includes(user?.role) ? (
    <Outlet />
  ) : user ? (
    <Navigate to="/unauthorised" state={{ from: location }} replace />
  ) : (
    <Navigate to="/" state={{ from: location }} replace />
  );
};

export default RequireAuth;

Backend code using Node.js

This is the verification token code which verify the token and checks if the user is a 'teacher'. However, this is not flexible. For example, if a page can only be seen by a different user that is a 'Student' for example, it wouldn't work because this checks if they are a 'Teacher' role.

import jwt from 'jsonwebtoken'
import { createError } from './error.js'

export const verifyToken = (req, res, next) => {
    const token = req.headers.cookie.split('=')[1]

    if (!token) {
      return next(createError(401, "You are not authenticated!"));
    }
    jwt.verify(token, process.env.JWT, (err, user) => {
        if (err) return next(createError(403, "Token is not valid!"));
        req.user = user;
        next();
      });
  };

export const verifyAdmin = (req, res, next) => {
    verifyToken(req, res, () => {
        if(req.user.role === 'teacher') {
            next()
        } else {
            return next(createError(403, 'You are not authorised'))
        }
    })
}

Routes

import express from 'express'
import {register, login, refreshToken, logout, protectedRoute} from '../Controllers/auth.js'
import { verifyToken, verifyAdmin } from '../verifyToken.js';

const router = express.Router();

//PROTECTED ROUTE
router.get("/protected", verifyAdmin, protectedRoute)

export default router

Controller

import mongoose from 'mongoose'
import Student from '../Models/student.js'
import bcrypt from 'bcrypt'
import { createError } from '../error.js'
import jwt from 'jsonwebtoken'


export const protectedRoute = async (req, res, next) => {
    res.status(200).send('Success')
      }

I have got to the stage of verifying the token. Do I do this on every page request to the backend to establish whether a user can actually view that page?

If so, how do I make my require auth component flexible to redirect based on whether a user is a: student, teacher, admin or anymore?

CodePudding user response:

I feel like I am cheating here, but a simple installation of jwt-decode solved it.

It's implementation was included into the RequireAuth component as below:

import { Navigate, useLocation, Outlet } from 'react-router-dom';
import { AuthContext } from '../../Hooks/context/AuthContext';
import { useContext, useEffect, useState } from 'react';
import axios from 'axios';
import jwtDecode from 'jwt-decode'

const RequireAuth = ({ allowedRoles }) => {
  const { user } = useContext(AuthContext);
  const [statusAuth, setStatusAuth] = useState(false);
  const location = useLocation();

  const decoded = jwtDecode(user)

  return allowedRoles.includes(decoded?.role) ? (
    <Outlet />
  ) : user ? (
    <Navigate to="/unauthorised" state={{ from: location }} replace />
  ) : (
    <Navigate to="/" state={{ from: location }} replace />
  );
};

export default RequireAuth;

However, there is still no backend validation. Does there need to be? If a user was validated on login and a JWT signed and delivered, can they be trusted to visit other routes?

Any guidance would be great.

CodePudding user response:

Here is an alternative answer which I would appreciate an opinion on.

Here I passed AllowedRoles through to the backend via a POST method.

In the backend, I decode the JWT and get the user's role. I then check to see if this is present in the authorised roles.

If so, I send a success message.

On the front end, if the res comes as success, then I set states: isAllowed (true) and isChecking (false).

This appears to work as I intended, but I am not sure it is best practice. Any opinion would be greatly appreciated.

import { Navigate, useLocation, Outlet } from 'react-router-dom';
import { AuthContext } from '../../Hooks/context/AuthContext';
import { useContext, useEffect, useState } from 'react';
import axios from 'axios';
import jwtDecode from 'jwt-decode'

const RequireAuth = ({ allowedRoles }) => {
  const { user  } = useContext(AuthContext);
  const location = useLocation();

  const [allowed, setAllowed] = useState(Boolean)
  const [isChecking, setIsChecking] = useState(true)

  useEffect(()=> {
    console.log('I was called to fetch')
    const fetch = async () => {
      const res = await axios.post('/api/auth/protected', allowedRoles);
      console.log(res)
      if (res.status === 200 ) {
        setAllowed(res.data)
        setIsChecking(false)
        return
      } else {
        setAllowed(res.data)
        setIsChecking(true)
      }
    }
    fetch()
  })

  console.log(allowed)

  if(isChecking) return <p>Checking....</p>

  if(!user) return <Navigate to="/" state={{ from: location }} replace />

  return allowed ? (
    <Outlet/> 
  ) : ( <Navigate to="/" state={{ from: location }} replace />
  )
};

export default RequireAuth;

Backend code

export const protectedRoute = async (req, res, next) => {
    console.log('Hello')
    try {
    const token = req.headers.cookie.split("=")[1]

    if(!token) return res.status(401).json("You are not authenticated")
    const allowedRoles = req.body
    const result = jwt.verify(token, process.env.JWT)
    const status = allowedRoles.includes(result.role)
    console.log(status)
    if (status) {
        res.status(200).send(status)
    }
    
    } catch (err) {

    }
      }
  • Related