Auth.js(NextAuth.js v5)とNext.jsで認証機能を実装する(Credentials Provider)
はじめに
本記事は,Auth.js(NextAuth.js v5)とNext.js14(App Router)を用いて認証機能(Credentials Provider)を実装したものです.
バージョン情報
"next": "14.2.5",
"next-auth": "5.0.0-beta.20",
Auth.jsについて
- Auth.jsは、認証機能を提供するライブラリ
- Next.js,Qwik,SvelteKitなどのフレームワークに対応している
install
npm install next-auth@beta
secretの設定
tokenのハッシュ化のためにAUTH_SECRETを設定する必要がある
以下でsecretを生成しAUTH_SECRETに設定する
npx auth secret
or
openssl rand -base64 33
環境変数に設定する
AUTH_SECRET=secret
Providerについて
Providerは認証方法を提供するもので、OAuth,Email,Credentialsなどがある
Credentials providerについて
Auth.jsを外部のAPI等と統合する場合などに使用する
email,passwordなどの認証情報を後述のauthorize
callbackに渡すことで認証を行う
Database Adapterについて
Auth.jsはデフォルトでSessionをCookieに保存するため,データベースの設定はOptional
今回はCookieを使用してSessionを管理します.
NextAuthConfigについて
Credentials providerを設定するために,NextAuth()メソッドにNextAuthConfigを渡す必要がある
NextAuthConfigについて
providers,callbacks,pages,session,strategyなどの任意のプロパティを設定することで,認証機能をカスタマイズできる
callbacksについて
認証に関連するイベントが発生した際に呼び出される非同期関数.
DBを使用せずにアクセス制御を実装したり,外部のDBやAPIと統合するなどが可能
jwt,session,signIn,redirect,authorizeを設定できる
参考:https://authjs.dev/reference/nextjs#callbacks
実装
Auth.jsに関する全般的な設定(auth.config.ts)
NextAuthConfigを行うファイル
import type { NextAuthConfig, Session, User } from "next-auth";
import { JWT } from "next-auth/jwt";
import { NextRequest } from "next/server";
export const authConfig = {
pages: {
signIn: "signin",
},
callbacks: {
// Middlewareでユーザーの認証を行うときに呼び出される
// NextResponseを返すことでリダイレクトやエラーを返すことができる
authorized({
auth,
request: { nextUrl },
}: {
auth: Session | null;
request: NextRequest;
}) {
console.log("authorized", auth, nextUrl.pathname);
// /user以下のルートの保護
const isOnAuthenticatedPage = nextUrl.pathname.startsWith("/user");
if (isOnAuthenticatedPage) {
const isLoggedin = !!auth?.user;
if (!isLoggedin) {
// falseを返すと,Signinページにリダイレクトされる
return false;
}
return true;
}
return true;
},
// JSON Web Token が作成されたとき(サインイン時など)や更新されたとき(クライアントでセッションにアクセスしたときなど)に呼び出される。ここで返されるものはすべて JWT に保存され,session callbackに転送される。そこで、クライアントに返すべきものを制御できる。それ以外のものは、フロントエンドからは秘匿される。JWTはAUTH_SECRET環境変数によってデフォルトで暗号化される。
// セッションに何を追加するかを決定するために使用される
async jwt({ token, user }: { token: JWT; user: User }) {
console.log("jwt", token, user);
if (user) {
token.backendToken = user.backendToken;
token.user = user;
}
return token;
},
//セッションがチェックされるたびに呼び出される(useSessionやgetSessionを使用して/api/sessionエンドポイントを呼び出した場合など)。
// 戻り値はクライアントに公開されるので、ここで返す値には注意が必要!
// jwt callbackを通してトークンに追加したものをクライアントが利用できるようにしたい場合,ここでも明示的に返す必要がある
// token引数はjwtセッションストラテジーを使用する場合にのみ利用可能で、user引数はデータベースセッションストラテジーを使用する場合にのみ利用可能
// JWTに保存されたデータのうち,クライアントに公開したいものを返す
async session({ session, token }: { session: Session; token: JWT }) {
console.log("session", session, token);
session.backendToken = token.backendToken;
session.user = token.user;
return session;
},
},
providers: [],
} satisfies NextAuthConfig;
Credentials Providerの設定(auth.ts)
Credentials Provider設定を行い,Auth.jsのAPI(auth,signIn,signOut)をexportする
import NextAuth from "next-auth";
import { authConfig } from "./auth.config";
import credentials from "next-auth/providers/credentials";
// auth: Next.jsアプリでNextAuth.jsとやりとりするための汎用メソッド。auth.ts(このファイル)でNextAuth.jsを初期化した後、Middleware、ServerComponents、Route Handler(app router)でこのメソッドを使う
//
// signIn: providerを指定してサインインすることができる。指定されていない場合、ユーザはサインインページにリダイレクトされる。デフォルトでは、ユーザはサインイン後に現在のページにリダイレクトされます。redirectToオプションに相対パスを設定することで、この動作をオーバーライドできる。
//
// signOut: ユーザーをサインアウトする。セッションがデータベース戦略を使用して作成された場合、セッションはデータベースから削除され、関連するクッキーは無効になります。セッションがJWTを使用して作成された場合、クッキーは無効になる.デフォルトでは、サインアウト後、ユーザーは現在のページにリダイレクトされます。redirectTo オプションに相対パスを設定することで、この動作をオーバーライドできます。
//
// handlers: 今回は使用しない
// NextAuth.jsのRouteHandlerメソッド。これらは、OAuth/Emailプロバイダー用のエンドポイント、および(`/api/auth/session`のような)クライアントから接続できるREST APIエンドポイントを公開するために使用されます。
export const { auth, signIn, signOut, handlers } = NextAuth({
...authConfig,
providers: [
credentials({
// signInが呼ばれた際にこの関数が呼び出される
async authorize({ email, password }) {
console.log("authorize:", email, password);
// 実際にはここでバックエンドにリクエストを送信して認証を行う
const url = process.env.API_URL + "/auth/login";
const res = await fetch(url, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({ email, password }),
});
const data = await res.json();
const backendToken = data.accessToken;
const user = { backendToken };
console.log("token:", backendToken);
if (!backendToken) {
// 認証に失敗した場合は nullを返すか,エラーを投げることが期待される
// CredentialsSignin がスローされた場合、または null が返された場合、以下の 2 つのことが起こる:
// 1. URL に error=CredentialsSignin&code=credentials を指定して、ユーザーをログインページにリダイレクトする。
// 2. フォームアクションをサーバーサイドで処理するフレームワークでこのエラーを投げる場合(例えばserver actionsでsignInを呼び出す場合)、このエラーはログインフォームアクションによって投げられるので、そこで処理する必要がある。
return null;
}
return user;
},
}),
],
});
ルートの保護(middleware.ts)
Next.jsのmiddleware
任意のrouteの保護,sessionの維持に使用する
import { auth } from "./auth";
// NextAuthConfigのauthorized callbackが呼び出される
export default auth;
export const config = {
matcher: ["/((?!api|_next/static|_next/image|.*\\.png$).*)"],
};
参考:https://authjs.dev/getting-started/session-management/protecting
型の拡張(types/next-auth.d.ts)
User,Session,JWTの型を拡張する
これを行わないとcallbacksで型エラーが発生するので注意
import { Session } from "inspector";
import { Session, User as DefaultUser, DefaultSession } from "next-auth";
import { DefaultJWT, JWT } from "next-auth/jwt";
declare module "next-auth" {
// ログインユーザーのセッション情報,auth(),useSession(),getServerSession()で使用可能
interface Session extends DefaultSession {
backendToken?: string;
user?: {
backendToken?: string;
} & DefaultSession["user"];
}
//jwt callbackとsession callbackで使用可能。データベースを使用する場合は、session callbackの2番目のパラメータ。
interface User extends DefaultUser {
backendToken?: string;
}
}
declare module "next-auth/jwt" {
// JWT session使用時にjwt callbackで返されるオブジェクトの形状
interface JWT extends DefaultJWT {
backendToken?: string;
user?: User;
}
}
Server Actionsでの使用
login(Server Actions)の実装例
"use server";
import { redirect } from "next/navigation";
import { signIn } from "../../../../auth";
import { AuthError } from "next-auth";
import { isRedirectError } from "next/dist/client/components/redirect";
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
export const signin = async (
_state: LoginState,
formData: FormData
): Promise<LoginState> => {
// login処理
try {
// NEXT_REDIRECTが投げられ,catchでリダイレクトされる
await signIn("credentials", formData);
return { message: "success" };
} catch (error) {
if (error instanceof AuthError) {
switch (error.type) {
case "CredentialsSignin":
console.error("Signin error:", error);
return {
message: "メールアドレスまたはパスワードが間違っています",
};
}
}
// リダイレクトエラーの場合はリダイレクト
if (isRedirectError(error)) {
redirect("/user");
}
return {
message: "An unexpected error occurred during signin",
};
}
};
ログインページの実装例
"use client";
import { useFormState } from "react-dom";
import { LoginState, signin } from "../_action/action";
export default function SignInPage() {
const initialState = {
errors: {},
} satisfies LoginState;
const [state, dispatch] = useFormState(signin, initialState);
return (
<div className="relative flex h-screen flex-col justify-center overflow-hidden">
<div className="m-auto w-full rounded bg-white p-6 shadow-md md:max-w-lg">
<h1 className="text-center text-3xl font-semibold text-primary">
SIGN IN
</h1>
{/* エラーメッセージを表示 */}
{state.message && (
<div className="text-red-500 text-center">{state.message}</div>
)}
<form action={dispatch} className="space-y-4">
{/* email */}
<div>
<label className="label">
<span className="label-text text-base">email</span>
</label>
<input
className="input input-bordered input-primary w-full"
defaultValue={""}
name="email"
type="email"
/>
</div>
{/* password */}
<div>
<label className="label">
<span className="label-text text-base">password</span>
</label>
<input
className="input input-bordered input-primary w-full"
defaultValue={""}
name="password"
type="password"
/>
</div>
<div>
<button className="btn btn-primary" type="submit">
Login
</button>
</div>
</form>
</div>
</div>
);
}
その他
ハマったこと
NEXT_REDIRECT エラーについて
signIn()が成功した場合NEXT_REDIRECTエラーが投げられる
そのためtry-catchでsignIn()を呼び出す場合は,catchでリダイレクト処理を行う必要があるっぽい?
上記のより良い方法があれば教えてください
"use server";
import { redirect } from "next/navigation";
import { signIn } from "../../../../auth";
import { AuthError } from "next-auth";
import { isRedirectError } from "next/dist/client/components/redirect";
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
export const signin = async (
_state: LoginState,
formData: FormData
): Promise<LoginState> => {
// login処理
try {
// NEXT_REDIRECTが投げられ,catchでリダイレクトされる
await signIn("credentials", formData);
return { message: "success" };
} catch (error) {
if (error instanceof AuthError) {
switch (error.type) {
case "CredentialsSignin":
console.error("Signin error:", error);
return {
message: "メールアドレスまたはパスワードが間違っています",
};
}
}
// リダイレクトエラーの場合はリダイレクト
if (isRedirectError(error)) {
redirect("/user");
}
return {
message: "An unexpected error occurred during signin",
};
}
};
zodについて
今回は簡略化のために適用していないが,zodなどを使用することが推奨されている
参考:https://authjs.dev/getting-started/authentication/credentials#verifying-data-with-zod
route handlerについて
OAuthやEmail認証などの認証方法を追加する際には、/app/api/auth/[...nextauth]/route.ts
を作成する
Credentials providerでは使用しないため不要
import { handlers } from "@/auth"
export const { GET, POST } = handlers
session取得について
下記URLのv5を参照
なおClient Componentでのsession取得はuseSession
を使用する.そのためSessionProviderを設定する必要がある
今回使用したレポジトリ
// Next.jsとNextAuth.jsを使用した認証機能のサンプル
// 簡易的なAPIサーバー
Discussion