• トップ
  • ブログ一覧
  • ソースコードから理解するNextAuth.jsのログイン処理
  • ソースコードから理解するNextAuth.jsのログイン処理

    こま(エンジニア)こま(エンジニア)
    2025.02.28

    IT技術

    概要

    NextAuth.jsを雰囲気で使っているので、ソースコードを読んで理解を深めていきます。

    ゴール

    設定をカスタマイズすることで処理を構築するNextAuth.jsが何をしているのか、ソースコードから明確にすることを目指します。
    全体像が見えてくれば、NextAuth.jsの設定を迷うことなく書けるようになるはずです。

    環境

    • Next.js: 15.1.6
    • NextAuth.js: 4.24.11

    用語

    • authOptions: 認証に関する設定値を記述したオブジェクト 本記事では、lib/authOptions.tsに書いたオブジェクトを指す
    • Credentials: いわゆるID・パスワード認証

    記事の構成

    最初に、ログインの簡単なサンプルからNextAuth.jsにおける認証処理の書き方を見ておきます。
    続いて、処理の特徴や公式ドキュメントの構成も踏まえ、何が分からないことがNextAuth.jsを難しくしているのか整理します。

    そして、分からないことを解消する材料を集めにソースコードを読んでいきます。
    つまり、最初に目的を見据えてからソースコードを読み進めることで、NextAuth.jsを理解するためのステップを確実に踏んでいくことを目指します。

    ログイン処理のサンプル

    まずはアプリケーションの認証処理をNextAuth.jsでどう実現するのか、簡単なサンプルを示します。
    今回はシンプルにユーザ名とパスワードでログインすることにします。

    けっこう長い処理なので、ひとまずどんなファイルがあるのかだけ掴めれば十分です。

    設定(src/lib/authOptions.ts

    1import CredentialsProvider from "next-auth/providers/credentials";
    2import {NextAuthOptions} from "next-auth";
    3
    4// ログイン処理の設定を記述
    5export const authOptions: NextAuthOptions = {
    6    // プロバイダ
    7    // ユーザの本人確認を責務とする
    8    providers: [
    9        // いわゆるID・パスワード方式
    10        // OAuthなども使えるが、今回はシンプルに扱えるクレデンシャル方式を採用
    11        CredentialsProvider({
    12            // ログインのタイミングで呼ばれる関数
    13            // ここで画面の入力情報が特定のユーザと紐づくか、つまり認証の成否を判定
    14            authorize(credentials, req) {
    15                // (後述)
    16                // credentialsオブジェクトのプロパティは、signIn()で渡された引数と対応
    17                if (!credentials?.username || !credentials?.password) {
    18                    return null;
    19                }
    20                const {username, password} = credentials;
    21
    22                // バックエンドなどと通信し、認証
    23                // ex:
    24                // await login(username, password);
    25
    26                return {
    27                    id: "USERID",
    28                    name: username
    29                }
    30            },
    31            // 本来は、このオプションからログイン画面を組み立てるときに参照されるプロパティ
    32            // ただし、authorize関数の引数credentialsのキー名がここで定義したものに限定されるので、名前だけ設定しておく
    33            credentials: {username: {}, password: {}},
    34        })
    35    ],
    36    // トークンをハッシュ化させたりなど、機密情報を生成するのに使うランダムな文字列
    37    secret: process.env.AUTH_SECRET,
    38    // 特定のタイミングでNextAuth.jsから呼ばれる処理
    39    callbacks: {
    40        // JWTトークンを生成/更新する準備が整ったときに呼ばれる
    41        // トークンに何を含めるか
    42        async jwt({token, user}) {
    43            if (user?.name) {
    44                token.name = user.name
    45            }
    46            return token;
    47        },
    48    }
    49}

    Route Handler(src/app/api/auth/[...nextauth]/route.ts)

    1import NextAuth from "next-auth";
    2import {authOptions} from "@/lib/authOptions";
    3
    4// サーバ側でログイン処理を実行するためのエンドポイント
    5const handler = NextAuth(authOptions);
    6
    7export {handler as GET, handler as POST};

    参考

    ログイン処理呼び出し(src/app/sign-in/page.tsx)

    1'use client'
    2import {signIn} from "next-auth/react";
    3
    4// ログイン画面 ボタンを押したらログイン成功画面へ遷移
    5export default function SignInPage() {
    6
    7    const handleSignIn = () => {
    8        // 第一引数は認証プロバイダ名 プロバイダによってリダイレクトなどの制御が異なるので指定が必要
    9        signIn('credentials', {
    10            // ログイン処理後のリダイレクト先
    11            callbackUrl: '/welcome',
    12            // 以降はクレデンシャル情報
    13            username: 'Snivy',
    14            password: 'strongPassword',
    15        });
    16    };
    17
    18    return (
    19        <div>
    20            <button onClick={handleSignIn}>
    21                ログイン
    22            </button>
    23        </div>
    24    );
    25}

    参考


    NextAuth.jsの難しさ

    さて、アプリケーションで書いたNextAuth.js向けのコードを見ると、オプションがたくさん書かれていて難しそうです。
    これらのオプションを適切に組み合わせることで、NextAuth.jsの提供する認証処理は完成します。

    ですが、公式ドキュメントにはサンプルがほとんどなく、部分に対する解説しかありません。
    つまり、ドキュメントを読んでもどのオプションをいつ・どのように使えば良いのか全体像を理解するのが困難でした。

    まとめると、十分なサンプルが提供されておらず、オプションの使い方が見えてこないことで
    NextAuth.jsが難しく感じているように思えました。

    ソースコードリーディング

    ドキュメントから使い方を読めないのであれば、ソースコードを解読するのが手っ取り早いです。
    それぞれのオプションがどこでどう使われているのか、ソースコードから読み解くことができれば、NextAuth.jsの使い方も見えてきそうです。

    ソースコードを読むゴール

    フレームワークやライブラリなど、膨大なソースコードを読みたくなった場合は、読むゴールを見据えるのが重要です。
    ゴールを定めておくことで、たくさんのコードからどこに絞って見ていけば良いか掴めてきます。

    ということで改めて、本記事でNextAuth.jsのソースコードを読むゴールは、アプリケーションで記述する設定値が
    いつ・どこでどう使われるのか明確にすることを目指します。

    設定値が使われていそうな処理を重点的に読んでいくことにします。

    エントリーポイント

    一連の処理を理解したいときは、入口から始めると枠組みを掴みやすいです。
    ということで、アプリケーションが最初に呼び出すNextAuth.jsの関数である、signIn()から見ていきます。

    signIn()

    復習がてら、signIn()を呼び出しているところを再掲します。

    1// 第一引数は認証プロバイダ名 プロバイダによってリダイレクトなどの制御が異なるので指定が必要
    2signIn('credentials', {
    3    // ログイン処理後のリダイレクト先
    4    callbackUrl: '/welcome',
    5    // 以降はクレデンシャル情報
    6    username: 'Snivy',
    7    password: 'strongPassword',
    8});

    コード量がけっこうあるので、まずは認証処理に着目して全体像を掴んでおきます。

    1// nextauthjs/next-auth/packages/next-auth/src/react/index.tsx
    2
    3// 宣言で見るべきは、どんな引数を受け取っているか
    4// 重要なのは引数provider(文字列), options(オブジェクト)
    5/*
    6 * Client-side method to initiate a signin flow
    7 * or send the user to the signin page listing all possible providers.
    8 * Automatically adds the CSRF token to the request.
    9 *
    10 * [Documentation](https://next-auth.js.org/getting-started/client#signin)
    11 */
    12export async function signIn<
    13  P extends RedirectableProviderType | undefined = undefined
    14>(
    15  provider?: LiteralUnion<
    16    P extends RedirectableProviderType
    17      ? P | BuiltInProviderType
    18      : BuiltInProviderType
    19  >,
    20  options?: SignInOptions,
    21  authorizationParams?: SignInAuthorizationParams
    22): Promise<
    23  P extends RedirectableProviderType ? SignInResponse | undefined : undefined
    24> {
    25  const { callbackUrl = window.location.href, redirect = true } = options ?? {}
    26
    27  // デフォルト値は、「http://localhost:3000/api/auth」
    28  // つまり、RouteHandlerのURLを取得
    29  const baseUrl = apiBaseUrl(__NEXTAUTH)
    30  // プロバイダに応じて処理をカスタマイズするために、事前にRoute Handlerから設定値を取得
    31  const providers = await getProviders()
    32
    33  // 中略...
    34
    35  // クレデンシャル方式なので、ログインURLは、`${baseUrl}/callback/credentials`となる
    36  const isCredentials = providers[provider].type === "credentials"
    37  const isEmail = providers[provider].type === "email"
    38  const isSupportingReturn = isCredentials || isEmail
    39
    40  const signInUrl = `${baseUrl}/${
    41    isCredentials ? "callback" : "signin"
    42  }/${provider}`
    43
    44  const _signInUrl = `${signInUrl}${authorizationParams ? `?${new URLSearchParams(authorizationParams)}` : ""}`
    45
    46  // RouteHandlerへ処理を委譲
    47  // ポイントは、bodyプロパティにoptions, つまりsignIn()を呼び出したときの引数を渡していること
    48  const res = await fetch(_signInUrl, {
    49    method: "post",
    50    headers: {
    51      "Content-Type": "application/x-www-form-urlencoded",
    52    },
    53    // @ts-expect-error
    54    body: new URLSearchParams({
    55      ...options,
    56      csrfToken: await getCsrfToken(),
    57      callbackUrl,
    58      json: true,
    59    }),
    60  })
    61
    62// 中略...

    ざっくり眺めていると、どうやらRoute Handlerへ認証処理を委譲しているようです。
    大量の処理が書かれていて難しそうなので、ひとまずここでは以下の点を押さえておきます。

    • Route Handlerを呼び出すときのURLは、/api/auth/callback/credentials
    • Route Handlerにはリクエストボディ経由でsignIn()の引数を渡している

    Route Handler

    続いて、認証処理の中核を担うRoute Handlerを見ていきます。

    1// nextauthjs/next-auth/packages/next-auth/src/next/index.ts
    2
    3// @see https://beta.nextjs.org/docs/routing/route-handlers
    4async function NextAuthRouteHandler(
    5  req: NextRequest,
    6  context: RouteHandlerContext,
    7  options: AuthOptions
    8) {
    9  // 中略...
    10  const { headers, cookies } = require("next/headers")
    11  // URL(/api/auth/callback/credentials)のauth以降のパス部分
    12  const nextauth = (await context.params)?.nextauth
    13
    14  const query = Object.fromEntries(req.nextUrl.searchParams)
    15  const body = await getBody(req)
    16
    17  // 認証処理本体
    18  // これを解読することがゴールへ近づくための肝
    19  const internalResponse = await AuthHandler({
    20    req: {
    21      body,
    22      query,
    23      cookies: Object.fromEntries(
    24        (await cookies()).getAll().map((c) => [c.name, c.value])
    25      ),
    26      headers: Object.fromEntries((await headers()) as Headers),
    27      method: req.method, // GET or POST
    28      action: nextauth?.[0] as AuthAction, // 認証処理で何をするか 今回はcallback
    29      providerId: nextauth?.[1],  // クレデンシャル
    30      error: query.error ?? nextauth?.[1],
    31    },
    32    options, // providers, secret, callbacksなどを事前に書いた設定値オブジェクト(サンプルのlib/authOptions.ts)
    33  })
    34
    35  const response = toResponse(internalResponse)
    36  // 中略...
    37  return response
    38}

    Route Handler自体は、リクエストを受け取ってレスポンスを返す役割を持つシンプルなものです。
    肝心の認証処理はAuthHandler()が責務を持っているようです。

    何を引数として渡しているかをコメントからざっくり掴めたら、AuthHandler()の中身を見ていきます。

    補足: NextAuth()

    1// app/api/auth/[...nextauth]/route.ts
    2
    3import NextAuth from "next-auth";
    4import {authOptions} from "@/lib/authOptions";
    5
    6// Route Handlerで書いたNextAuth関数は何者か?
    7const handler = NextAuth(authOptions);
    8
    9export {handler as GET, handler as POST};

    Route HandlerではNextAuthという関数を呼び出していました。せっかくなので、この関数の責務も知っておきたいです。

    これは先ほど見たNextAuthRouteHandler()を後で呼び出せるようにするのが主な責務なのですが、もう一つ大きな役割を持っています。
    Next.jsではAPIの記述方法がAPI Route, Route Handlerの二種類あります。
    NextAuth.jsではどちらにも対応できるよう、NextAuth関数で呼び出す対象を判断しています。

    公式ドキュメントでも双方がごちゃまぜに書かれていて混乱しやすかったので、
    なぜ混ざっているのか背景を理解するためにも軽く補足を書きました。

    AuthHandler

    名前の通り、認証処理を担当しています。
    設定値をもとにどうやって認証処理を実現しているのか、ここからじっくり読み解いていきます。

    前半: 設定値の整理

    設定内容はauthOptions, signIn()の引数, NextAuth.jsがRoute Handlerを呼び出すときに渡したパラメータなど多くの種類があります。

    これをばらばらの状態で扱っていると見通しが悪くなるので、optionsという一つのオブジェクトにまとめるのが前半の処理のポイントです。

    1// nextauthjs/next-auth/packages/next-auth/src/core/index.ts
    2
    3// Route Handlerで渡したオブジェクト(params)を受け取っていることだけ読み解けばOK
    4export async function AuthHandler<
    5  Body extends string | Record<string, any> | any[]
    6>(params: NextAuthHandlerParams): Promise<ResponseInternal<Body>> {
    7
    8  // optionsはauthOptions.tsで書いた設定値
    9  const { options: authOptions, req: incomingRequest } = params
    10
    11  // 中略...
    12
    13  // action: callback
    14  // providerId: credentials
    15  // method: POST
    16  const { action, providerId, error, method = "GET" } = req
    17
    18  // 後続の処理の利便性のために
    19  // authOptionsをまとめたり、デフォルト値を注入している
    20  const { options, cookies } = await init({
    21    authOptions,
    22    action,
    23    providerId,
    24    origin: req.origin,
    25    callbackUrl: req.body?.callbackUrl ?? req.query?.callbackUrl,
    26    csrfToken: req.body?.csrfToken,
    27    cookies: req.cookies,
    28    isPost: method === "POST",
    29  })
    30
    31  // 中略...

    後半: 認証処理

    後半はいよいよユーザ認証処理の本体です。
    ここでの処理は認証方式や、Route Handlerの呼び出し方法に応じてたくさんの分岐が書かれています。
    一つずつ見ていると帰ってこられなくなってしまうので、今回はシンプルなクレデンシャル方式に絞って見ていきます。

    1// 中略...
    2} else if (method === "POST") {
    3    switch (action) {
    4      case "callback":
    5        if (options.provider) {
    6          // 中略...
    7
    8          const callback = await routes.callback({
    9            body: req.body, // signIn()を呼び出すときに渡したユーザ名やパスワードが入っている
    10            query: req.query,
    11            headers: req.headers,
    12            cookies: req.cookies,
    13            method,
    14            options, // authOptionsで設定した設定値
    15            sessionStore,
    16          })
    17          // 中略...
    18          return { ...callback, cookies }
    19        }
    20        break

    リクエストボディや設定値(options)を入力にroutes.callback()なるメソッドを呼び出しているようです。


    小休止: ここまでのまとめ

    さて、少し呼び出し階層が深くなってきたので、ここまでの理解を整理しておきます。
    ゴールと今置かれている状況を確認しておくことで、どうやってゴールに近づいており、あとどれくらいでゴールにたどり着けそうか俯瞰することができます。

    まず、今回のゴールはauthOptionsやsignIn()の引数(callbackUrlや認証情報)がどこでどう使われているか理解することでした。
    使われ方が見えてくれば、ユースケースに応じた設定値を書けるようになり、NextAuth.jsを自信を持って使いこなせるようになるはずです。

    最初に、認証処理本体を発火させるために、Route Handlerを呼び出しました。
    Route HandlerからAuthHandler()に至るまでに多くの処理が書かれていましたが、どれも前処理が主で、authOptionsや認証情報をほぼそのまま渡しているような状態でした。

    つまり、これから見ていく処理を理解していけば、設定値やログイン処理の引数がどうやって使われるのか明らかになってきそうです。

    ゴールもだいぶ見えてきたので、もうひと頑張りしたいと思います。

    routes.callback()

    認証処理と関わりが深そうなroutes.callback()を見ていきます。
    コード量は多いですが、どれも設定値と認証処理の関係を理解するのに重要なものなので、ゆっくり見ていきます。

    1// nextauthjs/next-auth/packages/next-auth/src/core/routes/callback.ts
    2
    3/* Handle callbacks from login services */
    4export default async function callback(params: {
    5  options: InternalOptions
    6  query: RequestInternal["query"]
    7  method: Required<RequestInternal>["method"]
    8  body: RequestInternal["body"]
    9  headers: RequestInternal["headers"]
    10  cookies: RequestInternal["cookies"]
    11  sessionStore: SessionStore
    12}): Promise<ResponseInternal> {
    13  const { options, query, body, method, headers, sessionStore } = params
    14  const {
    15    provider,
    16    adapter,
    17    url,
    18    callbackUrl,
    19    pages,
    20    jwt,
    21    events,
    22    callbacks,
    23    session: { strategy: sessionStrategy, maxAge: sessionMaxAge },
    24    logger,
    25  } = options
    26
    27  const cookies: Cookie[] = []
    28
    29  const useJwtSession = sessionStrategy === "jwt"
    30  // 中略...
    31
    32} else if (provider.type === "credentials" && method === "POST") {
    33    // リクエストボディ
    34    // signIn()に渡したユーザ情報は、credentialsという名前で参照できるようになる
    35    const credentials = body
    36
    37    let user: User | null
    38    try {
    39      // providerで定義したauthorize()を呼び出し、認証の成否を判定
    40      user = await provider.authorize(credentials, {
    41        query,
    42        body,
    43        headers,
    44        method,
    45      })
    46      if (!user) {
    47        return {
    48          status: 401,
    49          redirect: `${url}/error?${new URLSearchParams({
    50            error: "CredentialsSignin",
    51            provider: provider.id,
    52          })}`,
    53          cookies,
    54        }
    55      }
    56    } catch (error) {
    57      return {
    58        status: 401,
    59        redirect: `${url}/error?error=${encodeURIComponent(
    60          (error as Error).message,
    61        )}`,
    62        cookies,
    63      }
    64    }
    65
    66    // ユーザ情報を後続のリクエストで参照できるよう、JWTに格納
    67    /** @type {import("src").Account} */
    68    const account = {
    69      providerAccountId: user.id,
    70      type: "credentials",
    71      provider: provider.id,
    72    }
    73    // 中略...
    74
    75    const defaultToken = {
    76      name: user.name,
    77      email: user.email,
    78      picture: user.image,
    79      sub: user.id?.toString(),
    80    }
    81
    82    // JWTに埋め込むデータを取捨選択できるよう、jwt callbackと呼ばれる処理を呼び出す
    83    const token = await callbacks.jwt({
    84      token: defaultToken,
    85      user,
    86      // @ts-expect-error
    87      account,
    88      isNewUser: false,
    89      trigger: "signIn",
    90    })
    91
    92    // Encode token
    93    const newToken = await jwt.encode({ ...jwt, token })
    94
    95    // 中略...
    96    // エンコードされたトークンをクッキーに詰め込むことで、後続のリクエストでもユーザ情報を
    97    // クッキーに埋め込まれたトークン経由で参照できるようになる
    98
    99    return { redirect: callbackUrl, cookies }

    authorize()

    認証処理本体として、authorize()なるメソッドを呼んでいるようです。
    これはauthOptionsに書いた設定値で、関数を定義していました。

    改めて定義した箇所を載せておきます。

    1// lib/authOptions.ts
    2// カスタマイズしたNextAuth.js向けの設定オブジェクトを抜粋
    3
    4 // プロバイダ
    5    // ユーザの本人確認を責務とする
    6    providers: [
    7        // いわゆるID・パスワード方式
    8        // OAuthなども使えるが、今回はシンプルに扱えるクレデンシャル方式を採用
    9        CredentialsProvider({
    10            // ログインのタイミングで呼ばれる関数
    11            // ここで画面の入力情報が特定のユーザと紐づくか、つまり認証の成否を判定
    12            authorize(credentials, req) {
    13                // (後述)
    14                // credentialsオブジェクトのプロパティは、signIn()で渡された引数と対応
    15                if (!credentials?.username || !credentials?.password) {
    16                    return null;
    17                }
    18                const {username, password} = credentials;
    19
    20                // バックエンドなどと通信し、認証
    21                // ex:
    22                // await login(username, password);
    23
    24                return {
    25                    id: "USERID",
    26                    name: username
    27                }
    28            },
    29            // 本来は、このオプションからログイン画面を組み立てるときに参照されるプロパティ
    30            // ただし、型の制約からauthorize関数の引数credentialsのキー名がここで定義したものに限定されるので、名前だけ設定しておく
    31            credentials: {username: {}, password: {}},
    32        })
    33    ],

    呼び出し方が見えたので、なぜこの設定が必要なのか・どこで生成された引数を受け取っているのか明確になりました。
    とくによく使うのは引数credentialsで、ユーザ情報が格納されています。

    エントリーポイントであるsignIn()で渡された引数が使われる場所にようやく行き着きました。

    つまり、authorize()は画面で渡した認証情報をもとにバックエンドなどを介して認証し、
    結果をオブジェクトで返却することを責務に持つ関数と捉えることができます。

    全体像を整理するのは振り返りの項へ譲ることにして、ほかの設定値も続けて探っていきます。

    callbacks.jwt()

    呼び出し元と設定値を最初に見ておきます。

    1const defaultToken = {
    2      name: user.name,
    3      email: user.email,
    4      picture: user.image,
    5      sub: user.id?.toString(),
    6    }
    7
    8    // JWTに埋め込むデータを取捨選択できるよう、jwt callbackと呼ばれる処理を呼び出す
    9    const token = await callbacks.jwt({
    10      token: defaultToken,
    11      user, // authorize()で返却したオブジェクト
    12      // @ts-expect-error
    13      account,
    14      isNewUser: false,
    15      trigger: "signIn",
    16    })
    1 callbacks: {
    2        // JWTトークンを生成/更新する準備が整ったときに呼ばれる
    3        // トークンに何を含めるか
    4        async jwt({token, user}) {
    5            if (user?.name) {
    6                token.name = user.name
    7            }
    8            return token;
    9        },
    10    }

    callbacks.jwt()はauthOptionsで定義されたものなので、認証の結果から得られたユーザ情報をもとに、トークンの形をカスタマイズするのが役割のようです。

    ソースコードから、いつ呼ばれるか明らかになったので、設定値を書く目的・何を書けば良いかもだいぶ見えてきました。


    レスポンス返却

    ユーザの認証が終わり、ユーザ情報を詰め込んだJWTも出来上がったので、あとは画面に結果を返却するだけです。
    Route Handlerは単に処理結果をHTTPレスポンスで返しているだけなので割愛し、signIn()まで一気に戻っていきます。

    1// nextauthjs/next-auth/packages/next-auth/src/react/index.tsx
    2// signIn()の抜粋
    3
    4  // Route Handlerを呼び出して認証結果を取得
    5  const res = await fetch(_signInUrl, {
    6    method: "post",
    7    headers: {
    8      "Content-Type": "application/x-www-form-urlencoded",
    9    },
    10    // @ts-expect-error
    11    body: new URLSearchParams({
    12      ...options,
    13      csrfToken: await getCsrfToken(),
    14      callbackUrl,
    15      json: true,
    16    }),
    17  })
    18
    19  const data = await res.json()
    20
    21  // redirectはデフォルトはtrue
    22  if (redirect || !isSupportingReturn) {
    23    // いずれにしてもクレデンシャル方式ではsignIn()に渡した引数callbackUrlが参照される
    24    // つまり、デフォルトの挙動はログイン処理完了後、callbackUrlへ遷移する
    25    const url = data.url ?? callbackUrl
    26    window.location.href = url
    27    // If url contains a hash, the browser does not reload the page. We reload manually
    28    if (url.includes("#")) window.location.reload()
    29    return
    30  }

    ユーザ情報はクッキー経由で渡されるので、あとは元の画面に返すだけです。
    実際にソースコードでもcallbackUrlという戻り先が指定されていたら、そこに遷移するよう記述しているぐらいのシンプルな処理でした。

    振り返り

    少々駆け足気味でしたが、ソースコードを探検してきました。
    中身を見た後なら、最初に見た設定値の枠組みも読み解けそうな予感がします。

    総仕上げの復習がてら、設定値がどんな構造で、どういう目的で書かれたものなのか理解を整理していきます。

    authOptions(認証の設定)

    最初に認証処理をどうやって実現するかを記述するauthOptionsを見直します。
    どこからどうやって呼び出されるのか見てきたことで、設定値が何を意図して書かれたものか掴みやすくなるはずです。

    1import CredentialsProvider from "next-auth/providers/credentials";
    2import {NextAuthOptions} from "next-auth";
    3
    4export const authOptions: NextAuthOptions = {
    5    // プロバイダ
    6    // ユーザの本人確認を責務とする
    7    providers: [
    8        // いわゆるID・パスワード方式
    9        // OAuthなども使えるが、今回はシンプルに扱えるクレデンシャル方式を採用
    10        CredentialsProvider({
    11            // ログインのタイミングで呼ばれる関数
    12            // ここで画面の入力情報が特定のユーザと紐づくか、つまり認証の成否を判定
    13            authorize(credentials, req) {
    14                // (後述)
    15                // credentialsオブジェクトのプロパティは、signIn()で渡された引数と対応
    16                if (!credentials?.username || !credentials?.password) {
    17                    return null;
    18                }
    19                const {username, password} = credentials;
    20
    21                // バックエンドなどと通信し、認証
    22                // ex:
    23                // await login(username, password);
    24
    25                return {
    26                    id: "USERID",
    27                    name: username
    28                }
    29            },
    30            // 本来は、このオプションからログイン画面を組み立てるときに参照されるプロパティ
    31            // ただし、authorize関数の引数credentialsのキー名がここで定義したものに限定されるので、名前だけ設定しておく
    32            credentials: {username: {}, password: {}},
    33        })
    34    ],
    35    // トークンをハッシュ化させたりなど、機密情報を生成するのに使うランダムな文字列
    36    secret: process.env.AUTH_SECRET,
    37    // 特定のタイミングでNextAuth.jsから呼ばれる処理
    38    callbacks: {
    39        // JWTトークンを生成/更新する準備が整ったときに呼ばれる
    40        // トークンに何を含めるか
    41        async jwt({token, user}) {
    42            if (user?.name) {
    43                token.name = user.name
    44            }
    45            return token;
    46        },
    47    }
    48}

    ざっくり言葉でもまとめておきます。

    • providersプロパティに認証方式を記述
      • 認証方法にあわせたProviderを選択
      • authorizeプロパティには実際にログインする方法を記述
      • 引数は画面から渡されたオブジェクトがcredentialsオブジェクトに詰め込まれて渡される
    • callbacksは、認証処理をカスタマイズするためにNextAuth.jsが特定のタイミングで呼び出すメソッド
      • jwt callbackはユーザ認証が終わり、ユーザ情報をJWTへ詰め込むタイミングで呼ばれる
      • つまり、認証の結果得られたユーザ情報(user)をもとにJWTに何を含めたら良いか決めるのが責務

    全体像が見えてきたので、あとは部分の知識をドキュメントで補強していけば認証処理もなんとか書くことができそうです。

    Route Handler

    Route HandlerはauthOptionsで書いた設定値を受け取りながら、認証ハンドラを呼び出すことを責務としています。

    1import NextAuth from "next-auth";
    2import {authOptions} from "@/lib/authOptions";
    3
    4const handler = NextAuth(authOptions);
    5
    6export {handler as GET, handler as POST};

    また、Route HandlerはsignIn()から呼び出すだけでなく、ビルトインのログインページを描画したり、APIから直接ログイン処理を叩けたりといった機能も持っています。

    参考

    ログイン処理呼び出し(signIn())

    最後に、ログイン処理をNextAuth.jsに合流させるエントリーポイントを見ておきます。

    1const handleSignIn = () => {
    2        // 第一引数は認証プロバイダ名 プロバイダによってリダイレクトなどの制御が異なるので指定が必要
    3        signIn('credentials', {
    4            // ログイン処理後のリダイレクト先
    5            callbackUrl: '/welcome',
    6            // 以降はクレデンシャル情報
    7            username: 'Snivy',
    8            password: 'strongPassword',
    9        });
    10    };

    第一引数は認証プロバイダ名を文字列で指定したものです。
    以降は認証オプションを記述したオブジェクトで、今回の例ではcallbackUrlプロパティのみ明示的に型やドキュメントで定義されています。

    以降のusername, passwordプロパティはユーザ独自のプロパティで、認証処理の実装であるprovider.authorize()で参照されます。


    ソースコードを見た後に全体像を振り返ることで、NextAuth.jsがどうやってログイン処理を実現しているのか整理してきました。
    関数呼び出しや設定値のつながりが見えてくることで、それぞれの設定がなぜ必要なのか・何を設定するのがよいか、以前よりも理解が深まっていれば幸いです。

    補足: その他のコールバック

    ログイン処理では大きく関わりませんでしたが、実務でそこそこ触る機会の多かったコールバックを備忘録がてらまとめておきます。

    session callback

    getServerSession()など、ユーザ情報を読み出す処理で発火します。
    具体的なサンプルは以下の通りです。

    1// app/welcome/page.tsx
    2
    3import {getServerSession} from "next-auth";
    4import {authOptions} from "@/lib/authOptions";
    5
    6// ログイン成功画面 セッションからユーザ名を取り出して表示
    7export default async function WelcomePage() {
    8    const token = await getServerSession(authOptions);
    9    const userId = token?.user?.name ?? 'JohnDoe';
    10
    11    return (
    12        <h1>Welcome, {userId}!!</h1>
    13    )
    14}
    1// session callback
    2 callbacks: {
    3       // セッションで公開する情報を設定
    4        async session({session, token}) {
    5            if (session.user && token.name) {
    6                session.user.name = token.name;
    7            }
    8            return session;
    9        }
    10    }

    ほぼjwt callbackと同じような構造をしています。
    jwt callbackだけで十分に見えますが、JWTとセッションは使われ方が異なります。

    具体的には、セッションはgetSession()のようにクライアントサイドで取得できるAPIも用意されています。
    つまり、セッションに詰め込まれた情報はクライアントサイドから第三者に読み取られる可能性があります。

    公開する情報を最小限に絞るために、jwt callbackとは別にsession callbackが用意されているようです。

    参考

    authorized callback

    ログイン済みかどうかを判定するときに利用します。
    コールバック本体はミドルウェアに定義します。

    1// middleware.ts
    2
    3import {withAuth} from "next-auth/middleware"
    4
    5export default withAuth(
    6    {
    7        callbacks: {
    8            // ログイン済みかどうかをbool値で表現
    9            authorized({token, req}) {
    10                // トークンにユーザ情報が含まれない, つまり未ログインの場合はfalseを返す
    11                // falseが返るとログインページにリダイレクトされる
    12                return !!token?.name;
    13            },
    14        },
    15        pages: {
    16            signIn: "/sign-in",
    17        },
    18    },
    19)

    jwt callbackのようにトークンを受け取れるので、ユーザ情報の有無からログイン済みかどうかをbool値で表現することができます。

    jwt, session, atuhorizedなどいくつかのコールバックを見ることで、コールバックがいつ・どのように呼ばれるかなんとなく見えてきました。
    枠組みが掴めれば、目的に応じてどんなコードを書けばよいのかも徐々に把握できるようになるはずです。

    参考

    まとめ

    本記事では、NextAuth.jsの理解を深めることを目指してソースコードを読んできました。
    ドキュメントが充実していればもっとスムーズに理解できた気がしないでもないですが、
    探検していて面白かったので、よしとします。

    フレームワークと比べるとコード量もそこまで多くなくて手軽に読めるので、興味が出てきたらぜひ読んでみてください。

    ライトコードでは、エンジニアを積極採用中!

    ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。

    採用情報へ

    おすすめ記事

    エンジニア大募集中!

    ライトコードでは、エンジニアを積極採用中です。

    特に、WEBエンジニアとモバイルエンジニアは是非ご応募お待ちしております!

    また、フリーランスエンジニア様も大募集中です。

    background