Let's secure your NextJs webapp
Tools: NextJs, ExpressJs, Discord Oauth & JWT.
In this article, I'm going to show y'all how to authorize your nextJs web applications, assuming you've express backend for your secured APIs & discord Oauth to authenticate users with JWT authentication.
Note:
- Even if you don't have express backend, you can still use this flow to secure your Next APIs or any other third party API that you're using.
- I'm assuming you already have Discord OAuth setup along with nextJs & express applications. This is not a beginner's setup tutorial, sorry!
Understanding the Auth Flow
Authentication process begins with clicking on
login with discord
button, which takes user to discord page that explains permissions your oauth app is asking for.https://discord.com/api/oauth2/authorize?client_id=<CLIENT-ID>&redirect_uri=http%3A%2F%2Flocalhost%3A3001%2Fcallback&response_type=code&scope=identify
Once user click on accept and continue, user will be redirected to callback page with
code
from discord in query param, which will be used to fetchaccess_token
from discord (in the backend)http://localhost:3000/callback?code=123
We'll handle this
code
and getaccess_token
in NextJs server-side via getServersideprops function of/callback
page.On same
/callback
page, here things gets different. Now we'll make a request to our express backend for aJWT
in return of theaccess_token
. It'll sign a JWT with proper payload and send back to nextJs function.
While at it, we can also have a whitelist users to have access to express API; Therefore, Only "few" or "selected" users will actually be able to complete auth process.
Finally, You can pass down the JWT from
getServerSideProp
function to your page function. And hence save it into your browser's local-storage.This goes without saying that you'll also need an auth middleware in your express application, to verify your jwt signature. But no worries, we'll discuss about that in details too.
Let's Begin Typing...
Login Page
Create a login page & add login with discord
button with link below. You'll get the discord oauth URL from discord developer portal.
https://discord.com/api/oauth2/authorize?client_id=<CLIENT-ID>&redirect_uri=http%3A%2F%2Flocalhost%3A3001%2Fcallback&response_type=code&scope=identify
Express Application
Endpoint
/api/auth/dashboard
expects discord's access_token which will be sent by nextJs server function and fetch user info, then sign a jwt after verifying user and send it back as response.const whitelistDiscordIDsForDashboard = ["your_discord_id", "other_user_discord_id"]; router.post('/api/auth/dashboard', async (req: Request, res: Response) => { try { const token = req.body.token; const userRes = await axios({ url: 'https://discord.com/api/oauth2/@me', headers: { Authorization: 'Bearer ' + token } }); const discordId = userRes.data.user.id; console.log(discordId); if (whitelistDiscordIDsForDashboard.includes(String(discordId)) == false) throw 'invalid user'; const jwtToken = jwt.sign({ discordId, token }, process.env.JWT_MASTER_KEY as string, { expiresIn: '30m' }); res.json({ jwtToken }); } catch (error) { console.log(error); res.status(400).send('something went wrong'); } });
Middleware that intercept any incoming request express server (specific endpoints) and verify the signature of JWT sent with request in
Authorization
header with value asBearer <token>
.import { NextFunction, Request, Response } from 'express'; import jwt from 'jsonwebtoken'; export const isAuthenticated = async (req: Request, res: Response, next: NextFunction) => { try { if (req.headers.authorization) { if (req.headers.authorization.split(' ')[0] !== 'Bearer') { throw 'Incorrect token prefix'; } const jwtToken = req.headers.authorization.split(' ')[1]; jwt.verify(jwtToken, process.env.JWT_MASTER_KEY as string, (err, decoded) => { if (err) { throw { message: 'Invalid token', status: 401 }; } // @ts-ignore req['user'] = decoded; next(); }); } else { throw { message: 'No token provided', status: 401 }; } } catch (error: any) { console.log(error); res.status(error.status ?? 500).json({ message: error.message ?? 'something went wrong!' }); } };
Callback Page
Once user approve discord permissions page, he/she will be redirect to callback page thru a
get
request withcode
query parameter. Now we take this code and requestaccess_token
from discord server. once we have that token, send it to our express server's endpoint/api/auth/dashboard
and get the jwt token in response. All of this will happen on next server-sidegetServerSideProps
function.export async function getServerSideProps(context: GetServerSidePropsContext) { try { const code = context.query.code; if (code == null) { throw "code not found!"; } const reqBody = new URLSearchParams(); reqBody.set("client_id", "993997262881562775"); reqBody.set("client_secret", "VmQrNx0bjtomlwFPtvGzsT9lUcEAWjEV"); reqBody.set("grant_type", "authorization_code"); reqBody.set("redirect_uri", "http://localhost:3001/callback"); reqBody.set("code", code as string); const discordRes = await axios({ method: "post", url: "https://discord.com/api/oauth2/token", headers: { "Content-Type": "application/x-www-form-urlencoded", }, data: reqBody, }); const token = discordRes.data.access_token; const authRes = await axios({ method: "post", url: "http://localhost:3000/api/auth/dashboard", headers: { "Content-Type": "application/json", }, data: { token }, }); return { props: { jwtToken: authRes.data.jwtToken, }, }; } catch (error: any) { console.log(error?.data ?? error); return { redirect: { permanent: false, destination: "/", }, props: {}, }; } }
Now that you've fetched JWT from express server and passed it to callback page as server-side prop. Let's save it in local storage and use it as authorized user.
const Callback: React.FC<{ jwtToken: string }> = ({ jwtToken }) => { const router = useRouter(); useEffect(() => { localStorage.setItem("authToken", jwtToken); setTimeout(() => { router.push("/custom"); }, 1000); }); return ( <Grid minHeight="80vh" w="full" placeContent="center"> <Spinner size="xl" m="auto" /> <Text textAlign={"center"} mt={4}> Verifying, Please Wait... </Text> </Grid> ); }; export default Callback;
Example: Make request to express server with JWT loaded in correct header.
const updateSomething = (data) => {
fetch("/api/xyz/", {
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + localStorage.getItem("authToken"),
},
body: JSON.stringify(data),
})
.then((res) => res.json())
.then((data) => {
setIsOpen(false);
showSuccessAlert();
})
}
Well that's all about authorization of nextJs application. I used it a commission so it's not open-source, hence i cannot share the full source code; But that's alright. Since I've shared all the relevant code for securing your application above. If you still have any doubts, reach me out at @Karan Sharma
Here's login demo for an application that uses above authentication system.
Thank you for reading, Happy coding :)