introduction
If you are like me and want to handle your website auth on your own you came to the right place.
Every time I want to use authentication in my website I get headache for trying to find the most safe and easy way to handle it.
I love to handle it myself, thats why I dont use things like firebase/auth0.
Recently I have found a way to create authentication with 2 tokens.
It makes the app safe and its pretty easy to handle..
In our case there is 2 jwt tokens, access token and refresh token.
The combination between them is what makes our app safe and protective against XSS/CSRF attacks.
What Is What?
Access Token
When a user logges in, the authorization server issues an access token, which is an artifact that client applications can use to make secure calls to an API server.
It will be valid for short amount to make it secure as we can, when it expires then something called silent refresh happens.
The silent refresh is an api call for the server to get new access token right before it expires in the memory.
Refresh Token
As mentioned, access token valid for short amount of time.
So for complete the cycle of renewing the access token we use the refresh token to get new access token.
The refresh token generated on the server and saved in a HttpOnly cookie.
Because client side Javascript can't read or steal an HttpOnly cookie, this is a little better at mitigating XSS than persisting it as a normal cookie or in localstorage.
This is safe from CSRF attacks, because even though a form submit attack can make a /refresh_token API call, the attacker cannot get the new JWT token value that is returned.
Lets look at the /refresh_token apieeeeeeeeeeeeeeeeeeeeeeeeeeeee
import { PrismaClient } from '@prisma/client' import { verify } from 'jsonwebtoken' import {createAccessToken, sendRefreshToken, createRefreshToken} from '../../functions/auth' import cookie from 'cookie' const prisma = new PrismaClient() export default async function refresh_token(req, res) { if (req.method === 'POST') { if(!req.headers.cookie) return res.send({ok: false,accessToken: ''}) const getToken = cookie.parse(req.headers.cookie) const token = getToken.refreshToken if(!token) return res.send({ok: false,accessToken: ''}) let payload = null try { payload = verify(token, process.env.REFRESH_TOKEN_SECRET) const user = await prisma.user.findUnique({ where: { id: payload.userId }, select: { id: true, firstName: true, secondName: true, email: true } }) if (!user) return res.send({ok: false,accessToken: ''}) sendRefreshToken(res, createRefreshToken(user)); const accessToken = createAccessToken(user) return res.send({ ok: true, accessToken,user }); } catch (e) { console.log(e) return res.send({ok: false,accessToken: ''}) } } else { res.status(500).send() } }
As you see above we get the request with cookie in the header, thats our refresh token cookie. We validate it with JWT Validate function.
We get the user id from the payload because we generated the jwt with the user id inside the payload.Then we fetch the user data from the database (using prisma in our case).
As you can see there is sendRefreshToken function....why?
When we sending back refresh token it renewing the current one means that the expire date is renewing aswell and extending.Thats simply means that as long as user uses our website he will be authorized.
Then we send to the client the relevant data - The access token and the basic user data (to access the main user data more conveniently).
How do we create the refresh token and access token?
export const createAccessToken = (user) => { return sign({ userId: user.id }, process.env.ACCESS_TOKEN_SECRET, { expiresIn: '15m' }); }; export const createRefreshToken = (user) => { return sign( { userId: user.id },process.env.REFRESH_TOKEN_SECRET,{ expiresIn: "7d" } ); }; export const sendRefreshToken = (res,token) => { res.setHeader('Set-Cookie',cookie.serialize('refreshToken',token, { httpOnly: true, maxAge: 60 * 60 * 24 * 7, path: '/' })) };
As you can see the access token expires after 15m and the refresh token is expires after 7 days. The refresh token gets renewd every time the user logges into the website, and the access token renewed with silent refresh.
How do we code the silent refresh?
//_app.js useEffect(() => { //initial funciton refreshToken().then(data => { if(data.ok) { store.setAccessToken(data.accessToken) store.setUser(data.user) } setLoading(false) }) //starts silent refreshes countdown setInterval(() => { refreshToken().then(data => { if(data.ok) { store.setAccessToken(data.accessToken) store.setUser(data.user) } }) },600000) },[])
On website load it runs the initial refresh token function (api call for /refresh_token, we send the refresh token as bearer token in the header request), and then the countdown begins.
Every 10 minutes it makes the same call to get the access token from the server and saves it in the client memory.
That way we get new access token and save it in the memory right before the old token expires.
Server Middleware
import { verify } from 'jsonwebtoken' const checkAuth = (handler) => { return async (req, res) => { try { const authorization = req.headers["authorization"] if (!authorization) throw new Error("not authenticated") const token = authorization.split(" ")[1] verify(token, process.env.ACCESS_TOKEN_SECRET); return handler(req, res) } catch (e) { console.log(e) res.status(401).send() } } } export default checkAuth
In the code above, we have the server middleware. Before accessing the api route we are validating the access token with the verify function.
How do we use it in the route?
import checkAuth from './middleware/checkAuthServer' const protectedRoute = async (req, res) => { if(req.method === 'GET') { console.log('got it') //secret data res.send('Hey, keep it in secret!') } } export default checkAuth(protectedRoute)
Now, when the user wants to access the protected route, he needs to pass access token that gets validated in our middleware.
Client Middleware
In some cases on the client, there will be 'protected' pages that only authenticated users can access. In that case we would want to use client middleware on the page.
import { useStore } from "../store"; import {useRouter} from 'next/router' const withAuth = Component => { const Auth = (props) => { const store = useStore() const router = useRouter() if(store.accessToken !== null) { return ( ); } else { router.replace("/"); return null; } }; return Auth; }; export default withAuth;
We are checking if there is access token in the memory, if it's valid then we pass the page component.
Lets look in our protected page
import { useStore } from '../store' import {useEffect, useState} from 'react' import useSWR from 'swr' //the middleware import checkAuthClient from '../functions/checkAuthClient' import axios from 'axios' function Protected() { const store = useStore() const [secret, setSecret] = useState(null) const [isError, setError] = useState(null) const [loading, setLoading] = useState(true) const fetcher = async () => { return await axios.get('/api/protectedRoute', { headers: { authorization: `Bearer ${store.accessToken}` } }) } const { data, error } = useSWR('/api/', fetcher) useEffect(() => { if(data) setSecret(data.data) if (error) setError(error) setLoading(false) },[data,error]) if(loading) { return (Loading...) } else { if(isError) { return ( YO! YOU ARE NOT AUTHENTICATED,GET AWAY FROM HERE!!! ) } else { return ( Welcome to protected Page, {secret} ) } } } export default checkAuthClient(Protected)
As you see there is double check,the first check is for the client page, and the second check is on the server (sending access token in the our request).
Lets Wrap The Registration Process
As you see in the diagram above we send the user registration data to to server.
It saves the data in the database and generating 2 tokens.
Refresh and access token, both of them gets back to the user,a ccess token as response body and refresh token as HttpOnly cookie.
On the client the access token (and the user data) get saved in the memory.
The login processs is the same, we fetch the user from the database (after all the validation of curse) and we send both of the tokens to the client.
On page load we run initial function that tries to get access token from the server. The server gets the HttpOnly cookie, if there isn't that means the user havent even logged in and the server will return nothing back. If the server gets the refresh token and validates it, that means the user has logged in and want to get his access token.
In the following diagram you can see the process when user tries to access protected page on the client.
If there is access token in the memory we send it as request header to the server that validates it,if there isnt that means user tries to access without getting authorized. For example some random client tries to access /url/profile, if he isn't authorized the website will kick him from the url.
Conclusion
Authentication and authorizing user is one of the most popular things and you likely to face in every app you make.
Thats why there is so many services thats provide you authentication helpers like firebase/next-auth/auth0 ext.
I like to create it myself, it makes my life easier because it can be customized as I want to.
If you have any questions feel free to ask.
Top comments (9)
the store you're using in
store.setAccessToken(data.accessToken)
is the zustand store? If you're storing the access token in a client-based store that kind of defeats the purpose no? It should be http-only and be in the header of requests. Otherwise there's no difference between what you're doing vs. storing it in localStorage. Correct me if I'm wrong.It stores it inside javascript memory.
Not inside cookie/localstorage which can be hacked.
Javascript memory is the same as-
let x = 0;
store.setAccessToken saves the accessToken inside javascript memory so you can imagine it as session.While the accessToken exists in the memory the session is running so user can run and access 'private data'.
Javascript memory is the safest way to keep secret data, more then http-only cookies.BTW, the token we store inside http cookie, wont give the user an option to access private data.
BTW, HttpOnly cookie, in aspect of security, is secure as much as localStorage is.
That's not accurate. HttpOnly cookies do help to protect against XSS by preventing client side access to the token. This is useful if you have 3rd party JavaScript injected to the page (plugins, trackers etc.).
See more here: cheatsheetseries.owasp.org/cheatsh...
The main idea is that even with HTTP-ONLY cookie type, I could XSS the browser and retrieve the token value by doing:
Then all I need to do is to set up a server with appropriate CORS
Please read this: academind.com/tutorials/localstora...
Very helpful. Would you recommend on storing the access token within Redux store, or SessionStorage? To me, these 2 methods are same in aspects of security
That's correct.
As far as I know, If you're rolling your own authentication, a session token in an HttpOnly cookie should suffice.
The main reason to separate the access and refresh token is in cases where you need access to the token on the client side, e.g. to make requests from an iFrame or in situations where you don't have access to the cookies.
Agree.Those are good methods.