🤢

NextAuth.jsの認証失敗時にエラーメッセージを取得するための工夫

2024/08/17に公開2

はじめに

NextAuth.jsのMiddlewareに関する課題と解決策をなんとか解決していたところ、新たに認証失敗時の課題が次々と発生しました・・・

NextAuthを使用したログインのフロー

  1. ログインボタンのクリック
    ユーザーがアプリケーション内のログインボタンをクリックします。このアクションには、usernameとpasswordが必要です(app/signin/pages)

  2. signInメソッドの実行
    usernameとpasswordを引数として、signInメソッドが実行されます(app/signin/pages)

  3. authorize関数の実行
    signInメソッド内部で、providersオブジェクトのauthorize関数が呼び出されます(app/api/auth/[...nextauth]/route.ts)

  4. API /api/loginの呼び出し
    authorize関数内部で、awaitを使って/api/login APIにリクエストします(app/api/auth/[...nextauth]/route.ts)

  5. ユーザー認証の確認
    /api/login APIがusernameとpasswordに基づいてデータベースでユーザーの存在を確認します。その結果が返されます(app/api/login/route.ts)

  6. 認証結果の返却
    データベースから返されたユーザー認証の結果がauthorize関数の内部変数に割り当てられます。この値がauthorize関数から返されます(app/api/auth/[...nextauth]/route.ts)

  7. callbacksメソッドの実行
    authorize関数から返された値に基づいて、callbacksメソッドが実行されます(app/api/auth/[...nextauth]/route.ts)

  8. クッキーへの暗号化保存
    認証された情報は暗号化され、クッキーに保存されます

やりたいこと

上記のログインフローの中で5.のAPIでの認証が失敗だった場合、APIサーバから「存在しないIDです」「パスワードが間違っています」など具体的なエラーメッセージが返され、そのエラーメッセージがクライアントに渡されて画面に表示されるようにしたいです。

試したこと

  1. NextAuthをインストールします
npm install next-auth
  1. NextAuthの設定
    認証の定義と認証後のコールバックを設定し、認証データをセッションで管理できるように設定しました
//src/app/api/auth/[...nextauth].js
import CredentialsProvider from "next-auth/providers/credentials"
...
providers: [
  CredentialsProvider({
    name: 'Credentials',
    credentials: {
      email: { label: "Username", type: "email"},
      password: { label: "Password", type: "password" }
    },
    async authorize(credentials, req) {
      const res = await fetch("/test/endpoint", {
        method: 'POST',
        body: JSON.stringify(credentials),
        headers: { "Content-Type": "application/json" }
      })
      const data = await res.json();
      if (res.ok && data) {
        return data
      }
      return null
    }
  })
],
callbacks: {
  async jwt({ token, user }) {
    if (user) {
      token = user;
    }
      return token;
    },
  },
  async session({ session, token, user }) {
    session.user = token;
    return session;
  },
}
...
  1. LoginFormを作成
    ログインボタンを押すとhandleLoginが呼ばれ、NextAuthのsignInメソッドを使用して認証を行い、エラーの場合はログインフォームにサーバから受けたエラーメッセージを表示されるようにしました
'use client';
import React, { useState } from 'react';
import { signIn } from 'next-auth/react';
import { useRouter } from 'next/navigation';
import { Button } from 'react-bootstrap';

export default function LoginForm() {
  const [email, setEmail] = useState<string>('');
  const [password, setPassword] = useState<string>('');
  const [error, setError] = useState<string | null>(null);
  const router = useRouter();

  const handleLogin = async (e: React.FormEvent) => {
    e.preventDefault();
    const result = await signIn('credentials', {
    redirect: false,
    email,
    password,
    });
    if (result?.error) {
    if (result.error.includes('Invalid credentials')) {
      setError('An error occurred. Please try again.');
    } else {
      setError(result?.error || 'Login failed');
    }
    } else if (result?.ok) {
    router.push('/dashboard');
    } else {
    setError(result?.error || 'Login failed');
    }
};

return (
  <form onSubmit={handleLogin}>
    {error && <p className="text-danger">{error}</p>}
    <div className="mb-3">
        <label className="form-label">ID(登録メールアドレス)</label>
        <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="user@openstreet.co.jp" className="form-control" required/>
    </div>
    <div className="mb-3">
        <label className="form-label">Password</label>
        <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="password" className="form-control" required />
    </div>
    <div className="form-check mb-3">
        <input type="checkbox" className="form-check-input" id="rememberMe" />
        <label className="form-check-label" htmlFor="rememberMe">
        ログイン状態を保存
        </label>
    </div>
    <Button type="submit" variant="primary" className="btn btn-primary btn mx-1 w-100 mb-3">
        ログイン
    </Button>
  </form>
  );
}

問題

  1. CredentialsSigninエラーがコンソールに残る
[auth][error] CredentialsSignin: Read more at https://errors.authjs.dev#credentialssignin
    at Module.callback (webpack-internal:///(rsc)/./node_modules/@auth/core/lib/actions/callback/index.js:256:30)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async AuthInternal (webpack-internal:///(rsc)/./node_modules/@auth/core/lib/index.js:65:24)
    at async Auth (webpack-internal:///(rsc)/./node_modules/@auth/core/index.js:123:29)
  1. CallbackRouteErrorエラーがコンソールに残る
[auth][error] CallbackRouteError: Read more at https://errors.authjs.dev#callbackrouteerror
[auth][cause]: Error: パスワードが間違いました。

3. APIサーバーへのリクエストで認証失敗後、authを通してクライアントへ送るとクライアント側ではerror: "Configuration”になって具体的なエラーメッセージがもらえない・・2.のコンソールログにある「Error: パスワードが間違いました。」が欲しいのにクライアントには提供されないよう・・

解決案

CredentialsSignin.codeを設定して、CredentialsSigninのエラーをスローするなど色々試しましたが、完全に解決できていません。最悪の場合、NextAuthの使用を中止し、直接ログイン機能を実装することも検討しています。方針が決まり次第、こちらの内容を更新します。

Discussion

HiroakiHiroaki

export class UserNotFoundCredentialsSignin extends CredentialsSignin {
static CODE = 404;
constructor(console_message: string = 'User Not Found') {
super()
this.code = "404";
this.stack = undefined;
this.message = console_message;
}
}

自分もこの問題にぶち当たりましたが、上記のように拡張したエラーでthrowしたところ、コンソールのエラーstack部分が消えて1行だけになり、目障りではなくなりました。
this.messageはコンソール上だけのメッセージでクライアントには届きませんが、最悪codeの中に
${variable1}|${variable2}|${variable3}のようにすれば細かいデータもクライアント側で受け取れますね。

1mono21mono2

ありがとうございます🙇‍♂️自分も同じ問題に直面していたので助かりました。