Auth.jsを完全に理解する (Next.js App Router 実装編) #2


# はじめに

この記事はAuth.jsがどのようなものか，どのように実装すればいいかなどをドキュメントを要約しながら紹介するものです．

https://authjs.dev/

:::note warn
**2024/02/19 追記**
Auth.jsはv5で破壊的変更がありました．新たにAuth.jsを学ぶ場合には，特に理由がない限りv5を使うことをお勧めします．v5の記事は近日中に出します．

Auth.jsはドキュメント整備中です．現在のドキュメントとは内容が異なる場合があります．この記事では旧ドキュメントの内容も交えて解説しています．
:::

今回はNext.js App Router実装編です．前回の記事はこちら

https://qiita.com/kage1020/items/fca49e9b42b972b70b8c

# 環境構築

JWTを用いたOAuth認証を行うためのNext.js環境を構築します．面倒であれば以下にテンプレートを用意しましたのでご自由にお使いください．

https://github.com/kage1020/NextStackApp

まず，Next.jsを作成します．以下のコマンドを実行し，質問に答えながら構築します．

```
npx create-next-app@latest
```

```
√ What is your project named? ... auth-app
√ Would you like to use TypeScript with this project? ... Yes
√ Would you like to use ESLint with this project? ... Yes
√ Would you like to use Tailwind CSS with this project? ... Yes
√ Would you like to use `src/` directory with this project? ... Yes
√ Use App Router (recommended)? ... Yes
√ Would you like to customize the default import alias? ... No
√ What import alias would you like configured? ... @/*
```

次に，NextAuth.js(Auth.js)をいれます．

```
npm install next-auth
```

# 実装

`src/app/api/auth/[...nextauth]/route.ts`を作成します．

```ts:src/app/api/auth/[...nextauth]/route.ts
import NextAuth from 'next-auth'
import GithubProvider from 'next-auth/providers/github'

export const authOptions = {
  secret: process.env.NEXTAUTH_SECRET,
  providers: [
    GithubProvider({
      clientId: process.env.GITHUB_CLIENT_ID ?? '',
      clientSecret: process.env.GITHUB_CLIENT_SECRET ?? '',
    }),
  ],
}

const handler = NextAuth(authOptions)

export { handler as GET, handler as POST }
```

これは，Next.jsの機能Dynamic Routesを利用して`/api/auth/*`のリクエストをすべてこの`route.ts`で処理するためのファイルです．

https://nextjs.org/docs/app/building-your-application/routing/dynamic-routes

Auth.jsは認証周りの機能をAPI Routes(サーバー)で処理するため，GETやPOSTを処理するための`handler`を名前付きでエクスポートします．ここで処理するリクエストは以下のとおりです．

<details>
<summary>リクエスト一覧</summary>

|http method|path|description|
|-|-|-|
|GET|`/api/auth/signin`|サインページの取得|
|POST|`/api/auth/signin/:provider`|各プロバイダにサインインリクエストを送る|
|GET/POST|`/api/auth/callback/:provider`|各プロバイダからサインイン結果が返ってくる|
|GET|`/api/auth/signout`|サインアウトページの取得|
|POST`|`/api/auth/signout`|サインアウト処理|
|GET|`/api/auth/session`|セッション情報の取得|
|GET|`/api/auth/csrf`|CSRFトークンの取得|
|GET|`/api/auth/providers`|サインインできるプロバイダ一覧の取得|
</details>

また，Auth.jsをクライアントで使えるようにするために，`src/providers/NextAuth.tsx`を作成します．

```tsx:src/providers/NextAuth.tsx
'use client'

import { SessionProvider } from 'next-auth/react'
import { ReactNode } from 'react'

const NextAuthProvider = ({ children }: { children: ReactNode }) => {
  return <SessionProvider>{children}</SessionProvider>
}

export default NextAuthProvider
```

`SessionProvider`は`useContext`を含むのでClient Componentsにしなければいけないことに注意です．
あとはこれを`src/app/layout.tsx`で読み込みます．

```tsx:src/app/layout.tsx
import './globals.css'
import { Inter } from 'next/font/google'
import NextAuthProvider from '@/providers/NextAuth'

const inter = Inter({ subsets: ['latin'] })

export const metadata = {
  title: 'Create Next App',
  description: 'Generated by create next app',
}

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang='en'>
      <body className={inter.className}>
        <NextAuthProvider>{children}</NextAuthProvider>
      </body>
    </html>
  )
}
```

次に，以下のOAuthアプリの作成に従って認可機構を作ります．入力内容で注意したいのは`callback URL`です．必ず`http://localhost:3000/api/auth/callback/github`のように`/api/auth/callback/github`をオリジンURLの後ろに入れましょう．

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

