Auth.js の OAuthAccountNotLinkedエラーはサーバーアクションでcatchできない
これは何?
-
OAuthAccountNotLinked
エラーはサーバーアクションでcatchできない - 上記エラーは、クライアントに解決策を促すタイプのエラーなので適切に処理したい
- ではどこで処理するのがよいか?についてアイデアを提供する
環境
Next.js 15.2.3 App Router
Auth.js (NextAuth) 5.0.0-beta.25
※ データベースアダプタにはPrisma
を使用
OAuthAccountNotLinkedエラーとは?
すでに同名のEmailでユーザーが登録されている場合にOAuthアカウントでユーザーを新規登録しようとすると、ユーザー重複とみなされエラーとなる。Auth.js
はセキュリティの関係で、登録済みユーザーにOAuthアカウントをデフォルトでリンクさせない。
詳しくは以下:
OAuthAccountNotLinkedエラーの理想のハンドリングは?
個人的には、連携しようとしたアカウントのemailが表示され、「すでにユーザーとして登録されているからまずログインして連携してください」と案内をするのが親切だと思う。
具体的にはどこでハンドリングするか?
真っ先に思いついたのはサーバーアクションでのtry-catch
だが、うまくいかなかった。
以下のコードでcatch
したe
の中身を見ると、digest
しか入っていない。おそらくAuth.js
のデフォルトの認証方式でPKCE
を使っている関係で、PKCE
の認可プロセスの初期段階の情報が入ってしまっているように見えた。
try {
await signIn("github", {
redirectTo: redirectPath,
});
} catch (e) {
if (e instanceof AuthError) {
switch (e.type) {
case "OAuthAccountNotLinked":
// something handling code
default:
// something handling code
}
}
throw e;
}
実際に欲しい情報は、PKCE
の認可が完了してprofile
情報を取得し、ユーザーデータベースとの通信が発生したタイミングなのだが、そのタイミングではなく、もっと前の段階の情報がcatch
されているような印象を受けた(詳しくは調べてない...)
ではどこでハンドリングしたか?
以下を参考にした。
OAuthAccountNotLinked
はエラー時にクエリパラメータに付与されるため、パラメータをサーバーコンポーネントでキャッチしてトーストメッセージを出すことも検討した。実際、そのようにハンドリングすることを推奨している節がある。この方法はAuth.js
のレールに乗っている方法で、良さそうに見えた。
参考:
しかしユーザーはメールアドレスを複数もつこともある。エラーコードだけのキャッチではどのメールアドレスが重複しているか不明であり、ユーザーに適切なアクションを促すには情報が不足しているように見えた。
そこで上記の記事に従い、Auth.js
のsignIn
コールバックを使うことにした。
詳細:
具体的なコード
signIn
コールバックでは以下のコードを実装
export const {
handlers: { GET, POST },
auth,
signIn,
signOut,
} = NextAuth({
...authConfig,
adapter: PrismaAdapter(prisma),
session: {
strategy: "jwt",
},
providers: [
GitHub,
],
callbacks: {
jwt({ token, user }) {
if (user) {
token.id = user.id;
}
return token;
},
async signIn({ account, profile }) {
// ユーザーに回避策を促す種類のエラーを適切にハンドリングするためのコード
return buildProviderAuthResponse({
authType: (await getAndDeleteCookie("mysite_provider_auth_type")) ?? "",
profileEmail: profile?.email ?? "",
providerAccountId: account?.providerAccountId ?? "",
provider: account?.provider ?? "",
});
},
},
});
export async function buildProviderAuthResponse(
props: Props
): Promise<boolean | string> {
// すでに同名のEmailでユーザーが存在する場合
if (await userExistsWithoutLinkedAccount(props)) {
await setFlash({
type: "error",
message: `Email: ${props.profileEmail} のアカウントが存在します。ログインして連携してください。`,
});
return "/login";
}
// ログインを試みたが連携済みのアカウントがない場合
if (await isSigninWithoutLinkedAccount(props)) {
await setFlash({
type: "error",
message: "アカウントが存在しません。",
});
return "/login";
}
// 新規登録を試みたが連携済みのアカウントがすでにある場合
if (await isSignupWithExistingLinkedAccount(props)) {
await setFlash({
type: "error",
message: "アカウントがすでに存在します。ログインしてください。",
});
return "/login";
}
// 正常系でログインリクエストの場合は、ログイン後のフラッシュメッセージをあらかじめ設定
if (isSigninRequest(props)) {
await setFlash({
type: "success",
message: "ログインしました。",
});
}
// 正常系で新規登録リクエストの場合は、登録後のフラッシュメッセージをあらかじめ設定
if (isSignupRequest(props)) {
await setFlash({
type: "success",
message: "アカウントを登録しました。",
});
}
return true;
}
細かいコードは省略するが、メインのロジックは上記の通り。
まとめ
-
OAuthAccountNotLinked
エラーはサーバーアクションで単純にcatch
できない -
OAuthAccountNotLinked
エラーはクエリパラメータとして表示される -
Auth.js
のsignIn
コールバックでハンドリングすると、重複したメールアドレスをユーザーに提示し、適切な回避策を促しやすい
サーバーアクションでcatch
できれば最もありがたいのだが、自分の調査不足かAuth.js
の仕様か、単純なcatch
はできなさそうだった。今回はSignIn
コールバックを使ってハンドリングしたが、より適切なハンドリング方法があればぜひ教えてほしい。
Discussion