🔒

NextAuthで指定したギルドユーザー限定のDiscordログインを実装する

2023/01/22に公開1

概要

こんにちは!現在仕事で東京で 1 ヶ月ホテル生活をしていますユウトです!

今回は自分の所属しているコミュニティで、NextAuth を使って Discord ログイン機能を実装し、その中で特定のギルドに入っていないとログインできないような機能を作りました!

実装していく中であんまり参考になるような記事がなかったので、メモがてら書きます!

使用技術

…etc

早速本題

Discord OAuth2 の設定

まずは Discord ログインの機能を使用するために、Discord の OAuth2 の設定をします。

  1. https://discord.com/developers/applications にアクセスをして、Discord ログインをする
  2. New Applicationをクリックし、アプリケーションの名前を入力する
    Discord OAuth2の設定
  3. リダイレクト URL を入力する
    Redirects という場所に、http://localhost:3000/api/auth/callback/discord を追加する
    (本番とかであれば、http://twitter.com/api/auth/callback/discord みたいな感じでもう一つ追加すれば OK)
    Redirectsの設定

NextAuth の設定

インストール

$ yarn add next-auth

Provider の設定

pages/api/auth/[...nextauth].js
import NextAuth from "next-auth";
import DiscordProvider from "next-auth/providers/discord";

export const authOptions = {
  providers: [
    DiscordProvider({
      clientId: process.env.DISCORD_CLIENT_ID,
      clientSecret: process.env.DISCORD_CLIENT_SECRET,
    }),
  ],
};

export default NextAuth(authOptions);

ここまでで一旦確認

実はここまでで Discord ログインは可能です。

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

export const LoginPage: NextPage = () => {
  const { data: session } = useSession();

  return <button onClick={signIn}>Discordでログイン</button>;
};

みたいな感じで、ボタン と NextAuth から import した signIn メソッドを使用すると、ログインができるようになっているはずです。

そしてログインに成功すると、useSessionsession にログインしたユーザーの情報が入っているのが確認できます。

ただ、これだと特定のギルドに入っているかというチェックができていないので、次にそのチェックを実装していきます。

ギルドに入っているかどうかを確認するための準備

ギルド情報を取得する許可を与える

pages/api/auth/[...nextauth].js
import NextAuth from "next-auth";
import DiscordProvider from "next-auth/providers/discord";

export const authOptions = {
  providers: [
    DiscordProvider({
      clientId: process.env.DISCORD_CLIENT_ID,
      clientSecret: process.env.DISCORD_CLIENT_SECRET,
+      authorization: {
+       params: { scope: 'identify email guilds' },
+     },
    }),
  ],
};

export default NextAuth(authOptions);

これでギルド情報を取得する許可を与えました。

ギルド情報を取得するための API を叩く

  1. アプリ内で、さっきと同じ方法で普通にログインをする

  2. https://discord.com/developers/docs/resources/user#get-current-user-guilds を使って、ログインしたユーザーのギルドを取得する

    NextAuth に便利な callback があるので、それを使います。

    pages/api/auth/[...nextauth].js
    import NextAuth from "next-auth";
    import DiscordProvider from "next-auth/providers/discord";
    
    export const authOptions = {
      providers: [
        DiscordProvider({
          clientId: process.env.DISCORD_CLIENT_ID,
          clientSecret: process.env.DISCORD_CLIENT_SECRET,
          authorization: {
            params: { scope: 'identify email guilds' },
          },
        }),
      ],
    
    +  callbacks: {
    +    /**
    +      * sessionにaccessTokenと、user.idを追加
    +      */
    +    session: async ({ session, token }) => {
    +      session.accessToken = token.accessToken;
    +      if (session.user) {
    +        session.user.id = token.id;
    +      }
    +      return session;
    +    },
    +
    +    /**
    +      * jwtにaccessTokenと、profile.idを追加
    +      */
    +    jwt: async ({ token, account, profile }) => {
    +      if (account && account.access_token) {
    +        token.accessToken = account.access_token;
    +      }
    +      if (profile) {
    +       token.id = profile.id;
    +      }
    +      return token;
    +    },
    +    },
    + };
    
    export default NextAuth(authOptions);
    
  3. 実際に api 叩く

    pages/index.tsx
    export const Home:FC = () => {
     const { data: session } = useSession();
    
     const handleClick = async () => {
       const res = await fetch(`https://discordapp.com/api/users/@me/guilds`, {
         headers: {
           Authorization: `Bearer ${session?.accessToken}`,
         },
       });
       console.log('res', res);
     };
    
     return <button onClick={handleClick}>ギルド情報を取得</button>;
    }
    

ここまでで、

  1. Discord でログインをする
  2. ログインユーザーの加入しているギルド情報を取得する

ということはできました。

ここから最後に、特定のギルドに入っていたらログイン成功の処理をやっていきます。

特定のギルドに入っていたらログイン成功

入っていて欲しいギルドの ID を指定する

  1. ID は discord の web 版から確認できます。

  2. https://discord.com/channels/10000000001/100000000210000000001のところが ID です。

  3. 以下のような型の値が返ってきます。

    features: string[]
    icon: string
    id: string
    name: string
    owner: boolean
    permissions: number
    permissions_new: string
    
  4. ギルドに加入しているかどうかを判定するメソッドを作ります

    async isJoinGuild(accessToken: string): Promise<boolean> {
      const res: Response = await fetch("https://discordapp.com/api/users/@me/guilds", {
        headers: {
          Authorization: `Bearer ${accessToken}`,
        },
      });
      if (res.ok) {
        const guilds: Guild[] = await res.json();
        return guilds.some((guild: Guild) => guild.id === guildId);
      }
      return false;
    },
    

最後にこの作成したメソッドを使用します!

isJoinGuild を使用する

  • callback 内の、signIn というメソッドの中で使用すると、signIn した際に自動で発火するようにしてくれます。
  • この際に、false を返すとログイン失敗になります。
callbacks: {
  signIn: async ({ account, user, profile }) => {
    if (account == null || account.access_token == null) return false;
    return await isJoinGuild(account.access_token)
  },
}

ここまでで、特定のギルドに入っていたらログイン成功という処理ができました!!

まとめ

最後に一連の流れで実装した[…nextauth].ts 内のコードを確認します!

import { discordClientId, discordClientSecret } from '@/constants/env';
import { DiscordClient } from '@/lib/discord-client';
import NextAuth, { type NextAuthOptions } from 'next-auth';
import DiscordProvider from 'next-auth/providers/discord';

async isJoinGuild(accessToken: string): Promise<boolean> {
  const res: Response = await fetch(`${BASE_DISCORD_URL}/api/users/@me/guilds`, {
    headers: {
      Authorization: `Bearer ${accessToken}`,
    },
  });
  if (res.ok) {
    const guilds: Guild[] = await res.json();
		return guilds.some((guild: Guild) => guild.id === guildId);
  }
  return false;
},

export const authOptions: NextAuthOptions = {
  providers: [
    DiscordProvider({
      clientId: discordClientId,
      clientSecret: discordClientSecret,
      authorization: {
        params: { scope: 'identify email guilds' },
      },
    }),
  ],

  /**
   * ログイン情報を保存できる
   */
  session: { strategy: 'jwt' },

  callbacks: {
    /**
     * sessionにaccessTokenと、user.idを追加
     */
    session: async ({ session, token }) => {
      session.accessToken = token.accessToken;
      if (session.user) {
        session.user.id = token.id;
      }
      return session;
    },

    /**
     * jwtにaccessTokenと、profile.idを追加
     */
    jwt: async ({ token, account, profile }) => {
      if (account && account.access_token) {
        token.accessToken = account.access_token;
      }
      if (profile) {
        token.id = profile.id;
      }
      return token;
    },

    /**
     * ログインボタンを押した際に発火される
     */
    signIn: async ({ account, user, profile }) => {
      if (account == null || account.access_token == null) return false;
      return await isJoinGuild(account.access_token);
    },
  },
};

export default NextAuth(authOptions);

あとは普通にボタンに nextauth の signIn を設定してあげれば、ログインできるようになります!

最後に

今回は、Discord でログインをする方法を紹介しました!
この記事が、誰かの役に立てば幸いです!

参考

Discussion

TecSocTecSoc

記事ありがとうございます。とても参考になりました。

些細なことですが
jwtの最後の方、括弧のインデントがおかしい気がします
(括弧の数は合ってそうです)

   jwt: async ({ token, account, profile }) => {
      if (account && account.access_token) {
        token.accessToken = account.access_token;
      }
      if (profile) {
       token.id = profile.id;
      }
      return token;
    },
    },
 };