- Git clone this project
- Switch to the authorization branch
git checkout authorization
- Run
npm install
- Setup your .env file in both
server
andclient
folder - Run
npm run db:migrate
- Run
npm run dev:server
The objective of this workshop is to set up a refresh token system to keep the connection active when refreshing your react application
We must set up the cookie-parser middleware to allow us to read the cookies sent to the server
- Install
cookie-parser
package in theserver
folder - In your
config.js
uncomment this line :
const cookieParser = require("cookie-parser");
app.use(cookieParser());
to have access to our authorization header from our client we must modify the cors configuration to allow reading
- Update cors configuration in your
config.js
with the following :
const cors = require("cors");
app.use(
cors({
exposedHeaders: ["Authorization"],
origin: [
process.env.CLIENT_URL, // keep this one, after checking the value in `server/.env`
],
credentials: true,
})
);
We added here the exposedHeaders
option to allow our Authorization header to be read from the client and the credentials
option that allow us to send/retrieve cookie to/from the client.
- First, on your login controller Generate a refresh token with one day of validity :
const refreshToken = jwt.sign(
{ id: user.id, role: user.role },
process.env.APP_SECRET,
{
expiresIn: "1d",
}
);
- Then, send the refresh token as a cookie to the client :
res
.cookie("refreshToken", refreshToken, {
httpOnly: true,
sameSite: "Lax",
})
.header("Authorization", token)
.json(user);
The httpOnly
option to restrict the read of the cookie from the client
{:.alert-info}
The sameSite
option allow us to send cookie to a client from another domain
{:.alert-info}
- Create a refresh method in your
authAction
:
const refresh = async (req, res, next) => {
try {
} catch (error) {
return next(error);
}
};
module.exports = { register, login, refresh, logout };
- In your refresh method, first get the
refreshToken
from the cookie :
const { refreshToken } = req.cookies;
if (!refreshToken) {
return res.status(401).send("Access Denied. No refresh token provided.");
}
- Verify the validity of the refreshToken and generate a new access token :
const decoded = jwt.verify(refreshToken, process.env.APP_SECRET);
const accessToken = jwt.sign({ id: decoded.id, role: decoded.role }, process.env.APP_SECRET, {
expiresIn: "1h",
});
- Get back the user information using the id stored in the refreshToken :
const [[user]] = await tables.user.read(decoded.id);
delete user.password;
- Finally, send the new generated token to the client :
return res.header("Authorization", accessToken).json(user);
- Link your middleware to a new get route
/refresh
- In your
authAction
add the logout method as following :
const logout = async ({res}) => {
res.clearCookie("refreshToken").sendStatus(200);
}
- Link your middleware to a new get route
/logout
- In your auth context located in
App.jsx
add a new state isLoading :
const [isLoading, setIsLoading] = useState(true);
- Then, add a useEffect hook with a
getAuth
method :
useEffect(() => {
const getAuth = async () => {
try {
} catch (error) {
toast.error("Une erreur est survenue");
setIsLoading(false);
}
}
getAuth();
}, []);
- In your
getAuth
method fetch the refresh routes :
const response = await fetch(
`${import.meta.env.VITE_API_URL}/auth/refresh`,
{ credentials: "include" }
);
Don't forget to add the credentials option to allow the client to send the cookie {:.alert-info}
- Then, if the response is okay, get the result (token, user) and set your authContext states :
const token = response.headers.get("Authorization");
const user = await response.json();
setAuth({ isLogged: true, user, token });
- Whatever if the response is ok, set the loading state to false after the request is done :
setIsLoading(false);
- In your main create the PrivateRoute component as following :
const PrivateRoute = ({children}) => {
const { auth, isLoading } = useOutletContext();
const navigate = useNavigate();
useEffect(() => {
if (!isLoading) {
if (!auth.isLogged) navigate("/login");
}
}, [auth, children, isLoading, navigate])
if (!isLoading && auth.isLogged) return children;
return "...loading";
}
Inspect the given code, if you don't understand it call your trainer ! {:.alert-info}
- Implement your PrivateRoute component to your restricted routes :
{
path: "/",
element: (
<PrivateRoute>
<Home />
</PrivateRoute>
),
},
We were able to set up a refresh token system to keep an active connection even when we refresh our application.
However, this is not perfect, we still need to deal with the case where the token expires due to its validity period to dynamically call the refresh route to retrieve a new token in a transparent way for the user.
Yes, as developers we must think about all the use cases and implement increasingly complex systems to find a balance between security and user experience.
You're not done learning, this is just the beginning!