DEV Community

Cover image for How to use NextAuth.JS with Discord
Afan Khan
Afan Khan

Posted on • Originally published at Medium

How to use NextAuth.JS with Discord

I was working on a dashboard project recently and had the task of letting Discord users log in to the application and choose one of their servers to modify its properties.

For someone who hadn't implemented authentication with NextJS before, I was clueless. I kept switching between NextAuth, Auth0, Clerk, and Supabase. I abandoned NextAuth in the first few seconds when I experienced an error.

Being a newbie is difficult. More often than not, these first options are the most ideal. I tried everything and came back to NextAuth. I figured out how to implement it and spent countless hours, so you don't have to!

Let's dive into the article and see how to implement authentication ourselves.

Discord Provider for NextAuth

NextAuth allows developers to set up authentication for multiple services, like Google, Facebook, Apple, Github, etc. NextAuth calls these services and platforms as providers. In other words, these providers are services, and users can use them to log in to your application.

We will use the Discord Provider for this article. Unlike other providers, Discord requires extra configuration to set up authentication. We must first sort those requirements.

You must create an application on the Discord Developer Portal to access the Discord API and Discord's infrastructure. Then, take the Client ID and the Client Secret. You might need to reset the Client Secret for security purposes.

You can proceed to the next step once you get the necessary credentials. If you cannot get the relevant information, do a basic search on your choice of browser, and someone will guide you.

NextJS App Initialisation

If you haven't already, you must set up the NextJS application. Use the following command in your terminal to initiate a NextJS application and follow the steps.

npx create-next-app@latest
Enter fullscreen mode Exit fullscreen mode

