【Next.js】Auth.jsの資格情報プロバイダーで認証機能を実装する
はじめに
フロントエンドの認証機能をAuth.jsを使って実装しました。
今回は既存のバックエンド認証を流用したいので、資格情報プロバイダ(CredentialsProvider)を使用します。
資格情報認証は推奨していないようです
本来、Auth.jsでは組込プロバイダ(Google, Facebook, Twitterなど)を使用することで、手軽に安全に認証機能を実装することができます。
公式ドキュメントの説明では資格情報認証はパスワードに関するセキュリティティリスクがあるため推奨していないように見えます。
以下翻訳です。
おっしゃる通り、パスワード流出すると大変ですよね。
でも、既存のバックエンド連携があるんです。ごめんなさいCredentialsProvider使っちゃいます。
ざっくり概要
今回実装した認証機能のポイントを整理しておきます。
ユーザから見える画面としては認証画面と認証後画面の2つを作成しました。
また、下記機能を実装するためにAuth.jsの設定をしていきます。
- セッション情報を拡張する
デフォルトのセッション情報ではname
、email
、image
などが用意されていますが、これを拡張して、ロールやバックエンド用アクセストークンなど、デフォルト以外の情報を保持させます。 - 自作の認証画面を使用する
Auth.jsでは標準で認証ページを自動生成してくれますが、バリデーションや見た目のカスタマイズが自由にできないため、自作の認証画面を作成します。ユーザ名とパスワードを入力する画面を想定しました。 - 全ページに認証をかける
middlewareを使用して、すべてのページを保護します。認証していないユーザがページにアクセスしようとすると、認証画面にリダイレクトします。
作ってみる
前提としてNext.js/TypeScriptを使用します。
プロジェクトの作成
create-next-app
でプロジェクトを作成します。
npx create-next-app my-auth-app --ts
これでNext.jsのひな形が完成しました。
Auth.jsのインストール
next-auth
をインストールします。元々はNextAuth.jsという名前だったようです。
cd my-auth-app
npm install next-auth
[...nextauth].tsの作成
ここからAuth.jsの設定をしていきます。
認証機能の定義ファイルをpages/api/auth/[...nextauth].ts
に作成します。
CredentialsProvider
を設定しています。
セッション情報のプロパティで型エラーが出ると思いますが、次の手順で型情報を拡張します。
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
export default NextAuth({
providers: [
CredentialsProvider({
name: "Credentials",
// `credentials`は、サインインページでフォームを生成するために使用されます。
credentials: {
username: { label: "ユーザ名", type: "text" },
password: { label: "パスワード", type: "password" },
},
async authorize(credentials, req) {
// `credentials`で定義した`username`、`password`が入っています。
// ここにロジックを追加して、資格情報からユーザーを検索します。
// 本来はバックエンドから認証情報を取得するイメージですが、ここでは定数を返しています。
// const user = await authenticationLogic(credentials?.username, credentials?.password);
const user = {
id: "1",
name: "J Smith",
email: "jsmith@example.com",
role: "admin",
backendToken: "backEndAccessToken",
};
if (user) {
// 返されたオブジェクトはすべて、JWT の「user」プロパティに保存されます。
return user;
} else {
// 認証失敗の場合はnullを返却します。
return null;
}
},
}),
],
pages: {
// カスタムログインページを追加します。
signIn: "/auth/signin",
},
callbacks: {
// `jwt()`コールバックは`authorize()`の後に実行されます。
// `user`に追加したプロパティ`role`と`backendToken`を`token`に設定します。
jwt({ token, user }) {
if (user) {
token.role = user.role;
token.backendToken = user.backendToken;
}
return token;
},
// `session()`コールバックは`jwt()`の後に実行されます。
// `token`に追加したプロパティ`role`と`backendToken`を`session`に設定します。
session({ session, token }) {
session.user.role = token.role;
session.user.backendToken = token.backendToken;
return session;
},
},
});
next-auth.d.tsの作成
types/next-auth.d.ts
を作成し、型情報を拡張します。セッション情報にrole
とbackendToken
を追加しました。任意のプロパティを持たせたい場合はこのファイルに型情報を持たせます。
import NextAuth, { DefaultSession } from "next-auth";
import { JWT } from "next-auth/jwt";
declare module "next-auth" {
// クライアント側で使用するsession(useSessionから取得するオブジェクト)にプロパティを追加します。
// ここでは`role`と`backendToken`を追加しています。
interface Session {
user: {
role?: string;
backendToken?: string;
} & DefaultSession["user"];
}
interface User {
role?: string;
backendToken?: string;
}
}
declare module "next-auth/jwt" {
// "jwt"コールバックのtokenパラメータに任意のプロパティを追加します。
interface JWT {
role?: string;
backendToken?: string;
}
}
型情報については下記を参考にしました。
.envの作成
環境変数NEXTAUTH_SECRET
とNEXTAUTH_URL
を定義します。
NEXTAUTH_SECRET=TESTSECRET
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET
はJWTの暗号化に使用します。公開しないようにしてください。openssl
コマンドでランダムな文字列を生成すると良いそうです。
openssl rand -base64 32
_app.tsxの修正
SessionProvider
を追加すると、クライアント側でセッション情報が参照可能になります。
ここでは見た目は重視しないため、globals.css
は削除しました。
import type { AppProps } from "next/app";
import { SessionProvider } from "next-auth/react";
function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
return (
<SessionProvider session={session}>
<Component {...pageProps} />
</SessionProvider>
);
}
export default MyApp;
index.tsxの修正
セッション情報に格納したname
、email
、role
、backendToken
を表示するように修正しました。サインアウトボタンもつけています。
import { useSession, signOut } from "next-auth/react";
export default function Home() {
const { data: session, status } = useSession();
return (
<main>
<div>{status}</div>
<div>{session?.user?.name}</div>
<div>{session?.user?.email}</div>
<div>{session?.user?.role}</div>
<div>{session?.user?.backendToken}</div>
<button onClick={() => signOut()}>サインアウト</button>
</main>
);
}
signin.tsxの作成
自作の認証画面を作成します。ユーザ名とパスワードを入力する画面を作成しました。
import { GetServerSideProps } from "next";
import { getCsrfToken } from "next-auth/react";
import { useRouter } from "next/router";
type SignInProps = {
csrfToken?: string;
};
export default function SignIn({ csrfToken }: SignInProps) {
const router = useRouter();
const { error } = router.query;
return (
<form method="post" action="/api/auth/callback/credentials">
<input name="csrfToken" type="hidden" defaultValue={csrfToken} />
<label>
ユーザ名
<input name="username" type="text" />
</label>
<label>
パスワード
<input name="password" type="password" />
</label>
<button type="submit">サインイン</button>
{error && <div>サインイン失敗</div>}
</form>
);
}
export const getServerSideProps: GetServerSideProps = async (context) => {
return {
props: {
csrfToken: await getCsrfToken(context),
},
};
};
下記を参考にしました。
middleware.tsの作成
middlewareを作成して、すべてのページを保護します。
export { default } from "next-auth/middleware";
export const config = {
matcher: ["/((?!auth).*)"],
};
ただし、認証画面にもmiddleware
が働いて、リダイレクトループしまうため、matcher
の正規表現でauth
パス配下は除外しています。
動作確認
さて、準備が整いました。実行してみましょう。
npm run dev
localhost:3000
にアクセスしてみましょう。
インデックスページpages\index.tsx
が表示されるところですが、認証されていないため、サインインページpages\auth\signin.tsx
にリダイレクトされました。成功です。
サインインボタンをクリックしてみます。
期待通りname
、email
、role
、backendToken
の値が表示されました。
サインアウトボタンをクリックすると、サインインページにリダイレクトされました。
まとめ
Auth.jsで認証機能を作成することができました。
落ち着いて公式ドキュメントを見てみると一応書いてあるのですが、作成には少々苦労しました。
現状ではドキュメントも一部バージョンアップに追いついていない様子でした。
最後に個人的つまづきポイントを書いておきます。
- middlewareがループする
仕様なのかバグなのかわかりませんが、signin
ページに繰り返しリダイレクトされてしまいます。除外ページで回避しました。 - .envの作成忘れ
NEXTAUTH_SECRET
とNEXTAUTH_URL
の説明を読み飛ばしていたのか、.env
を作成しておらず、しばらく悩みました。ターミナルにメッセージが表示されたのでかろうじて気が付きました。 - セッション情報までの変数引き渡しがわかりづらい
authorize()
からuseSession()
までの動線を理解するのに時間がかかりました。
authorize()
のuser
オブジェクト
⇒ コールバック関数jwt()
のtoken
オブジェクト
⇒ コールバック関数session()
のsession
オブジェクト
⇒ クライアント関数useSession()
のsession
オブジェクト
という変数の引き渡しをしないといけないと理解するのに少々時間がかかりました。
CredentialsProvider
の使用を思いとどまらせるために、あえて複雑にしているのかもしれません。
以上です。Auth.jsの実装など参考にして頂けると幸いです。
Discussion