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) {
}
}