Let's secure your NextJs webapp

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 fetch access_token from discord (in the backend)

    http://localhost:3000/callback?code=123
    
  • We'll handle this code and get access_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 a JWT in return of the access_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 getServerSidePropfunction 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

  1. 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');
      }
    });
    
  2. Middleware that intercept any incoming request express server (specific endpoints) and verify the signature of JWT sent with request in Authorization header with value as Bearer <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

  1. Once user approve discord permissions page, he/she will be redirect to callback page thru a get request with code query parameter. Now we take this code and request access_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-side getServerSideProps 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: {},
        };
      }
    }
    
  2. 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 :)