![github-app-callbackurl](https://authjs.dev/assets/images/getting-started-oauth-callback-url-2b1a1e64269f012fb7c26017a375118d.png)
(<https://authjs.dev> より)

すると，`client ID`が表示されます．また，その少し右下に`Generate a new client secret`というボタンがあるので，そこをクリックすると`Client Secret`が生成されます．

この`Client ID`と`Client Secret`2つをコピーします．

![github-app-clientsecret](https://authjs.dev/assets/images/getting-started-oauth-clientid-secret-b26acc4526886be919c35bbd486d8001.png)
(<https://authjs.dev> より)

プロジェクトルートに`.env`ファイルを作成し，先ほどコピーした2つの値を以下のように貼り付けます．

```.env
NEXTAUTH_URL="http://localhost:3000"
NEXTAUTH_SECRET="secret"

GITHUB_CLIENT_ID="12345"
GITHUB_CLIENT_SECRET="67890"
```

また，`NEXTAUTH_URL`と`NEXTAUTH_SECRET`も設定しましょう．前者に関してはコピペでいいですが，後者は別の値を使うことを推奨します．この値はJWTを暗号化するのに使われるなど秘密鍵としての役割を担うので以下のように生成するなど対策を施してください．

```
$ openssl rand -base64 32
```

または， <https://generate-secret.vercel.app/32> にアクセスして生成してもいいです．

これで設定は終了です．`src/app/page.tsx`でセッションを確認してみましょう．

<details><summary>クライアントサイドでのセッション確認</summary>

```tsx:src/app/page.tsx
'use client';

import Image from 'next/image';
import { useSession, signIn, signOut } from 'next-auth/react';

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

  return (
    <main className='flex min-h-screen flex-col items-center justify-between p-24'>
      <div className='z-10 max-w-5xl w-full items-center justify-between font-mono text-sm lg:flex'>
        <p className='fixed left-0 top-0 flex w-full justify-center border-b border-gray-300 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl dark:border-neutral-800 dark:bg-zinc-800/30 dark:from-inherit lg:static lg:w-auto  lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4 lg:dark:bg-zinc-800/30'>
          Hello&nbsp;
          <code className='font-mono font-bold'>{session?.user?.name ?? 'guest'}</code>
        </p>
        {!session && (
          <button
            onClick={() => signIn()}
            className='flex w-full justify-center border-2 border-b border-red-300 hover:border-red-400 bg-red-200 hover:bg-red-300 pb-6 pt-8 backdrop-blur-2xl dark:border-red-800 dark:hover:border-red-900 dark:bg-red-800/50 dark:hover:bg-red-900/30 lg:static lg:w-auto lg:rounded-xl lg:p-4'
          >
            Sign In
          </button>
        )}
        {session && (
          <button
            onClick={() => signOut()}
            className='flex w-full justify-center border-2 border-b border-green-300 bg-green-200 pb-6 pt-8 backdrop-blur-2xl dark:border-green-800 dark:bg-green-800/30 lg:static lg:w-auto lg:rounded-xl lg:p-4 hover:border-green-400 hover:bg-green-300 dark:hover:border-green-900 dark:hover:bg-green-900/30'
          >
            Sign Out
          </button>
        )}
        <div className='fixed bottom-0 left-0 flex h-48 w-full items-end justify-center bg-gradient-to-t from-white via-white dark:from-black dark:via-black lg:static lg:h-auto lg:w-auto lg:bg-none'>
          <a
            className='pointer-events-none flex place-items-center gap-2 p-8 lg:pointer-events-auto lg:p-0'
            href='https://vercel.com?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app'
            target='_blank'
            rel='noopener noreferrer'
          >
            By{' '}
            <Image
              src='/vercel.svg'
              alt='Vercel Logo'
              className='dark:invert'
              width={100}
              height={24}
              priority
            />
          </a>
        </div>
      </div>

      <div className="relative flex place-items-center before:absolute before:h-[300px] before:w-[480px] before:-translate-x-1/2 before:rounded-full before:bg-gradient-radial before:from-white before:to-transparent before:blur-2xl before:content-[''] after:absolute after:-z-20 after:h-[180px] after:w-[240px] after:translate-x-1/3 after:bg-gradient-conic after:from-sky-200 after:via-blue-200 after:blur-2xl after:content-[''] before:dark:bg-gradient-to-br before:dark:from-transparent before:dark:to-blue-700 before:dark:opacity-10 after:dark:from-sky-900 after:dark:via-[#0141ff] after:dark:opacity-40 before:lg:h-[360px] z-[-1]">
        <Image
          className='relative dark:drop-shadow-[0_0_0.3rem_#ffffff70] dark:invert'
          src='/next.svg'
          alt='Next.js Logo'
          width={180}
          height={37}
          priority
        />
      </div>

      <div className='mb-32 grid text-center lg:max-w-5xl lg:w-full lg:mb-0 lg:grid-cols-4 lg:text-left'>
        <a
          href='https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app'
          className='group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30'
          target='_blank'
          rel='noopener noreferrer'
        >
          <h2 className={`mb-3 text-2xl font-semibold`}>
            Docs{' '}
            <span className='inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none'>
              -&gt;
            </span>
          </h2>
          <p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
            Find in-depth information about Next.js features and API.
          </p>
        </a>

        <a
          href='https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app'
          className='group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30'
          target='_blank'
          rel='noopener noreferrer'
        >
          <h2 className={`mb-3 text-2xl font-semibold`}>
            Learn{' '}
            <span className='inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none'>
              -&gt;
            </span>
          </h2>
          <p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
            Learn about Next.js in an interactive course with&nbsp;quizzes!
          </p>
        </a>

        <a
          href='https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app'
          className='group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30'
          target='_blank'
          rel='noopener noreferrer'
        >
          <h2 className={`mb-3 text-2xl font-semibold`}>
            Templates{' '}
            <span className='inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none'>
              -&gt;
            </span>
          </h2>
          <p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
            Explore starter templates for Next.js.
          </p>
        </a>

        <a
          href='https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app'
          className='group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30'
          target='_blank'
          rel='noopener noreferrer'
        >
          <h2 className={`mb-3 text-2xl font-semibold`}>
            Deploy{' '}
            <span className='inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none'>
              -&gt;
            </span>
          </h2>
          <p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
            Instantly deploy your Next.js site to a shareable URL with Vercel.
          </p>
        </a>
      </div>
    </main>
  );
}
```

</details>

<details><summary>サーバーサイドでのセッション確認</summary>

```tsx:src/app/page.tsx
import Image from 'next/image'
import {getServerSession} from 'next-auth'
import { authOptions } from '@/api/auth/[..nextauth]/route'
import SessionTip from '@/components/SessionTip'

export default async function Home() {
  const session = await getServerSession(authOptions)

  return (
    <main className='flex min-h-screen flex-col items-center justify-between p-24'>
      <div className='z-10 max-w-5xl w-full items-center justify-between font-mono text-sm lg:flex'>
        <p className='fixed left-0 top-0 flex w-full justify-center border-b border-gray-300 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl dark:border-neutral-800 dark:bg-zinc-800/30 dark:from-inherit lg:static lg:w-auto  lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4 lg:dark:bg-zinc-800/30'>
          Hello&nbsp;
          <code className='font-mono font-bold'>{session?.user?.name ?? 'guest'}</code>
        </p>
        <SessionTip session={session} />
        <div className='fixed bottom-0 left-0 flex h-48 w-full items-end justify-center bg-gradient-to-t from-white via-white dark:from-black dark:via-black lg:static lg:h-auto lg:w-auto lg:bg-none'>
          <a
            className='pointer-events-none flex place-items-center gap-2 p-8 lg:pointer-events-auto lg:p-0'
            href='https://vercel.com?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app'
            target='_blank'
            rel='noopener noreferrer'
          >
            By{' '}
            <Image
              src='/vercel.svg'
              alt='Vercel Logo'
              className='dark:invert'
              width={100}
              height={24}
              priority
            />
          </a>
        </div>
      </div>

      <div className="relative flex place-items-center before:absolute before:h-[300px] before:w-[480px] before:-translate-x-1/2 before:rounded-full before:bg-gradient-radial before:from-white before:to-transparent before:blur-2xl before:content-[''] after:absolute after:-z-20 after:h-[180px] after:w-[240px] after:translate-x-1/3 after:bg-gradient-conic after:from-sky-200 after:via-blue-200 after:blur-2xl after:content-[''] before:dark:bg-gradient-to-br before:dark:from-transparent before:dark:to-blue-700 before:dark:opacity-10 after:dark:from-sky-900 after:dark:via-[#0141ff] after:dark:opacity-40 before:lg:h-[360px] z-[-1]">
        <Image
          className='relative dark:drop-shadow-[0_0_0.3rem_#ffffff70] dark:invert'
          src='/next.svg'
          alt='Next.js Logo'
          width={180}
          height={37}
          priority
        />
      </div>

      <div className='mb-32 grid text-center lg:max-w-5xl lg:w-full lg:mb-0 lg:grid-cols-4 lg:text-left'>
        <a
          href='https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app'
          className='group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30'
          target='_blank'
          rel='noopener noreferrer'
        >
          <h2 className={`mb-3 text-2xl font-semibold`}>
            Docs{' '}
            <span className='inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none'>
              -&gt;
            </span>
          </h2>
          <p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
            Find in-depth information about Next.js features and API.
          </p>
        </a>

        <a
          href='https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app'
          className='group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30'
          target='_blank'
          rel='noopener noreferrer'
        >
          <h2 className={`mb-3 text-2xl font-semibold`}>
            Learn{' '}
            <span className='inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none'>
              -&gt;
            </span>
          </h2>
          <p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
            Learn about Next.js in an interactive course with&nbsp;quizzes!
          </p>
        </a>

        <a
          href='https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app'
          className='group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30'
          target='_blank'
          rel='noopener noreferrer'
        >
          <h2 className={`mb-3 text-2xl font-semibold`}>
            Templates{' '}
            <span className='inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none'>
              -&gt;
            </span>
          </h2>
          <p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
            Explore starter templates for Next.js.
          </p>
        </a>

        <a
          href='https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app'
          className='group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30'
          target='_blank'
          rel='noopener noreferrer'
        >
          <h2 className={`mb-3 text-2xl font-semibold`}>
            Deploy{' '}
            <span className='inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none'>
              -&gt;
            </span>
          </h2>
          <p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
            Instantly deploy your Next.js site to a shareable URL with Vercel.
          </p>
        </a>
      </div>
    </main>
  )
}
```
```tsx:/src/components/SessionTip.tsx
'use client'

import { signIn, signOut } from 'next-auth/react'
import type { Session } from 'next-auth'

export default function SessionTip({ session }:{session: Session | null}) {
  if (!session) return (
    <button
      onClick={() => signIn()}
      className='flex w-full justify-center border-2 border-b border-red-300 hover:border-red-400 bg-red-200 hover:bg-red-300 pb-6 pt-8 backdrop-blur-2xl dark:border-red-800 dark:hover:border-red-900 dark:bg-red-800/50 dark:hover:bg-red-900/30 lg:static lg:w-auto lg:rounded-xl lg:p-4'
    >
      Sign In
    </button>
  )

  return (
    <button
      onClick={() => signOut()}
      className='flex w-full justify-center border-2 border-b border-green-300 bg-green-200 pb-6 pt-8 backdrop-blur-2xl dark:border-green-800 dark:bg-green-800/30 lg:static lg:w-auto lg:rounded-xl lg:p-4 hover:border-green-400 hover:bg-green-300 dark:hover:border-green-900 dark:hover:bg-green-900/30'
    >
      Sign Out
    </button>
  )
}
```

</details>


サインイン前
![Web キャプチャ_11-6-2023_222739_localhost.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2274081/2db98bb4-8a35-758a-8372-60912bfbf318.jpeg)

サインイン後
![Web キャプチャ_11-6-2023_22282_localhost.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2274081/950bfda5-5619-44f8-46d6-57fa4c45840c.jpeg)

無事サインイン情報によってビューが変化しています．

# デプロイ

基本どこでもデプロイできますが，今回はVercelにデプロイしてみます．

[Vercel](https://vercel.com/)アカウントを作成したうえで[こちら](https://vercel.com/new/clone)にアクセスして，Githubアカウントと連携します．すると，レポジトリ一覧が表示されるのでデプロイしたいレポジトリを選択します．

選択をすると，以下の画像のような画面になるので，`.env`ファイルに書いてある内容をコピペします．

![Web キャプチャ_17-6-2023_224457_vercel.com.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2274081/db1be5c3-6874-bb7f-387f-8b544672b51c.jpeg)

後はDeployボタンを押すと無事デプロイが完了します．

# API等具体的な内容

## Session情報

`useSession`を使って得られるセッション情報は以下のとおりです．

```
{
  user?: {
    name?: string
    email?: string
    image?: string
  },
  expires: Date // This is the expiry of the session, not any of the tokens within the session
}
```

この値はセキュリティを考慮してプロバイダから得られる情報の中のごく一部となっています．各ユーザー個別の情報が欲しい場合には，以下のように都度拡張することができます．

```ts:src/app/api/auth/[...nextauth]/route.ts
// jwtを利用する場合
const handler = NextAuth({
  session: {
    strategy: 'jwt', // default
  },
  callbacks: {
    jwt: async ({ token, account }) => {
      if (account) { // 初回サインイン時にアカウント情報を取得できる
        token.id = account.id
      }
      return token
    },
    session: async ({ session, token }) => {
      session.user.id = token.id // jwt関数でセットしたidをtoken.idで取得できる
      return session
    }
  }
})

// databaseを利用する場合
const handler = NextAuth({
  session: {
    strategy: 'database',
  },
  callbacks: {
    session: async ({ session, user }) => {
      session.user.id = user.id // `User`テーブルのフィールド値を取得できる
      return session
    }
  }
})
```

また，TypeScriptではinterfaceの拡張も必要です．

```ts:src/types/next-auth.d.ts
import NextAuth from 'next-auth'

import type { DefaultSession } from 'next-auth'
import type { JWT } from "next-auth/jwt"

declare module 'next-auth' {
  interface Session {
    user?: {
      id: string
    } & DefaultSession['user']
  }
}

declare module "next-auth/jwt" {
  interface JWT {
    id: string
  }
}
```

## 認証情報の取得

### `useSession`

何度も説明していますが，クライアント側でセッション情報を取得するhooksです．内部ではReactの`useContext`を使用しているのでクライアントコンポーネントでしか使えません．

```tsx
'use client'

export default function ProtectedPage() {
  const { data, status, update } = useSession({
    required: true, // SSRやCSRで有効
    onUnauthenticated: () => {
      // デフォルトではビルトインの`signIn`関数が呼ばれる
      // またサイン後にこのページに戻ってくるようcallbackUrlの設定も行われる
      // loggerやクライアントでの特別な処理をしたいときに有効
    }
  })

  return (
    ...
  )
}
```

`useSession`はオプショナルな引数として`required`と`onUnauthenticated`をとります．
また，返り値として`data`, `status`, `update`があります．前２つはセッションの状態によって次のような値をとります．

|状態|`data`|`status`|
|-|-|-|
|セッション取得中|`undefined`|`loading`|
|認証失敗|`null`|`unauthenticated`|
|認証成功|`Session`|`authenticated`|

`update`関数はセッション情報を即座(非同期)に変更したいときに使います．詳細は[こちら](#認証情報の更新)

`useSession`の欠点としてセッションを確認するたびに(ページが更新されたりフォーカスされたりしたとき)`/api/auth/session`にリクエストが飛びます．セッション情報自体はそんなにサイズは大きくありませんが処理に時間がかかる場合があります．RootLayoutなどで`useSession`を呼ぶ場合セッション情報が必要ない場面でも無駄なリクエストが増えてしまうためあまりよろしくないです．これを回避するため，以下のような方法が提案されています．

```tsx
// src/app/layout.tsx
export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <SessionProvider>
      {children.auth.role === 'admin' && (
        <Auth>{children}</Auth>
      )}
      {children.auth.role !== 'admin' && children}
    </SessionProvider>
  )
}

// src/components/auth.tsx
'use client'

function Auth({ children }: { children: ReactNode }) {
  const router = useRouter()
  const { status } = useSession({
    required: true,
    onUnauthenticated: () => router.push(children.unauthorized)
  })

  if (status === 'loading') return children.loading
  return children
}

// src/app/admin/page.tsx
export default function AdminPage() {
  ...
}

AdminPage.auth = {
  role: "admin",
  loading: <AdminLoadingSkeleton />,
  unauthorized: "/login-with-different-user", // redirect to this url
}
```

つまり，コンポーネント側で`Session`を使うかどうか定義しておいて必要な時に呼び出そうというわけです．できるなら，ページやレイアウト単位で分けるのではなくコンポーネント単位でセッション情報を取得すれば無駄なClient Componentsが増えなくてよいですね．

### `getServerSession`

サーバー側でセッション情報を取得するための関数です．この関数は`/api/auth/session`へリクエストするのではなく，リクエストヘッダやCookieなどからセッション情報を取得します．

```ts
// src/app/api/auth/[...nextauth]/route.ts
export const authOptions = {
  ...
}

// src/app/api/cart/route.ts
export const POST = async (req: Request) => {
  const session = await getServerSession(authOptions)
  ...
}
```

### `getCsrfToken`

各プロバイダでサインインをするときに必要なCross Site Request Forgery Tokenを取得するための関数です．自前でプロバイダを用いたサインインページを作成する場合に使います．

```ts
async function myFunction() {
  const csrfToken = await getCsrfToken()
  ...
}
```

`/api/auth/csrf`にGETしても取得できます．

### `getProviders`

自前のプロバイダを用いたサインインページを作る場合，サインインできるプロバイダリストを取得できます．

```ts
async function myFunction() {
  const providers = await getProviders()
  ...
}
```

```js:providers
{
  github: {
    id: "github",
    name: "GitHub",
    type: "oauth",
    signinUrl: "http://localhost:3000/api/auth/signin/github",
    callbackUrl: "http://localhost:3000/api/auth/callback/github"
  },
  ...
}
```

`/api/auth/providers`にGETしても取得できます．

### `signIn`, `signOut`

NextAuth.jsが標準で提供するサインイン・サインアウト関数です．

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

export default () => <button onClick={() => signIn()}>Sign in</button>
// or
export default () => (
  <button onClick={() => signIn("google")}>Sign in with Google</button>
)
// or
export default ({ email }) => (
  <button onClick={() => signIn("email", { email })}>Sign in with Email</button>
)

export default () => <button onClick={() => signOut()}>Sign out</button>
```

また，第2引数に認証後のリダイレクト先を指定することもできます．

```ts
signIn(undefined, { callbackUrl: 'http://localhost:3000/foo' })
signIn('email', { email, callbackUrl: 'http://localhost:3000/foo' })
signOut({ callbackUrl: 'http://localhost:3000/foo' })
```

emailとcredentialsでの認証のみにおいてリダイレクトをせずにPromiseを取得できます．

```ts
const res = signIn('credentials', { redirect: false, password: 'password' })

// resの中身
{
  error: string | undefined
  status: number
  ok: boolean
  url: string | null
}
```

## 認証情報の更新

クライアントサイドから認証情報の更新ができます．セッションにいろいろ情報を持たせているときに，リロードすることなくクライアントから変更のイベントを発火させられるので便利です．
引数なしで使うとデータベースの情報を取得してクライアントを新しいものに更新します．引数あり(セッションオブジェクトを渡す)で使うと以下の`jwt`または`session`が再度呼ばれるのでその中で更新を行います．

```tsx
import { useSession } from "next-auth/react"

export default function Page() {
  const { data: session, status, update } = useSession()

  if (status === "authenticated") {
    return (
      <>
        <p>Signed in as {session.user.name}</p>

        {/* Update the value by sending it to the backend. */}
        <button onClick={() => update({ name: "John Doe" })}>
          Edit name
        </button>
        {/*
          * Only trigger a session update, assuming you already updated the value server-side.
          * All `useSession().data` references will be updated.
          */}
        <button onClick={() => update()}>
          Edit name
        </button>
      </>
    )
  }

  return <a href="/api/auth/signin">Sign in</a>
}
```

クライアントから送信した情報はcallbacksの中で処理されます．

```ts:/app/api/auth/[...nextauth]/route.ts
export default NextAuth({
  callbacks: {
    // strategyがjwtの場合
    jwt({ token, trigger, session }) {
      if (trigger === "update" && session?.name) {
        // Note, that `session` can be any arbitrary object, remember to validate it!
        token.name = session.name
      }
      return token
    },
    // strategyがsessionの場合
    async session({ session, trigger, newSession }) {
      if (trigger === "update" && newSession?.name) {
        // await adapter.updateUser(session.user.id, { name: newSession.name })
        session.name = newSession.name
      }
      return session
    }
  }
})
```

`update()`は，後述の`SessionProvider.refetchInterval`や`SessionProvider.refetchOnWindowFocus`と機能は同じですが，複数タブ間で新しいセッションは共有されないので注意が必要です．

## `SessionProvider`

前述のとおり，セッション情報を`useContext`を用いて配信するためのプロバイダです．ここで配信されるセッション情報は複数のタブやウィンドウで同期されCookieに保存します．また，セッション情報を扱ううえでいくつかのオプションを設定できます．

```tsx
'use client'

import { SessionProvider } from "next-auth/react"

export default function NextAuthProvider({ children }: { children: ReactNode }) {
  return (
    <SessionProvider
      baseUrl="https://example.com"
      // In case you use a custom path and your app lives at "/cool-app" rather than at the root "/"
      basePath="cool-app"
      // Re-fetch session every 5 minutes
      refetchInterval={5 * 60}
      // Re-fetches session when window is focused
      refetchOnWindowFocus={true}
      // Set to `false` to stop polling when the device has no internet access offline (determined by `navigator.onLine`)
      refetchWhenOffline={false}
    >
      {children}
    </SessionProvider>
  )
}
```

ここで`baseUrl`や`basePath`を変更した場合は，`NEXTAUTH_URL`も変更しましょう．

## 環境変数

Auth.jsを使うにあたって設定可能な環境変数は以下のとおりです．

|変数名|説明|例|
|-|-|-|
|`NEXTAUTH_URL`|認証のためのエンドポイントのベースパス．Vercelにデプロイする場合のみ本番環境での設定は必須ではない|`https://example.com`|
|`NEXTAUTH_URL_INTERNAL`|認証のためのエンドポイントのベースパス．クライアントとサーバーでプロジェクトルートが違う場合にサーバー側の設定として利用される|`https://example.com`|
|`NEXTAUTH_SECRET`|JWTを暗号化するための秘密鍵などに使われる．[ここ](https://generate-secret.vercel.app/32)で生成できる|`17a1594cf715274bf3373f560cde609f`|

## 詳細設定

### `providers`

50個以上のプロバイダを用いて認証処理ができます．

42 School, Amazon Cognito, Apple, Atlassian, Auth0, Authentik, Azure Active Directory, Azure Active Directory B2C, Battle.net, Box, BoxyHQ SAML, Bungie, Coinbase, Discord, Dropbox, DuendeIdentityServer6, EVE Online, Facebook, FACEIT, Foursquare, Freshbooks, FusionAuth, GitHub, GitLab, Google, HubSpot, IdentityServer4, Instagram, Kakao, Keycloak, LINE, LinkedIn, Mail.ru, Mailchimp, Medium, Naver, Netlify, Okta, OneLogin, Osso, Osu!, Patreon, Pinterest, Pipedrive, Reddit, Salesforce, Slack, Spotify, Strava, Todoist, Trakt, Twitch, Twitter, United Effects, VK, Wikimedia, WordPress.com, WorkOS, Yandex, Zitadel, Zoho, Zoom,

```ts
import TwitterProvider from "next-auth/providers/"
...
providers: [
  TwitterProvider({
    clientId: process.env.TWITTER_ID,
    clientSecret: process.env.TWITTER_SECRET
  })
],
...
```

また，メールでの認証も可能です．ただし，データベースは必須です．

```ts
import EmailProvider from `next-auth/providers/email`
...
providers: [
  EmailProvider({
    id: "myEmail", // Unique ID for the provider
    name: myMail, // Descriptive name for the provider
    type: "email",
    server: process.env.EMAIL_SERVER,
    from: process.env.EMAIL_FROM,
    sendVerificationRequest: () => {
    // Callback to execute when a verification request is sent
    }
  }),
],
...
```

2要素認証などをする場合にはCredentialでの認証が便利です．

```ts
import CredentialsProvider from "next-auth/providers/credentials"
...
providers: [
  CredentialsProvider({
    // The name to display on the sign in form (e.g. 'Sign in with...')
    name: 'Credentials',
    id: "myCredential",
    type: "credentials",
    // You can pass any HTML attribute to the <input> tag through the object.
    credentials: {
      username: { label: "Username", type: "text", placeholder: "jsmith" },
      password: {  label: "Password", type: "password" }
    },
    async authorize(credentials, req) {
      const res = await fetch("/your/endpoint", {
        method: 'POST',
        body: JSON.stringify(credentials),
        headers: { "Content-Type": "application/json" }
      })
      const user = await res.json()

      // If no error and we have user data, return it
      if (res.ok && user) {
        return user
      }
      // Return null if user data could not be retrieved
      return null
    }
  })
]
...
```

`authorize`関数の`credentials`は直前に記述した`credentials`を参照できます．
また，`authorize`関数は`User`を返す必要があります．デフォルトでは`id`, `name`, `email`, `image`しか認識しないので，認証時に追加情報をもたせたい場合はカスタムプロバイダーで追加するか`callbacks`で都度追加しなければいけないことに注意してください．

https://authjs.dev/reference/adapters#models

### `secret`

トークンのハッシュ化，Cookieの署名/暗号化，秘密鍵の生成などに使われます．NEXTAUTH_SECRETを指定している場合，必須ではありません．linuxでは以下のコマンドで生成できます．

```
openssl rand -base64 32
```

:::note warn
`NEXTAUTH_SECRET`も`secret`も指定しない場合，開発環境では警告が，本番環境ではエラーが発生します．
:::

### `session`

セッション情報の取り扱い方の設定ができます．

```ts
session: {
  // The default is `"jwt"`, an encrypted JWT (JWE) stored in the session cookie.
  // If you use an `adapter` however, we default it to `"database"` instead.
  strategy: "database",

  // Seconds - How long until an idle session expires and is no longer valid.
  maxAge: 30 * 24 * 60 * 60, // 30 days

  // Seconds - Throttle how frequently to write to database to extend a session.
  // Use it to limit write operations. Set to 0 to always update the database.
  // Note: This option is ignored if using JSON Web Tokens
  updateAge: 24 * 60 * 60, // 24 hours

  // The session token is usually either a random UUID or string, however if you
  // need a more customized session token string, you can define your own generate function.
  generateSessionToken: () => {
    return randomUUID?.() ?? randomBytes(32).toString("hex")
  }
}
```

### `jwt`

jwtモードにおけるセッションの取り扱い方の設定ができます．

```ts
jwt: {
  // The maximum age of the NextAuth.js issued JWT in seconds.
  // Defaults to `session.maxAge`.
  maxAge: 60 * 60 * 24 * 30,
  // You can define your own encode/decode functions for signing and encryption
  async encode() {},
  async decode() {},
}
```

### `pages`

通常，サインインや認証エラーページはAuth.jsによって自動生成されます．カスタムページで認証を行いたい場合はpagesを設定します．

```ts
...
  pages: {
    signIn: '/auth/signin',
    signOut: '/auth/signout',
    error: '/auth/error', // Error code passed in query string as ?error=
    verifyRequest: '/auth/verify-request', // (used for check email message)
    newUser: '/auth/new-user' // New users will be directed here on first sign in (leave the property out if not of interest)
  }
...
```

### `theme`

デフォルトテーマはシステム依存ですが，強制的にライトテーマやブランドカラーに変更することができます．

```ts
theme: {
  colorScheme: "auto", // "auto" | "dark" | "light"
  brandColor: "", // Hex color code
  logo: "" // Absolute URL to image
}
```

### `callbacks`

```ts:例
...
  callbacks: {
    async signIn({ user, account, profile, email, credentials }) {
      const isAllowedToSignIn = ...
        if (isAllowedToSignIn) {
          return true
        } else {
          // Return false to display a default error message
          return false
          // Or you can return a URL to redirect to:
          // return '/unauthorized'
        }
      }
    },
    async redirect({ url, baseUrl }) {
      // Allows relative callback URLs
      if (url.startsWith("/")) return `${baseUrl}${url}`
      // Allows callback URLs on the same origin
      else if (new URL(url).origin === baseUrl) return url
      return baseUrl
    },
    async session({ session, user, token, newSession, trigger }) {
      if (account) {
        token.accessToken = account.access_token
        token.id = profile.id
      }
      return token
    },
    async jwt({ token, user, account, profile, trigger, isNewUser, session }) {
      // Send properties to the client, like an access_token and user id from a provider.
      session.accessToken = token.accessToken
      session.user.id = token.id

      return session
    }
...
}
```

`signIn`はユーザーのサインインを許可するかを判定します．許可する場合には`true`を，許可しない場合には`false`またはURL文字列を返します．
`redirect`はcallback URLへリダイレクトされるたびに呼ばれます．

### `events`

外部のロギングサービスへはこのオプションから送ることができます．

```ts
events: {
  async signIn(message) { /* on successful sign in */ },
  async signOut(message) { /* on signout */ },
  async createUser(message) { /* user created */ },
  async updateUser(message) { /* user updated - e.g. their email was verified */ },
  async linkAccount(message) { /* account (e.g. Twitter) linked to a user */ },
  async session(message) { /* session is active */ },
}
```

### `adapter`

データベースやORMと連携するための関数を指定できます．また，自作したアダプタも指定できます．

```ts
import { type Adapter } from "@auth/core/adapters"

const MyAdapter: Adapter {
 // implement the adapter methods here
}

export default NextAuth({
  ...
  adapter: MyAdapter,
})
```

### `debug`

デバッグモードです．ユーザーの生情報がターミナルに出力される場合があるので本番環境では使ってはいけません．

```ts
debug: process.env.NODE_ENV !== 'production'
```

### `logger`

ログをレベル別で取得できます．こちらも外部のロギングサービスと連携できます．

```ts
import log from "logging-service"

export default NextAuth({
  ...
  logger: {
    error(code, metadata) {
      log.error(code, metadata)
    },
    warn(code) {
      log.warn(code)
    },
    debug(code, metadata) {
      log.debug(code, metadata)
    }
  }
  ...
})
```

# おわりに

以上がNext.js App RouterでのAuth.jsの実装方法と具体的な設定項目の説明でした．