🐙

NextAuth.jsを使って、ORCID・The Open Science Framework・ GakuNin RDMの認証を行う

2024/11/15に公開

概要

NextAuth.jsを使って、ORCID・OSF(The Open Science Framework)・ GRDM(GakuNin RDM)の認証を行う方法です。

デモアプリ

ORCID

https://orcid-app.vercel.app/

OSF

https://osf-app.vercel.app/

GRDM

https://rdm-app.vercel.app/

リポジトリ

ORCID

https://github.com/nakamura196/orcid_app

以下がオプションの記述例です。

https://github.com/nakamura196/orcid_app/blob/main/src/app/api/auth/[...nextauth]/authOptions.js

export const authOptions = {
  providers: [
    {
      id: "orcid",
      name: "ORCID",
      type: "oauth",
      clientId: process.env.ORCID_CLIENT_ID,
      clientSecret: process.env.ORCID_CLIENT_SECRET,
      authorization: {
        url: "https://orcid.org/oauth/authorize",
        params: {
          scope: "/authenticate",
          response_type: "code",
          redirect_uri: process.env.NEXTAUTH_URL + "/api/auth/callback/orcid",
        },
      },
      token: "https://orcid.org/oauth/token",
      userinfo: {
        url: "https://pub.orcid.org/v3.0/[ORCID]",
        async request({ tokens }) {
          const res = await fetch(`https://pub.orcid.org/v3.0/${tokens.orcid}`, {
            headers: {
              Authorization: `Bearer ${tokens.access_token}`,
              Accept: "application/json",
            },
          });
          return await res.json();
        },
      },
      profile(profile) {
        return {
          id: profile["orcid-identifier"].path, // ORCID の ID を取得
          name: profile.person?.name?.["given-names"]?.value + " " + profile.person?.name?.["family-name"]?.value,
          email: profile.person?.emails?.email?.[0]?.email,
        };
      },
    },
  ],
  callbacks: {
    async session({ session, token }) {
      session.accessToken = token.accessToken;
      session.user.id = token.orcid; // ORCID ID をセッションに追加
      return session;
    },
    async jwt({ token, account }) {
      if (account) {
        token.accessToken = account.access_token;
        token.orcid = account.orcid;
      }
      return token;
    },
  },
};

OSF

https://github.com/nakamura196/osf-app

以下がオプションの記述例です。

https://github.com/nakamura196/osf-app/blob/main/src/app/api/auth/[...nextauth]/authOptions.js

export const authOptions = {
  providers: [
    {
      id: "osf",
      name: "Open Science Framework",
      type: "oauth",
      clientId: process.env.OSF_CLIENT_ID,
      clientSecret: process.env.OSF_CLIENT_SECRET,
      authorization: {
        url: "https://accounts.osf.io/oauth2/authorize",
        params: {
          scope: process.env.OSF_SCOPE || "osf.full_read osf.full_write", // 環境変数でスコープを管理
          response_type: "code",
          redirect_uri: `${process.env.NEXTAUTH_URL}/api/auth/callback/osf`, // 環境変数からリダイレクトURIを構築
        },
      },
      token: "https://accounts.osf.io/oauth2/token",
      userinfo: {
        url: "https://api.osf.io/v2/users/me/",
        async request({ tokens }) {
          const res = await fetch("https://api.osf.io/v2/users/me/", {
            headers: {
              Authorization: `Bearer ${tokens.access_token}`,
              Accept: "application/json",
            },
          });
          return await res.json();
        },
      },
      profile(profile) {
        return {
          id: profile.data.id, // GakuNin RDM のユーザー ID
          name: profile.data.attributes.full_name, // attributesの中からfull_nameを取得
          email: profile.data.attributes.email,    // attributesの中からemailを取得
        };
      }
    },
  ],
  callbacks: {
    async session({ session, token }) {
      session.accessToken = token.accessToken;
      session.refreshToken = token.refreshToken; // リフレッシュトークンをセッションに保存
      session.user.id = token.id; // osf ID をセッションに追加
      return session;
    },
    async jwt({ token, account, user }) {
      if (account) {
        token.accessToken = account.access_token;
        token.refreshToken = account.refresh_token; // リフレッシュトークンを保存
      }
      if (user) {
        token.id = user.id; // ユーザー ID をトークンに保存
      }
      return token;
    },
  }
};

GRDM

https://github.com/nakamura196/rdm_app

以下がオプションの記述例です。

https://github.com/nakamura196/rdm_app/blob/main/src/app/api/auth/[...nextauth]/authOptions.js

export const authOptions = {
  // debug: true, // next-auth のデバッグモードを有効化
  providers: [
    {
      id: "gakunin",
      name: "GakuNin RDM",
      type: "oauth",
      clientId: process.env.GAKUNIN_CLIENT_ID,
      clientSecret: process.env.GAKUNIN_CLIENT_SECRET,
      authorization: {
        url: "https://accounts.rdm.nii.ac.jp/oauth2/authorize",
        params: {
          client_id: process.env.GAKUNIN_CLIENT_ID, // クエリパラメータでclient_idを送信
          scope: process.env.OSF_SCOPE || "osf.full_read osf.full_write", // 環境変数でスコープを管理
          response_type: "code",
          redirect_uri: `${process.env.NEXTAUTH_URL}/api/auth/callback/gakunin`, // 環境変数からリダイレクトURIを構築
        },
      },
      token: {
        url: "https://accounts.rdm.nii.ac.jp/oauth2/token",
        async request(context) {
          const body = new URLSearchParams({
            client_id: process.env.GAKUNIN_CLIENT_ID, // 明示的に client_id を追加
            client_secret: process.env.GAKUNIN_CLIENT_SECRET,
            code: context.params.code, // 認可コード
            grant_type: "authorization_code",
            redirect_uri: `${process.env.NEXTAUTH_URL}/api/auth/callback/gakunin`,
          });

          const res = await fetch("https://accounts.rdm.nii.ac.jp/oauth2/token", {
            method: "POST",
            headers: {
              "Content-Type": "application/x-www-form-urlencoded",
            },
            body,
          });

          const json = await res.json(); // Parse the response body once

          if (!res.ok) {
            throw new Error(`Token request failed: ${res.statusText}`);
          }

          return {
            tokens: json
          }
        }
      },
      userinfo: "https://api.rdm.nii.ac.jp/v2/users/me/",
      profile(profile) {
        if (!profile.data || !profile.data.attributes) {
          throw new Error("Invalid user profile structure");
        }

        const user = {
          id: profile.data.id || "unknown", // Handle missing ID gracefully
          name: profile.data.attributes.full_name || "No Name",
          email: profile.data.attributes.email || "No Email",
        };

        return user
      },
    },
  ],
  callbacks: {
    async session({ session, token }) {
      // トークンからセッションに必要な情報を追加
      session.accessToken = token.accessToken;
      session.user = {
        ...session.user,
        id: token.id, // トークンのIDをセッションのユーザーに追加
      };
      return session;
    },

    async jwt({ token, account, user }) {
      if (account) {
        token.accessToken = account.access_token;
        token.refreshToken = account.refresh_token; // 必要であれば
      }
      if (user) {
        token.id = user.id; // プロファイルからユーザーIDをトークンに保存
      }
      return token;
    },
  },
};

まとめ

改善すべき点などがあるかと思いますが、参考になりましたら幸いです。

Discussion