For the demonstration, I chose JavaScript over TypeScript (don't be angry at me) and TailwindCSS to demonstrate the authentication features in the front end. Also, I'm using the latest App Router to facilitate the API routes and a better experience overall.

I haven't chosen the src/ configuration either. We're going to do everything inside the app/ directory. However, you can follow the same steps and adjust the code snippets for your application.

Installing NextAuth

You must use the following command to install NextAuth in your project.

npm install next-auth
Enter fullscreen mode Exit fullscreen mode

After the installation, create a new API route to handle different requests related to authentication. First, create a folder named api/ inside the app/ directory.

Then, add a nested folder inside the api/ directory and name it auth/. Finally, make a directory called [...nextauth] inside the auth/ directory. NextAuth requires this directory structure strictly to do its magic.

To handle the incoming requests, create a route.js file inside the [...nextauth] folder. It is the dynamic route handler. Required Path: app/api/auth/[...nextauth]/route.js.

Directory Structure - Afan Khan LLC

Creating API Route for NextAuth

The dynamic route handler will store our NextAuth configuration and handle the incoming requests related to NextAuth.

Start by importing NextAuth to the route handler file. Then, import the Discord Provider from NextAuth. After importing them, we will begin configuring NextAuth.

import NextAuth from "next-auth/next";
import DiscordProvider from "next-auth/providers/discord";
Enter fullscreen mode Exit fullscreen mode

You will need your Client ID and Client Secret now. Keep them handy. Now, you will create an object named AuthOptions. It will be an object with values required for the authentication.

Remember, you must use the export keyword while initialising the object.

We will use this object to access the current session in server components. By the way, even the session is server-based and not DB-related. Here's how your file should look like.

import NextAuth from "next-auth/next";
import DiscordProvider from "next-auth/providers/discord";

export const AuthOptions = {
  providers: [
    DiscordProvider({
      clientId: "CLIENT_SECRET",
      clientSecret:
        "CLIENT_SECRET",
    }),
  ],
};
Enter fullscreen mode Exit fullscreen mode

Now, you must initialise the authentication using the NextAuth function. This function requires the AuthOptions and its properties as parameters. Therefore, we will pass the AuthOptions object as a parameter.

const handler = NextAuth(AuthOptions);
Enter fullscreen mode Exit fullscreen mode

You successfully integrated your configuration options with NextAuth. However, there's more. When someone uses Discord to log in to their account, Discord generates a code for the application to process and access the account of the signed-in user.

We can use this unique code to get a serialized token. A token is a unique string of characters representing the signed-in user and acts as a keycard to access the Discord API. We can only access the Discord API if we have the Client Secret of the bot or the token of a valid Discord user.

You can think of the token as the password to Discord's infrastructure and database but limited to the signed-in user.

Some of you might be wondering where this unique code resides. Discord puts it in the URL where the application redirects the user after a successful sign-in. Here's how NextAuth helps us.

Instead of putting our custom redirection URL, we will take the help of NextAuth. In the OAuth section of the selected application in the Discord Developer Portal, I will insert the following redirection URL, which will redirect the user to the route handler of NextAuth after a successful login.

The redirection URL is the path to the dynamic route handler we had created earlier in our locally stored application. In the production build, you will replace the localhost:3000 with your domain name or VPS IP.

Once the user logs in, they will get redirected to NextAuth's route handling system, and NextAuth will immediately swap the code for a token and store it.

NextAuth Callbacks

I want the user to get redirected to a different page once the user logs in. I don't want them to remain on the default NextAuth page or get redirected to the root directory (homepage) by default. Therefore, I'll use a callback by NextAuth to handle the redirection.

NextAuth provides callbacks that developers can configure for their unique use cases. NextAuth callbacks get invoked whenever specific events are triggered. For example, the signIn() callback will execute when a user logs in.

In our case, we will override the redirect() callback. The callback takes two parameters, but we only need the baseUrl, which is usually the URL of the root directory (localhost:3000/).

import NextAuth from "next-auth/next";
import DiscordProvider from "next-auth/providers/discord";

export const AuthOptions = {
  providers: [
    DiscordProvider({
      clientId: "YOUR_CLIENT_ID",
      clientSecret:
        "YOUR_CLIENT_SECRET",
    }),
  ],

  callbacks: {
    async redirect({ url, baseUrl }) {
      return baseUrl + "/dashboard";
    },
  },
};

const handler = NextAuth(AuthOptions);
Enter fullscreen mode Exit fullscreen mode

You will also export the result of invoking the NextAuth() function from the route handler. Otherwise, GET and POST requests to access the sessions, or otherwise, will not work.

Here's what your route.js file should look like. You can replace the /dashboard with whatever you desire.

import NextAuth from "next-auth/next";
import DiscordProvider from "next-auth/providers/discord";

export const AuthOptions = {
  providers: [
    DiscordProvider({
      clientId: "YOUR_CLIENT_ID",
      clientSecret:
        "YOUR_CLIENT_SECRET",
    }),
  ],

  callbacks: {
    async redirect({ url, baseUrl }) {
      return baseUrl + "/dashboard";
    },
  },
};

const handler = NextAuth(AuthOptions);

export { handler as GET, handler as POST };
Enter fullscreen mode Exit fullscreen mode

Discord Permissions

By default, you can only access the username and email of the signed-in user. You can try to use the Discord API and its endpoints to access more information, but Discord will deny the access. It is because Discord boosts privacy and security.

You need the user's permission to access more information. You can take their permission while they are trying to log in. If you have noticed, Discord lists the permissions the user gives to the application when trying to log in.

Discord Permissions Image - Afan Khan LLC

If you thought they were random, you were mistaken. The developer requests these permissions, and users usually overlook them. We can request permissions by adding them to the authorization URL. It's time to go back to the route.js file.

In the DiscordProvider function, you must add another property named authorization. The value of this property will depend on your requirements of the application. As I had stated earlier, I want to access the guilds of the signed-in user and display them.

Therefore, I will ask for that permission, including a few others. You can find them in the Discord Developer Portal.

Permission Options - Afan Khan LLC

Every permission name will be connected using the '+' symbol in the URL query params.

providers: [
    DiscordProvider({
      clientId: "YOUR_CLIENT_ID",
      clientSecret:
        "YOUR_CLIENT_SECRET",
      authorization:
        "https://discord.com/api/oauth2/authorize?scope=identify+guilds",
    }),
  ],
Enter fullscreen mode Exit fullscreen mode

Now, let us try to configure the front end to initiate the login process.

Configuring the front-end

Since I initiated a new application, I will make modifications in the root directory. If you wish to put the login button elsewhere, you can do so. The steps remain the same.

I will create a client component named LoginUsingDiscord.jsx in the root directory to handle the onClick events because my main page.js file will remain in the default server component mode.

Inside the component, I will import the signIn() function from "next-auth/react" and invoke it whenever someone clicks on the "Login" button. We can pass one parameter to the signIn() function. It is the name of the provider that the login button represents and should get invoked when clicked.

"use client";

import { signIn, signOut } from "next-auth/react";

export default function LoginUsingDiscord() {
  return (
    <section className="flex gap-8 items-center justify-center mt-12">
      <p className="font-bold text-lg">Login Using Discord</p>
      <button
        onClick={() => signIn("discord")}
        className="text-base py-3 px-4 bg-[#5865F2]"
      >
        Login
      </button>
    </section>
  );
}
Enter fullscreen mode Exit fullscreen mode

And that's it. Once you click the button, you will get redirected to the Discord login interface. After the serialization of the token, you will get pointed towards your selected URL from the redirect() callback. For me, it was /dashboard.

After a successful login, you can access the session at localhost:3000/api/auth/session. As I explained, this session will only have the username and email by default. I want to store the Discord user ID of the signed-in user and use it globally in the application.

I will override another callback function named signIn() inside route.js. For security purposes, the token isn't accessible elsewhere in the application except in the boundaries of NextAuth.

async signIn({ account }) {
  const discordToken = account.access_token;

  return true;
},
Enter fullscreen mode Exit fullscreen mode

I'm glad you found this article to be helpful. I'm writing an eBook to help people get jobs, earn outsized salaries, etc. It will take me a month or two to complete it.

However, I've opened the waitlist for people to hop in early and benefit from the early-bird discount and exclusive access to the community to grow a personal brand. You will also meet like-minded people.

Here's the link to sign up. I will not bore you with emails. But if I send an email, it will be worth looking out for.


The signIn() callback provides the user, account, session and other values as parameters. We only require the account parameter to retrieve the access token.

In my dashboard project, I stored the token inside the DB after hashing it. However, I won't do the same here. I will try to access the information about the user using the token and print it in the console for simplicity.

You must save the token somewhere to use the Discord API in routes or server components. Every request to the Discord API requires the Discord user token. Since I cannot make this article complex, I will not store the token anywhere and solely display the fetched data in the console.

However, I will guide you on accessing the session on other pages in your application and display information later.

Since I'm not storing the token anywhere, I cannot use it outside the route.js file. Therefore, I will make an API request to the Discord API inside the signIn() callback and show you how it works.

Using the Discord API

Let us continue modifying the signIn() callback. I will fetch the information about the user and print it to the console for simplicity. The endpoints of these API requests are in the Discord Documentation.

I'm making a GET request to the api/users/@me endpoint provided by Discord to get an object with the information of the signed-in user. It doesn't matter whether the user is signed in. Each access token represents a unique user.

The token expires and requires a renewal every month. I make those API requests on the user's behalf whenever I use their token. Discord treats me as a mediator.

async signIn({ account }) {
   const discordToken = account.access_token;

   const discordUser = await fetch(`https://discord.com/api/users/@me`, {
      method: "GET",
      headers: {
        Authorization: `Bearer ${discordToken}`,
      },
    }).then((res) => res.json());

    console.log(discordUser);

    return true;
},
Enter fullscreen mode Exit fullscreen mode

You can pass the access token in the Authorization property of the header and get a response from the API endpoint if you did everything correctly.

Response from API

I eventually added a sign-out button inside the LoginUsingDiscord component. I used the signOut() function. It does not require any parameters because only one service can have an active session. Here's the code.

<button
  onClick={() => signOut()} className="text-base py-3 px-4 bg-[#ED4245]">
  Sign Out
</button>
Enter fullscreen mode Exit fullscreen mode

Conclusion

Authentication is challenging to implement in most applications, and NextAuth makes it easier for us to do so. You learned how to create a session, allow users to log in using Discord, use the token to send API requests to the Discord API, etc.

In the same way, you can use other providers, such as Google. I hope this article proved helpful to you. If you want to get a job as a software engineer, I suggest you join the waitlist.

If you have any doubts, reach out to hello@afankhan.com or @whyafan elsewhere.

Top comments (2)

Collapse
 
thomasbnt profile image
Thomas Bnt ☕

Hello ! Don't hesitate to put colors on your codeblock like this example for have to have a better understanding of your code 😎

console.log('Hello world!');
Enter fullscreen mode Exit fullscreen mode

Example of how to add colors and syntax in codeblocks

Collapse
 
document-translate profile image
Document Translate

Nice