(Translated by https://www.hiragana.jp/)
NextAuth.jsについて調べたので使い方まとめとく
🔑

NextAuth.jsについて調しらべたので使つかかたまとめとく

2022/11/06公開こうかい
1けん

アプリ設定せってい

GitHubとGoogleでのログインをためすため、あらかじめアプリをつくっておく。かくアプリ設定せってい参考さんこうリンク参照さんしょうかくクライアントIDとクライアントシークレットは取得しゅとくしてあるものする。

開発かいはつ環境かんきょう準備じゅんび

以下いかのファイルを準備じゅんびしてdocker compose起動きどう

docker-compose.yml
version: "3"
services:
  app:
    build:
      context: .
    container_name: nextauth-example-app
    ports:
      - "3000:3000"
    tty: true
    volumes:
      - ./app:/app
    working_dir: /app
Dockerfile
FROM node:18-slim

RUN apt-get update
RUN apt-get install -y openssl

アプリようのコンテナ起動きどう

$ docker compose up

コンテナにログインして、Next.jsをインストール起動きどうできることを確認かくにんする

$ docker exec -it nextauth-example-app bash
$ npx create-next-app . --ts --use-npm
$ npm run dev

NextAuth.js をインストール

$ npm install --save next-auth

データベースなしで使つか

アプリではユーザー情報じょうほう管理かんりはせずに、GitHubにログインしてユーザーめいとメールアドレスを取得しゅとくする。GitHubの認証にんしょう必要ひつようなクライアントIDとシークレットを.env記述きじゅつする。また、NEXTAUTH_SECRETもここで指定していする。

app/.env
NEXTAUTH_SECRET=xxxxxxxxxx
GITHUB_ID=xxxxxxxxxxxxxxxxxxxx
GITHUB_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

.envのかた定義ていぎ

app/types/environment.d.ts
namespace NodeJS {
  interface ProcessEnv extends NodeJS.ProcessEnv {
    GITHUB_ID: string;
    GITHUB_SECRET: string;
  }
}
app/tsconfig.json
{
  ・・・
  "include": [
    "types/**/*.ts",
    ・・・
  ],
}

NextAuthの設定せってい。データベースを使つかわないのでセッションの保存ほぞんさきにJWTを指定してい

app/pages/api/auth[...nextauth].ts
import NextAuth, { NextAuthOptions } from "next-auth";
import GithubProvider from "next-auth/providers/github";

export const authOptions: NextAuthOptions = {
  providers: [
    GithubProvider({
      clientId: process.env.GITHUB_ID,
      clientSecret: process.env.GITHUB_SECRET,
    }),
  ],
  session: { strategy: "jwt" },
}

export default NextAuth(authOptions);

かくコンポーネントからセッションにアクセスするためにSessionProvider適用てきよう

app/pages/_app.tsx
import "../styles/globals.css";
import type { AppProps } from "next/app";
import { Session } from "next-auth";
import { SessionProvider } from "next-auth/react";

export default function App({ Component, pageProps }: AppProps<{ session: Session }>) {
  return (
    <SessionProvider session={pageProps.session}>
      <Component {...pageProps} />
    </SessionProvider>
  );
}

GitHubでの認証にんしょうとセッションを使つか準備じゅんびできたので、ログインならSigninボタン、ログインちゅうならセッションの中身なかみとSignoutボタンを表示ひょうじする。

ログイン状態じょうたいuseSessionがえ判定はんていすることができる。ここではsignIn()引数ひきすうなしでんでいる。こうすることで有効ゆうこうなプロバイダーのログインボタンが一覧いちらん表示ひょうじされる。signIn("github")のようにプロバイダーを指定していしてぶこともできる。

app/pages/index.tsx
import { useSession, signIn, signOut } from "next-auth/react";

export default function Home() {
  const { data: session } = useSession();

  return session ? (
    <>
      {JSON.stringify(session)}
      <button onClick={() => signOut()}>SignOut</button>
    </>
  ) : (
    <>
      <button onClick={() => signIn()}>SignIn</button>
    </>
  );
}

また、デフォルトのSessionは以下いかのとおり定義ていぎされている

export interface DefaultSession {
  user?: {
    name?: string | null
    email?: string | null
    image?: string | null
  }
  expires: ISODateString
}

export interface Session extends DefaultSession {}

アクセストークンを使つか

ログインちゅうのユーザーのGitHubのリポジトリの一覧いちらんをアクセストークンを使用しようして取得しゅとくする。うえ定義ていぎのとおり、デフォルトのSessionにはアクセストークンが定義ていぎされていないので、Sessionのuserにこれを定義ていぎする。

app/types/next-auth.d.ts
import { DefaultSession } from "next-auth";

declare module "next-auth" {
  interface Session {
    user: {
      accessToken?: string;
    } & DefaultSession["user"];
  }
}

デフォルトの挙動きょどうではセッションにアクセストークンがふくまれないので[...nextauth].tscallbacksでデフォルトの挙動きょどう拡張かくちょうする。sessionコールバックでかえがセッションとして使つかえるのでここでアクセストークンをセットする。

app/pages/api/auth/[...nextauth].ts
callbacks: {
  ・・・
  async session({ session, token }) {
    session.user.accessToken = token.accessToken;
    return session;
  },
},

ただし、sessionコールバックの引数ひきすうわたされるtoken(JWT)にもアクセストークンがふくまれないので、JWTも拡張かくちょうする必要ひつようがある。

app/types/next-auth.d.ts
import { JWT } from "next-auth/jwt";
・・・
declare module "next-auth/jwt" {
  interface JWT {
    accessToken?: string;
  }
}

また、デフォルトのJWTは以下いかのとおり定義ていぎされている

export interface DefaultJWT extends Record<string, unknown> {
  name?: string | null
  email?: string | null
  picture?: string | null
  sub?: string
}

export interface JWT extends Record<string, unknown>, DefaultJWT {}

以下いかは、引用いんよう翻訳ほんやく

セッション コールバックは、セッションがチェックされるたびにされます。デフォルトでは、セキュリティをたかめるためにトークンのサブセットのみがかえされます。もしあなたがjwt()コールバックをとおしてトークンに追加ついかしたもの(上記じょうきのaccess_tokenやuser.idなど)を利用りよう可能かのうにしたい場合ばあいは、明示めいじてきにここに転送てんそうしてクライアントが利用りようできるようにしなければなりません。

引数ひきすうuser、account、profile、isNewUserは、ユーザーがサインインしたのちあたらしいセッションでこのコールバックが最初さいしょされたときのみわたされます。それ以降いこうしでは、tokenのみが使用しよう可能かのうです。

app/pages/api/auth/[...nextauth].ts
callbacks: {
  ・・・
  async jwt({ token, account }) {
    if (account) {
      token.accessToken = account.access_token
    }
    return token
  }
},

セッションからアクセストークンを取得しゅとくできるようになる。また、今回こんかいのサンプルアプリではリポジトリの一覧いちらん取得しゅとくしたいので、scope設定せっていする。

app/pages/api/auth/[...nextauth].ts
・・・
export const authOptions: NextAuthOptions = {
  providers: [
    GithubProvider({
      ・・・
      authorization: {
        params: { scope: "repo" },
      },
    }),
  ],
  ・・・
}

export default NextAuth(authOptions);

アクセストークンを使つかったAPIへリクエストができるようになった

app/pages/index.tsx
export default function Home() {
  const { data: session } = useSession();
  
  const fetchRepos = () => {
    if (session?.user.accessToken) {
      return;
    }
    const url = "https://api.github.com/user/repos?per_page=10";
    const headers = {
      Authorization: "token " + session.user.accessToken,
    };
    fetch(url, { headers })
      .then((res) => res.json())
      .then((json) => console.log(json));
  }
  ・・・
}

データベースありで使つか

データベース連携れんけいすることでアプリでユーザー情報じょうほう管理かんりすることができる。その場合ばあいは、NextAuth.jsが期待きたいするテーブル構造こうぞうにする必要ひつようがある

このアプリではUserAccount使用しようする。また、VerificationTokenはメールアドレスを使つかったパスワードレスログインが必要ひつよう場合ばあいに、Sessionはセッションをデータベースで管理かんりする場合ばあい使用しようする

具体ぐたいてきなデータベース処理しょりはNextAuth.jsでかくアダプター用意よういされている。このアプリではPrismaMariaDB使用しようする

データベースの準備じゅんび

MariaDBの接続せつぞく情報じょうほうdocker-compose.yml編集へんしゅうしてdocker compose upしなおす

app/.env
・・・
MARIADB_HOST=nextauth-example-mariadb
MARIADB_USER=user
MARIADB_DATABASE=app
MARIADB_PASSWORD=password
MARIADB_ROOT_PASSWORD=password

DATABASE_URL="mysql://${MARIADB_USER}:${MARIADB_PASSWORD}@${MARIADB_HOST}:3306/${MARIADB_DATABASE}"
docker-compose.yml
version: "3"
services:
    ・・・
  mariadb:
    image: mariadb:10.9.3
    container_name: nextauth-example-mariadb
    restart: always
    ports:
      - "3306:3306"
    env_file:
      - app/.env
    volumes:
      - mariadb:/var/lib/mysql
      - ./mariadb:/docker-entrypoint-initdb.d

volumes:
  mariadb:
    name: nextauth-example-mariadb
mariadb/dcl.sql
GRANT ALL ON *.* TO user;

Next.jsが起動きどうするコンテナにログインしてPrismaのインストールと初期しょき処理しょりおこな

$ docker exec -it nextauth-example-app bash
$ npm install --save next-auth @prisma/client
$ npx prisma init

prisma/schema.prisma作成さくせいされるのでNextAuth.jsのModel参考さんこうにスキーマ定義ていぎ追記ついきする

app/prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

model Account {
  id String @id @default(cuid())
  userId String
  type String
  provider String
  providerAccountId String @map("provider_account_id")
  refresh_token String? @db.Text
  refresh_token_expires_in Int?
  access_token String? @db.Text
  expires_at Int?
  token_type String?
  scope String?
  id_token String? @db.Text
  session_state String?

  user User @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@unique([provider, providerAccountId])
  @@map("accounts")
}

model User {
  id String @id @default(cuid())
  name String?
  email String? @unique
  emailVerified DateTime? @map("email_verified")
  image String?
  accounts Account[]

  @@map("users")
}

NextAuthアダプターの設定せってい

Prismaクライアントの生成せいせいとマイグレーション実行じっこうする。また、NextAuth.jsPrismaアダプターをインストールしておく

$ npx prisma generate
$ npx prisma migrate dev --name init
$ npm install --save @next-auth/prisma-adapter

[...nextauth].tsPrismaアダプターを使用しようするように追記ついき

app/pages/api/auth/[...nextauth].ts
・・・
import { PrismaClient } from "@prisma/client";
import { PrismaAdapter } from "@next-auth/prisma-adapter";

const prisma = new PrismaClient();

export const authOptions: NextAuthOptions = {
    ・・・
  adapter: PrismaAdapter(prisma),
}

export default NextAuth(authOptions);

さきほどつくったSigninボタンでログインしなおすとテーブルにユーザー情報じょうほう保存ほぞんされることが確認かくにんできる

複数ふくすうアカウントと連携れんけいする

さきほど登録とうろくされたUserとしてGoogleログインもできるようにする。ここでは省略しょうりゃくするが.envtypes/environment.d.tsにGitHubとおなじようにGOOGLE_IDGOOGLE_SECRET追記ついきする

Googleプロバイダを追記ついき

app/pages/api/[...nextauth].ts
import GoogleProvider from "next-auth/providers/google";
・・・

export const authOptions: NextAuthOptions = {
  providers: [
    ・・・
    GoogleProvider({
      clientId: process.env.GOOGLE_ID,
      clientSecret: process.env.GOOGLE_SECRET,
    }),
  ],
  ・・・
}

export default NextAuth(authOptions);

ログイン状態じょうたいsignInぶとログインちゅうUserひもづくAccount生成せいせいされる

app/pages/index.tsx
import { signIn, ・・・ } from "next-auth/react";
・・・

export default function Home() {
  const { data: session } = useSession();

  return session ? (
    <>
      <button onClick={() => signIn("google")}>Link Google</button>
      ・・・
    </>
  ) : (
    ・・・
  );
}

Userは1レコードのままでAccountが2レコードになっていることが確認かくにんできる

アダプターをカスタマイズする

GoogleとGitHubで使用しようしているメールアドレスがおな場合ばあい、それぞれでSignInしようとするとメールアドレスが重複じゅうふくするため登録とうろくできない。これはNextAuth.jsの実装じっそうである。ここではこれを回避かいひするためにUser登録とうろくするさいemail強制きょうせいてきnullをセットするようにする。

app/pages/api/[...nextauth].ts
・・・
import { PrismaAdapter } from "@next-auth/prisma-adapter";
import { AdapterUser } from "next-auth/adapters";

const prisma = new PrismaClient();

export const prismaAdapter: Adapter = {
  ...PrismaAdapter(prisma),
  
  // メールアドレスでの検索けんさく不可ふかとする
  getUserByEmail: () => null,
  
  // email = null として User を登録とうろくする
  createUser: async (data) => {
    const user = await prisma.user.create({ data: { ...data, email: null } });
    return user as AdapterUser;
  },
};

export const authOptions: NextAuthOptions = {
    ・・・
  adapter: prismaAdapter,
}

export default NextAuth(authOptions);

ミドルウェアを使つかったSignInとSignUp

このアプリでははじめてSigninするとSignupしたことになるので、はじめてのSigninにSignupページをはさんでからアプリを使つかえるようにする。うえUser.emailnullとしたので、ここではSignupページでEmailの入力にゅうりょくフォームをもうけてメールアドレスを登録とうろくしたらSignupしたこととする

ミドルウェアでページの保護ほごができるので、メールアドレスの設定せってい完了かんりょうしているかを条件じょうけんれる

app/middleware.ts
import { withAuth } from "next-auth/middleware";

export default withAuth({
  callbacks: {
    async authorized({ token }) {
      if (token?.name && !token?.email) {
        return false;
      } else {
        return true;
      }
    },
  },
});

export const config = { matcher: ["/"] };

SigninのURLで/signup指定していする

app/pages/api/auth/[...nextauth].ts
callbacks: {
  ・・・
  async redirect({ baseUrl }) {
    return `${baseUrl}/signup`;
  },
},

signupページを追加ついかする。ここではsignout状態じょうたいまたはすでにメールの設定せってい完了かんりょうしていないかどうかを表示ひょうじまえにチェックする

app/pages/signup.tsx
import { GetServerSideProps } from "next";
import { getSession } from "next-auth/react";
・・・

export const getServerSideProps: GetServerSideProps = async (context) => {
  const session = await getSession(context);

  if (!session || session?.user.email != null) {
    return {
      redirect: {
        permanent: false,
        destination: "/",
      },
    };
  }

  return { props: {} };
};

メールを設定せっていするAPIとリクエストする部分ぶぶん実装じっそうする

app/pages/signup.tsx
import { useState } from "react";
import { ・・・, signIn } from "next-auth/react";
・・・

export default function Signup() {
  const [email, setEmail] = useState("");
  const [signuped, setSignuped] = useState(false);
  const onSignup = async () => {
    const res = await fetch("/api/signup", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ email }),
    });
    if (res.status == 200) {
      setSignuped(true);
    } else {
      alert("error signup");
    }
  };

  return signuped ? (
    <>
      <h2>Success Signup</h2>
      <button onClick={() => signIn()}>Re Signin</button>
    </>
  ) : (
    <>
      <h2>Signup</h2>
      <input placeholder="email" onChange={(e) => setEmail(e.target.value)} />
      <button onClick={onSignup}>Signup</button>
    </>
  );
}
・・・

