🐷

NextAuth(Auth.js)でのemail, passwordログイン時の処理を追ってみる

2023/07/30に公開

はじめに

NextAuthでemail, passwordでの認証をするには、ProviderとしてCredentialsProviderを設定して実装する必要があります。
https://next-auth.js.org/providers/credentials

以下のように諸々設定することになるかと思います。

/**
 * Options for NextAuth.js used to configure adapters, providers, callbacks, etc.
 *
 * @see https://next-auth.js.org/configuration/options
 */
export const authOptions: NextAuthOptions = {
  callbacks: {
    jwt: async ({ token, user }) => {
      if (user) {
        token.id = user.id;
      }
      return token;
    },
  },
  providers: [
    CredentialsProvider({
      credentials: {
        email: { type: "text" },
        password: { type: "password" },
      },
      async authorize(credentials) {
        // ...
      },
    }),
  ],
};

この時に、optionで設定した内容がどのようにサインイン処理に絡んでくるのかを知っておきたくなったのでNextAuthのコードをかいつまんで追ってみました。

signIn

Credentials Providerを使ってsigninするときの処理の一部を追ってみます。

以下のようにemai, passwordを引数にsignInメソッドを呼びだしたとします。

import { signIn } from "next-auth/react";
signIn("credentials", { email: "hoge@example.com", password: "hogehoge", redirect: false })

signIn()のコードはこちらになります。
https://github.com/nextauthjs/next-auth/blob/next-auth%404.22.3/packages/next-auth/src/react/index.tsx#L212

このメソッド内で

const _signInUrl = `${signInUrl}?${new URLSearchParams(authorizationParams)}`

に対してPOSTリクエストを行っています。
https://github.com/nextauthjs/next-auth/blob/v4.2.1/src/react/index.tsx#L205-L223

_signInUrlhttp://localhost:3000/api/auth/callback/credentials?となります。

http://localhost:3000/api/auth/callback/credentials?へのリクエスト処理を追う

https://github.com/nextauthjs/next-auth/blob/next-auth%404.22.3/packages/next-auth/src/next/index.ts#L22
AuthHandler()が呼ばれ、
https://github.com/nextauthjs/next-auth/blob/next-auth%404.22.3/packages/next-auth/src/core/index.ts#L88
が呼ばれます。

ここでは、HTTPリクエストメソッドやパスでその後の処理を振り分けています。

今回はPOSTcallbackなので
https://github.com/nextauthjs/next-auth/blob/next-auth%404.22.3/packages/next-auth/src/core/index.ts#L272

const callback = await routes.callback({
            body: req.body,
            query: req.query,
            headers: req.headers,
            cookies: req.cookies,
            method,
            options,
            sessionStore,
          })

が呼ばれます。

routes.callback

このメソッドは以下になります。
https://github.com/nextauthjs/next-auth/blob/next-auth%404.22.3/packages/next-auth/src/core/routes/callback.ts#L13

このメソッド内で

user = await provider.authorize(credentials, {
        query,
        body,
        headers,
        method
      });

という箇所があり、これがNextAuthのoptionで設定したauthorizeになります。

 providers: [
    CredentialsProvider({
      credentials: {},
      async authorize(credentials) {
        // ...
      },
    }),
  ],

第一引数は以下のようにobjectになっています。

// credentials
{
  email: 'hoge@example.com',
  password: 'hogehoge',
  redirect: 'false',
  csrfToken: 'd202ec1cb30e52f8b7ee910acc9a6efd1107c7c4d39b09a9731f157b2149a9dd',
  callbackUrl: 'http://localhost:3000/',
  json: 'true'
}

そのため、authorize()では最初にsignIn()で渡したemail, passwordを使うことができます。
このemail, passwordを使ってログインさせるuserを取得することができます。

authorize()でuserを取得した後の処理を追っていきます。
https://github.com/nextauthjs/next-auth/blob/v4.2.1/src/core/routes/callback.ts#L334-L389

const isAllowed = await callbacks.signIn({
        user,
        // @ts-expect-error
        account,
        credentials,
      })

