NextAuth.jsの認証失敗時にエラーメッセージを取得するための工夫
はじめに
NextAuth.jsのMiddlewareに関する課題と解決策をなんとか解決していたところ、新たに認証失敗時の課題が次々と発生しました・・・
NextAuthを使用したログインのフロー
-
ログインボタンのクリック
ユーザーがアプリケーション内のログインボタンをクリックします。このアクションには、usernameとpasswordが必要です(app/signin/pages) -
signInメソッドの実行
usernameとpasswordを引数として、signInメソッドが実行されます(app/signin/pages) -
authorize関数の実行
signInメソッド内部で、providersオブジェクトのauthorize関数が呼び出されます(app/api/auth/[...nextauth]/route.ts) -
API /api/loginの呼び出し
authorize関数内部で、awaitを使って/api/login APIにリクエストします(app/api/auth/[...nextauth]/route.ts) -
ユーザー認証の確認
/api/login APIがusernameとpasswordに基づいてデータベースでユーザーの存在を確認します。その結果が返されます(app/api/login/route.ts) -
認証結果の返却
データベースから返されたユーザー認証の結果がauthorize関数の内部変数に割り当てられます。この値がauthorize関数から返されます(app/api/auth/[...nextauth]/route.ts) -
callbacksメソッドの実行
authorize関数から返された値に基づいて、callbacksメソッドが実行されます(app/api/auth/[...nextauth]/route.ts) -
クッキーへの暗号化保存
認証された情報は暗号化され、クッキーに保存されます
やりたいこと
上記のログインフローの中で5.のAPIでの認証が失敗だった場合、APIサーバから「存在しないIDです」「パスワードが間違っています」など具体的なエラーメッセージが返され、そのエラーメッセージがクライアントに渡されて画面に表示されるようにしたいです。
試したこと
- NextAuthをインストールします
npm install next-auth
- 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;
},
}
...
- 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>
);
}
問題
- 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)
- 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
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}
のようにすれば細かいデータもクライアント側で受け取れますね。ありがとうございます🙇♂️自分も同じ問題に直面していたので助かりました。