API

app/pages/api/signup.ts
import type { NextApiRequest, NextApiResponse } from "next";
import { unstable_getServerSession } from "next-auth";
import { authOptions, prismaAdapter } from "./auth/[...nextauth]";

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  const session = await unstable_getServerSession(req, res, authOptions);
  if (!session?.user.id) {
    return res.status(401).end();
  }

  let user = await prismaAdapter.getUser(session!.user.id!);
  if (!user || user.email) {
    return res.status(401).end();
  }

  await prismaAdapter.updateUser({ ...user, email: req.body["email"] });

  res.status(200).json({});
}

メール + パスワードでの認証にんしょう

https://next-auth.js.org/providers/credentials#example---username--password
Credentials Provider使用しようする。ただし、ユーザー登録とうろく・メール認証にんしょうそなえたユーザー登録とうろく・パスワードリセットなどの機能きのう必要ひつよう場合ばあい独自どくじ実装じっそうする必要ひつようがある。また、Credentials ProviderではJWTを使つかったセッション管理かんりおこな

その

今回こんかいつくったサンプルアプリ
https://github.com/nrikiji/nextauth-example

参考さんこうリンク

GitHub
https://docs.github.com/ja/developers/apps/building-oauth-apps/creating-an-oauth-app

Google
https://reffect.co.jp/react/next-auth#Google_Cloud_PlatfomGCP

Discussion

kage1020kage1020

大変たいへん記事きじ参考さんこうになりました.

1つ補足ほそくですが,@v4.15.0(2022/10/24)からかくプロバイダーにおいてallowDangerousEmailAccountLinkingというプロパティを設定せっていできるようになったらしいです.

GithubProvider({
  clientId: process.env.GITHUB_ID,
  clientSecret: process.env.GITHUB_SECRET,
  allowDangerousEmailAccountLinking: true,
}),

これは,重複じゅうふくするメールアドレスでSignUpしようとしたときUserAccountをリンクするかをえらべる設定せっていで,アダプターをカスタマイズする以降いこうをまるっとってくれるようです.
名前なまえにdangerousとついているのですこ躊躇ちゅうちょしますが,実装じっそう大変たいへんときにはこちらを使つかうのもありですね.