という箇所があります。

これがNextAuthのoptionの

...
  callbacks: {
    async signIn({ user, account, profile, email, credentials }) {
      // ....
    },

の部分になります。

ちなみに、optionとして設定していなかったらデフォルトが使われます。
callbacksのデフォルトはここにあります。
https://github.com/nextauthjs/next-auth/blob/next-auth%404.22.3/packages/next-auth/src/core/lib/default-callbacks.ts

https://next-auth.js.org/configuration/callbacks#sign-in-callback
ここでは取得したuserにサインインを許可していいかをチェックすることができます。

このチェックの後は、
https://github.com/nextauthjs/next-auth/blob/v4.2.1/src/core/routes/callback.ts#L391-L422
が続き、

const token = await callbacks.jwt({
      token: defaultToken,
      user,
      account,
      isNewUser: false,
      trigger: "signIn"
    });

で、これがNextAuthのoptionの

...
callbacks: {
  async jwt({ token, account, profile }) {
    // ...
  }
}
...

となります。

https://next-auth.js.org/configuration/callbacks#jwt-callback

実際の値としては例えばログインuserとして以下のようなuserを取得していた時、

const user = { id: "1", name: "J Smith", email: "jsmith@example.com" };

以下のようになります。

defaultToken {
  name: 'J Smith',
  email: 'jsmith@example.com',
  picture: undefined,
  sub: '1'
}
user { id: '1', name: 'J Smith', email: 'jsmith@example.com' }
account {
  providerAccountId: '1',
  type: 'credentials',
  provider: 'credentials'
}
token {
  name: 'J Smith',
  email: 'jsmith@example.com',
  picture: undefined,
  sub: '1'
}

その後の処理はjwt()の返却値を暗号化しCookieに含めて、
http://localhost:3000/api/auth/callback/credentials?のResponseとなります。

http://localhost:3000/api/auth/callback/credentials?へのResponseが返ってきてからの処理

singIn()での
https://github.com/nextauthjs/next-auth/blob/next-auth%404.22.3/packages/next-auth/src/react/index.tsx#L212
http://localhost:3000/api/auth/callback/credentials?へのリクエストが完了してからの処理を見ていきます。

https://github.com/nextauthjs/next-auth/blob/next-auth%404.22.3/packages/next-auth/src/react/index.tsx#L250-L289
といってもあとは特に気になることはなくて、signInに成功した時にリダイレクトするのかを見て、成功可否をreturnしています。

email, password認証する時のoptionまとめ

コードをかいつまんで追った結果、Optionとしてはデフォルトでよければ以下だけでemail, password認証が実現できるということがわかりました。

/**
 * Options for NextAuth.js used to configure adapters, providers, callbacks, etc.
 *
 * @see https://next-auth.js.org/configuration/options
 */
export const authOptions: NextAuthOptions = {
  providers: [
    CredentialsProvider({
      // ここのKeyは下のauthorize()のcredentialsに型が追加されるので追加してる
      credentials: {
        email: { type: "text" },
        password: { type: "password" },
      },
      async authorize(credentials) {
        const email = credentials?.email;
        const password = credentials?.password;

        // TODO: email, passwordを使ってuserを取得する
        const user = { id: "1", name: "J Smith", email: "jsmith@example.com" };

        if (user) {
          return user;
        } else {
          return null;
        }
      },
    }),
  ],
};

authorize()でユーザーを取得した後に、そのユーザーのsigninを弾きたい時はsingIn()で、何かしらの判定処理を入れてfalseで返す感じです。

...
  callbacks: {
    async signIn({ user, account, profile, email, credentials }) {
      // ....
    },

(ここで弾くのと、authorize()で弾くのとなにか違いがあるんだろうか)

jwt()の方は、token(Cookie)にさらに情報を含めたい時に使う感じかと。

...
  callbacks: {
    jwt: async ({ token, user }) => {
      if (user) {
        token.id = user.id;
      }
      return token;
    },

https://next-auth.js.org/configuration/callbacks#jwt-callback ではアクセストークンを含めている。

Discussion