【Next.js】NextAuth×Firebaseで認証管理 in appディレクトリ
はじめに
Next.js 13から利用可能になったappディレクトリでは、コンポーネント単位でSSRができるのでよりUXを向上させる実装が可能になりました。
個人開発者の強い味方であるFirebaseと合わせて利用するために、NextAuthとFirebase Authenticationを利用して、サーバサイドでも認証情報を参照できるようにするやり方の紹介です。
今回はEメール&パスワードを用いた認証を利用しますが、基本的にFirebase Authenticationが用意しているどの認証方法でも利用可能です。
前提条件
今回の記事で利用しているライブラリのバージョンは下記のとおりです。
{
"name": "auth-test-app",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@next/font": "13.1.6",
"@types/node": "18.13.0",
"@types/react": "18.0.28",
"@types/react-dom": "18.0.10",
"eslint": "8.34.0",
"eslint-config-next": "13.1.6",
"firebase": "^9.17.1",
"firebase-admin": "^11.5.0",
"next": "13.1.6",
"next-auth": "^4.19.2",
"react": "18.2.0",
"react-dom": "18.2.0",
"typescript": "4.9.5"
}
}
環境構築
Firebaseプロジェクト作成
Firebaseプロジェクトの作成方法
Firebaseコンソールからプロジェクト作成
ここはどちらでも良いです
始めるをクリック
メール / パスワードを選択して以下の通りに設定
プロジェクトの設定からウェブを追加
この後下記firebaseConfigの中身を利用するので、どこかに控えておいてください(後からでも参照できます)
プロジェクト作成後、Authenticationからメール / パスワードプロバイダを有効にしておいてください。
秘密鍵の生成
admin sdkを利用するため、秘密鍵をダウンロードし、プロジェクトルートに配置してください。
このとき、ダウンロードしたjsonファイルの名前を変更し、.gitignore
に記載しておいてください。
※ファイル名の変更は任意です。
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
+ # Firebase
+ firebaseSecretKey.json # ←ダウンロードした秘密鍵
テストアカウント作成
Firebaseコンソールからテスト用アカウントを作成します。
メールアドレス/PWはお好きなものをご利用ください。
開発環境作成
npx create-next-app@latest --experimental-app
コマンド実行ログ
What is your project named? › auth-test-app
Would you like to use TypeScript with this project? › Yes
Would you like to use ESLint with this project? › Yes # ← NoでもOKです
Would you like to use `src/` directory with this project? › Yes # ← NoでもOKです
What import alias would you like configured? › @/*
Creating a new Next.js app in /Users/hiro/src/auth-test-app.
Using npm.
Installing dependencies:
- react
- react-dom
- next
- @next/font
- typescript
- @types/react
- @types/node
- @types/react-dom
- eslint
- eslint-config-next
added 270 packages, and audited 271 packages in 16s
102 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
Initializing project with template: app
Initialized a git repository.
Success! Created auth-test-app at /Users/user/src/auth-test-app
Firebase SDKの設定
.env
プロジェクトのルートディレクトリに.env
を作成し、以下のようにFirebaseのconfigを記述します。
NEXT_PUBLIC_API_KEY=AIzaSyBwX5ygcz9oD-sfe1ESox_Z8ulFeW5Xn_U
NEXT_PUBLIC_AUTH_DOMAIN=auth-test-app-140df.firebaseapp.com
NEXT_PUBLIC_PROJECT_ID=auth-test-app-140df
NEXT_PUBLIC_STORAGE_BUCKET=auth-test-app-140df.appspot.com
NEXT_PUBLIC_MESSAGIN_SENDER_ID=528579025242
NEXT_PUBLIC_APP_ID=1:528579025242:web:517529998dcf25a0ecfb63
firebase/client
Firebase client SDKの初期化設定のため、srcディレクトリ配下にfirebase
ディレクトリを作成し、client.ts
を作成してください。
client SDKはログイン認証で利用します。
import { initializeApp, getApps } from "firebase/app";
import { getAuth } from "firebase/auth";
const firebaseConfig = {
apiKey: process.env.NEXT_PUBLIC_API_KEY,
authDomain: process.env.NEXT_PUBLIC_AUTH_DOMAIN,
databaseURL: process.env.NEXT_PUBLIC_DATABASE_URL,
projectId: process.env.NEXT_PUBLIC_PROJECT_ID,
storageBucket: process.env.NEXT_PUBLIC_STORAGE_BUCKET,
messagingSenderId: process.env.NEXT_PUBLIC_MESSAGING_SENDER_ID,
appId: process.env.NEXT_PUBLIC_APP_ID,
measurementId: process.env.NEXT_PUBLIC_MEASUREMENT_ID,
};
const app = getApps()?.length ? getApps()[0] : initializeApp(firebaseConfig);
export const auth = getAuth(app);
firebase/admin
続いて、Firebase admin SDKの初期化設定のため、firebase
ディレクトリにadmin.ts
を作成してください。
admin SDKはNextAuthのcredential認証の際に、クライアントから送られたトークンの検証に利用します。
import { initializeApp, cert, getApps } from "firebase-admin/app";
import { getAuth } from "firebase-admin/auth";
const serviceAccount = require("/firebaseSecretKey.json");
export const firebaseAdmin =
getApps()[0] ??
initializeApp({
credential: cert(serviceAccount),
});
export const auth = getAuth();
各種必要なライブラリをインストール
NextAuth install
SSRでも認証情報を参照できるようにするために、NextAuthをインストールします。
cd auth-test-app
npm i next-auth
Firebase install
Firebase SDKをインストールします。
サーバ側でトークンの検証を行うために、client SDKの他にadmin SDKもあわせてインストールしておきます。
npm i firebase firebase-admin
NextAuth関連
NEXTAUTH_SECRETの登録
JWTを暗号化し、トークンをハッシュするために利用するシークレットを登録します。
シークレットの値は何でも良いですが、以下コマンドをターミナル等で叩いて生成される値を利用しましょう。
openssl rand -base64 32
# Next Auth
NEXTAUTH_SECRET=rDo15EMKxXGIkjDfbkgoBWgKFg4PwoeLnYzPY6RYNSs=
SessionProviderの作成
NextAuthはクライアントコードでsessionを閲覧(useSession()
を利用)するために、SessionProviderでラップしておく必要があります。
SessionProviderはクライアントコンポーネントで利用しなければいけないですが、Next.js 13のappディレクトリはデフォルトでサーバコンポーネントのため、任意のディレクトリにSessionProvider.tsx
というコンポーネントを作成します。
'use client';
import { SessionProvider as NextAuthSessionProvider } from 'next-auth/react';
export interface SessionProviderProps {
children: React.ReactNode;
}
const SessionProvider = ({ children }: SessionProviderProps) => {
return <NextAuthSessionProvider>{children}</NextAuthSessionProvider>;
};
export default SessionProvider;
先程作成したSessionProvider.tsx
を用いてアプリケーション全体をラップします。
import SessionProvider from "../provider/SessionProvider";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<SessionProvider>
<html lang="ja">
<head />
<body>{children}</body>
</html>
</SessionProvider>
);
}
ログイン画面&クライアント認証作成
ログイン画面を作成し、Firebase client SDKのsignInWithEmailAndPassword
を用いて認証し、idToken
を取得します。
認証後、NextAuthのsignIn
メソッドに対して取得したidToken
と認証後のリダイレクト先URLを渡します。
具体的には、/src/app
にsignin
ディレクトリを作成し、page.tsx
に以下のように記述します。
"use client";
import { useState } from "react";
import { signInWithEmailAndPassword } from "firebase/auth";
import { auth } from "@/firebase/client";
import { signIn as signInByNextAuth } from "next-auth/react";
const SingIn = () => {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const signIn = async () => {
if (!email) return;
if (!password) return;
try {
const userCredential = await signInWithEmailAndPassword(
auth,
email,
password
);
const idToken = await userCredential.user.getIdToken();
await signInByNextAuth("credentials", {
idToken,
callbackUrl: "/",
});
} catch (e) {
console.error(e);
}
};
return (
<div>
<input
type="email"
value={email}
onChange={(event) => setEmail(event.target.value)}
placeholder="メールアドレス"
/>
<input
type="password"
value={password}
onChange={(event) => setPassword(event.target.value)}
placeholder="パスワード"
/>
<button
type="button"
onClick={() => {
signIn();
}}
>
ログイン
</button>
</div>
);
};
export default SingIn;
NextAuth configの設定
NextAuthでクライアントから送られたトークンを検証し、セッションに格納する設定を記述します。
トークンの検証
authOption
のCredentialsProvider
でクライアントから送られたidToken
の検証を行います。
ユーザ認証が完了した際に実行するコールバックをauthorize
プロパティに記述します。
第1引数にcredentials
を受け取ります。
ここで受け取るcredentials
は先程NextAuthのsignIn
メソッドに渡したidToken
が渡ってくるので、分割代入で受け取ります。
その後、admin SDKを利用して受け取ったidToken
が正しいものか検証しつつ、ユーザ情報を返します。
また、サーバコンポーネントでセッションを確認する際にここで定義したauthOption
を渡す必要があるため、宣言と同時にexport
します
import NextAuth from 'next-auth';
import type { NextAuthOptions } from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
import { auth } from '@/firebase/admin';
export const authOptions: NextAuthOptions = {
providers: [
CredentialsProvider({
credentials: {},
authorize: async ({ idToken }: any, _req) => {
if (idToken) {
try {
const decoded = await auth.verifyIdToken(idToken);
return { ...decoded };
} catch (err) {
console.error(err);
}
}
return null;
},
}),
],
};
export default NextAuth(authOptions);
続いて、authorize
で返したユーザ情報をSessionに格納するために、以下を追記します。
import NextAuth from "next-auth";
import type { NextAuthOptions } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import { auth } from "@/firebase/admin";
export const authOptions: NextAuthOptions = {
providers: [
CredentialsProvider({
credentials: {},
authorize: async ({ idToken }: any, _req) => {
if (idToken) {
try {
const decoded = await auth.verifyIdToken(idToken);
return { ...decoded };
} catch (err) {
console.error(err);
}
}
return null;
},
}),
],
+ session: {
+ strategy: "jwt",
+ },
+ callbacks: {
+ async jwt({ token, user }) {
+ return { ...token, ...user };
+ },
+ // sessionにJWTトークンからのユーザ情報を格納
+ async session({ session, token }) {
+ session.user.emailVerified = token.emailVerified;
+ session.user.uid = token.uid;
+ return session;
+ },
+ },
};
export default NextAuth(authOptions);
callbacks
のjwt
からreturn
した値がsession
の引数から参照できるようになり、session
でreturn
した値がアプリケーションコードで参照するsession情報に格納されます。
流れは以下のとおりです。
-
authorize
でユーザ認証し、取得したユーザ情報を返す -
callbacks
のjwt
で、1から返されたユーザ情報を取得し、返す -
callbacks
のsession
で、2から返されたユーザ情報を取得し、Sessionに格納する - アプリケーションコードで
useSession()
やgetServerSession()
を利用して3から返された情報を取得する
型定義
TypeScriptを利用していると、Firebaseのユーザ情報を参照している箇所で型エラーが出てしまうため、型定義をします。
import NextAuth, { DefaultSession } from 'next-auth';
import { JWT } from 'next-auth/jwt';
declare module 'next-auth' {
interface Session {
user: {
// Firebaseの認証情報
uid: string;
emailVerified?: boolean;
} & DefaultSession['user'];
}
}
declare module 'next-auth/jwt' {
interface JWT {
// Firebaseの認証情報
uid: string;
emailVerified: boolean;
}
}
ログイン後の画面でユーザ情報を取得
ログイン後にリダイレクトする画面(今回は/
)でログインしているユーザ情報を取得します。
クライアントコンポーネント、サーバコンポーネントで取得方法が異なるので、それぞれの取得方法を記述しておきます。
クライアントコンポーネント
"use client";
import { useSession } from "next-auth/react";
const ClientComponent = () => {
const { data: session } = useSession();
const user = session?.user;
return <p>{JSON.stringify(user)}</p>;
};
export default ClientComponent;
サーバコンポーネント
import { getServerSession } from "next-auth/next";
// path/toは適宜書き換えてください
import { authOptions } from "path/to/api/auth/[...nextauth]";
const ServerComponent = async () => {
const session = await getServerSession(authOptions);
const user = session?.user;
return <p>{JSON.stringify(user)}</p>;
};
export default ServerComponent;
確認
/app/page.tsx
を以下のように書き換えて、npm run dev
を実行後、http://localhost:3000/signin
にアクセスしてログインをすると下記ページにリダイレクトしてユーザ情報が表示されます。
import ClientComponent from "@/components/ClientComponent";
import ServerComponent from "@/components/ServerComponent";
const Home = async () => {
return (
<main>
<ClientComponent />
{/* @ts-ignore */}
<ServerComponent />
</main>
);
};
export default Home;
まとめ
以上がNextAuth×Firebaseで認証する方法でした。
今回はEメール/パスワードのみでしたが、Firebase Authenticationが用意している他のプロバイダを利用しても、idToken
を検証すれば良いのでどのやり方でも利用できると思います。
また、どこかのホスティングサービスにデプロイする場合はデプロイ先によって多少設定が異なるので、下記公式ドキュメントを参照してください。
(.env.local
に設定した環境変数と、adminSDKで利用しているjsonファイルをホスティングサービスに登録するのも忘れずに)
Discussion
大変学びになる記事ありがとうございます!
手順に従いサーバサイドで認証情報を取得することには成功したのですが、セキュリティールールを設定したところ
Internal error: FirebaseError: Missing or insufficient permissions.とエラーとなり
request.auth
がnullとして返ってくる問題が起きています。なにか特別な設定をする必要があるでしょうか?
thanks, your code helped me solve this problem.