Overview

This article describes how to perform authentication with ORCID, OSF (The Open Science Framework), and GRDM (GakuNin RDM) using NextAuth.js.

Demo Apps

ORCID

OSF

GRDM

Repository

ORCID

Below is an example of the options configuration.

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, // Get 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; // Add ORCID ID to session
      return session;
    },
    async jwt({ token, account }) {
      if (account) {
        token.accessToken = account.access_token;
        token.orcid = account.orcid;
      }
      return token;
    },
  },
};

OSF

Below is an example of the options configuration.

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", // Manage scope via environment variable
          response_type: "code",
          redirect_uri: `${process.env.NEXTAUTH_URL}/api/auth/callback/osf`, // Build redirect URI from environment variable
        },
      },
      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 user ID
          name: profile.data.attributes.full_name, // Get full_name from attributes
          email: profile.data.attributes.email,    // Get email from attributes
        };
      }
    },
  ],
  callbacks: {
    async session({ session, token }) {
      session.accessToken = token.accessToken;
      session.refreshToken = token.refreshToken; // Save refresh token to session
      session.user.id = token.id; // Add OSF ID to session
      return session;
    },
    async jwt({ token, account, user }) {
      if (account) {
        token.accessToken = account.access_token;
        token.refreshToken = account.refresh_token; // Save refresh token
      }
      if (user) {
        token.id = user.id; // Save user ID to token
      }
      return token;
    },
  }
};

GRDM

Below is an example of the options configuration.

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

export const authOptions = {
  // debug: true, // Enable next-auth debug mode
  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, // Send client_id as query parameter
          scope: process.env.OSF_SCOPE || "osf.full_read osf.full_write", // Manage scope via environment variable
          response_type: "code",
          redirect_uri: `${process.env.NEXTAUTH_URL}/api/auth/callback/gakunin`, // Build redirect URI from environment variable
        },
      },
      token: {
        url: "https://accounts.rdm.nii.ac.jp/oauth2/token",
        async request(context) {
          const body = new URLSearchParams({
            client_id: process.env.GAKUNIN_CLIENT_ID, // Explicitly add client_id
            client_secret: process.env.GAKUNIN_CLIENT_SECRET,
            code: context.params.code, // Authorization 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 }) {
      // Add necessary information from token to session
      session.accessToken = token.accessToken;
      session.user = {
        ...session.user,
        id: token.id, // Add token ID to session user
      };
      return session;
    },

    async jwt({ token, account, user }) {
      if (account) {
        token.accessToken = account.access_token;
        token.refreshToken = account.refresh_token; // If needed
      }
      if (user) {
        token.id = user.id; // Save user ID from profile to token
      }
      return token;
    },
  },
};

Summary

There may be areas for improvement, but I hope this serves as a useful reference.