はじめに
業務で認証処理を実装しました。
フロント側で認証する時にNextAuth側の動きが曖昧で混乱したので今後のために整理してみます。
対象の環境情報
| パッケージ名 | バージョン |
|---|---|
| Next.js | 14.0.4 |
| NextAuth | 4.23.2 |
| React | 18.2.0 |
NextAuthの概要
Next.jsアプリケーションに簡単に認証機能を追加するためのライブラリ
以下の認証処理を簡単にNext.jsで実装することができます。
- OAuth(Google、GitHub、Facebookなど)
- Email/Password認証
- 特定のバックエンドを使った認証 (Credentials Provider)
今回は、バックエンドとしてLaravel 11 (Sanctum) を利用することを前提に、3つ目の
「特定のバックエンドを使った認証 (Credentials Provider)」
について解説します。
この記事で扱うこと
- フロントエンド側でNextAuthを使ってどのように認証処理を行うか
- App Routerでの認証処理について
扱わないこと
- Laravel側でトークンを発行する処理について
- PageRouterでの認証処理について
ログイン処理のざっくり流れ
まずは認証の処理を大まかに整理します。
認証処理は「誰が」「何をするのか」を押さえないとそれぞれの役割なんだ?と混乱する思うので、登場人物と役割を大まかに整理します。
登場人物
| だれ | 役割 |
|---|---|
| ユーザ | ログイン情報の入力を行う |
| Next.js(画面側) | 入力情報をNextAuthに渡す |
| NextAuth(認証の仕組み) | 認証の流れを管理する |
| Laravel(サーバー側) | 入力情報が正しいか確認する |
上記を踏まえた上で認証フローの中でNextAuthがどこのタイミングで使われるのかを理解します。
処理フロー
| Step | だれ | 何をするか |
|---|---|---|
| 1 | ユーザ | メールアドレスとパスワードを入力し、「ログイン」ボタンをクリックする |
| 2 | Next.js | 入力値を受け取り、signIn('credentials', { email, password }) を使って NextAuth に送信する |
| 3 | NextAuth |
authorize() 関数が呼び出され、Laravel API に認証リクエストを送信する |
| 4 | Laravel | メール・パスワードを検証し、正しければユーザー情報を返す/間違いなら 401 エラーを返す |
| 5 | NextAuth |
authorize() 内で Laravel の応答を受け取り、成功時:ユーザー情報を返す 失敗時: null を返してログイン失敗扱いにする |
| 6 | NextAuth |
jwt コールバックが実行され、ユーザー情報をトークン(JWT)に保存する |
| 7 | NextAuth(内部処理) | 生成した JWT を暗号化し、セッションクッキーとしてブラウザに保存(自動で処理) |
| 8 | ユーザ | クッキーが保持され、ログイン済み状態でアプリを利用できる |
| 9 | NextAuth(再利用時) | クライアントが useSession() / getServerSession() を呼ぶと、session コールバックが実行され、JWT からセッション情報を復元して返す |
シーケンス図
NextAuthの各処理について
上のフローの中で、NextAuthが関与するのは主に以下のタイミングです。
| 該当Step | 処理箇所 | 主な役割 |
|---|---|---|
| 2 | signIn() |
Next.js側から呼び出される。認証処理を開始する時に呼び出す |
| 3,5 | authorize() |
Laravelに問い合わせて、ログイン可能か確認する |
| 6 |
jwt コールバック |
ユーザー情報をトークン(JWT)に保存する |
| 7 | 内部処理 | JWTを暗号化し、クッキーに保存する(自動) |
| 9 |
session コールバック |
JWTからユーザー情報を読み出してセッションを返す |
jwt(ジョット)については以下を参考
https://zenn.dev/nameless_sn/articles/the_article_of_jwt
では、各処理について説明していきます。
まず最初に、NextAuthの設定ファイルについて説明します。
route.tsについて
NextAuth は、app/api/auth/[...nextauth]/route.ts に設定をまとめて記述します。
ここが「NextAuth全体の設定ファイル」であり、authorize() や jwt() などのコールバックもここに書かれます。
import { type NextAuthConfig } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
export const authConfig: NextAuthConfig = {
providers: [
CredentialsProvider({
name: "credentials",
credentials: {
email: { label: "Email", type: "text" },
password: { label: "Password", type: "password" },
},
async authorize(credentials) {
// ここでLaravelのAPIを叩いて認証(詳細省略)
// 成功時: return user
// 失敗時: return null
},
}),
],
callbacks: {
async jwt({ token, user }) {
// JWTにユーザー情報を保持(初回のみuserが存在)
if (user) {
token.id = user.id;
token.name = user.name;
token.email = user.email;
}
return token;
},
async session({ session, token }) {
// JWTからセッション情報を組み立てる
session.user = {
id: token.id as string,
name: token.name as string,
email: token.email as string,
};
return session;
},
},
};
ログイン処理完了まで
①signIn
STEP2の部分です。
NextAuth の認証処理は、フロントエンド側で signIn() 関数を呼び出すことで開始されます。
この関数を呼ぶと、NextAuth 側で自動的に authorize() が実行され、バックエンド(Laravel)との認証処理が行われます。
import { signIn } from "next-auth/react";
signIn("credentials", {
email: "test@example.com",
password: "password123",
});
signIn() の第1引数に route.tsのCredentialsProviderで設定したnameを指定します。
CredentialsProvider({
name: "credentials", ←ここ!!!!!
credentials: {
email: { label: "Email", type: "text" },
password: { label: "Password", type: "password" },
},
async authorize(credentials) {
// ここでLaravelのAPIを叩いて認証(詳細省略)
// 成功時: return user
// 失敗時: return null
},
}),
],
②authorize()
STEP3,5の部分です。
NextAuthがLaravel(バックエンド)へ問い合わせを行う箇所です。
ここでは、フォームで入力されたメール・パスワードをLaravelのAPIに送信して、
認証が成功すればユーザー情報を返す、失敗すれば null を返すようにします。
第1引数のcredentialsにsignIn()を呼び出した時の第2引数が渡ってきます
今回だとemailとpassword
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
export const authOptions: NextAuthOptions = {
providers: [
CredentialsProvider({
name: "Credentials",
credentials: {
email: { label: "Email", type: "text" },
password: { label: "Password", type: "password" },
},
async authorize(credentials) {
try {
//ログインAPIの呼び出し
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email: credentials?.email,
password: credentials?.password,
}),
});
if (!res.ok) return null; // ← 401などの場合はnullを返して失敗扱い
const user = await res.json();
return user; // ← 成功時にユーザー情報を返す
} catch (e) {
console.error(e);
return null;
}
},
}),
],
// ...jwtやsessionなどのコールバック設定
});
③jwtコールバック
step6の処理です
authorize() から返された user がここに渡ってきます。
ここではJWTの中に、セッション維持に必要な情報だけを保存します。
セッションIDなど
export const authOptions: NextAuthOptions = {
...
callbacks: {
async jwt({ token, user }) {
// 初回ログイン時のみ user が存在する
if (user) {
token.id = user.id;
token.name = user.name;
token.email = user.email;
}
return token;
},
},
});
④Cookie保存(内部処理)
step7の部分です。
この部分はNextAuthの内部処理です。
開発者が明示的にコードを書く必要はありません。
NextAuthが自動的にJWTを暗号化してnext-auth.session-token というCookie HttpOnlyとしてブラウザに保存します。
Cookie HttpOnlyとは?
JavaScriptから参照できないCookieのこと
JSからは直接参照できない。
これにより、XSS攻撃でもセッションが盗まれにくい構造になっている。
ここまででログイン処理が完了します
セッション情報の問い合わせがあった時
sessionコールバック
Next.js側からuseSession() または getServerSession() を呼び出したときに実行されます。
ここで、JWTに入っているトークン情報をもとに、返すセッション情報を整形します。
以下でログイン中のユーザ情報を取得してます。
export default NextAuth({
...
callbacks: {
async session({ session, token }) {
session.user = {
id: token.id,
name: token.name,
email: token.email,
};
return session;
},
},
});
※上記の方式だとユーザ情報が裏で削除された場合にセッションが残り続けてしまうので、都度ユーザ情報を返却するAPIをコールしたほうがいいかも?
まとめ
NextAuthを使った認証処理について整理してみました。
認証処理って複雑でなんとなく苦手意識があるのですが、処理フローと登場人物と役割を整理することで、理解が深まりました。
CookieHttpOnlyって存在を初めて知りました・・・
セッションコールバックは都度APIを呼び出さないとユーザ情報の存在を担保できないのでは?と思ったのでもう少し調べてみたいです。
参考
