NextAuth.js と Firebase Authentication の連携
はじめに
NextAuth.js は Next.js のために作られた OSS の認証ライブラリです。
このライブラリは主に OAuth もしくは Email で認証したユーザーの情報やセッションを連携したデータベースで自動管理できるのが売りです。他の認証システムで既に認証済みのユーザーを管理する方法もありはするのですが、OAuth や Email 認証に比べるとあまり分かりやすいサンプルコードがありません。防備録も兼ねて、ここで知見を共有したいと思います。
なお、NextAuth ではログイン中のユーザーの情報は firebase.auth.User
を React Context で保持するようなことはできず、あくまでユーザー ID など JSON にシリアライズして(JWT のペイロードに含めて)管理できるものだけが対象となります。この記事では Firebase Authentication が持つ ID トークンをアプリで利用し、Cookie にログインユーザーの情報を JWT として保存してログイン状態を保てるようにすることを目的とします。
NextAuth.js や Firebase Authentication 自体への入門は膨大になってしまうのでこの記事では書きません。
ソースコード
Vercel にデプロイしたものは下リンクにあります。
準備
Next.js アプリの新規作成
npx create-next-app --ts nextauth-firebaseauth-example
cd nextauth-firebaseauth-example
npm install next-auth@beta firebase firebase-admin
パスエイリアス
これは個人の好みですが、コードは全てプロジェクトルートに src/
ディレクトリを作成してその中で管理し、 @/
のエイリアスを貼っておきます。
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
Firebase の設定
Firebase コンソールから新規プロジェクトを作成し、
- ウェブアプリの有効化
- Firebase Admin SDK 用の秘密鍵の作成(JSON がダウンロードされる)
をしておきます。
ウェブアプリの設定値と、管理者用秘密鍵の project_id
・client_email
・private_key
を .env.local
にコピペし、環境変数に含めます(参考:柔軟に firebase admin を初期化する)。
# Firebase SDK の初期化に必要
NEXT_PUBLIC_FIREBASE_API_KEY=<apiKey>
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=<authDomain>
NEXT_PUBLIC_FIREBASE_PROJECT_ID=<projectId>
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=<storageBucket>
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=<messagingSenderId>
NEXT_PUBLIC_FIREBASE_APP_ID=<appId>
NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID=<measurementId>
# Firebase Admin SDK の初期化に必要
FIREBASE_PROJECT_ID=<project_id>
FIREBASE_CLIENT_EMAIL=<client_email>
FIREBASE_PRIVATE_KEY=<private_key>
Firebase ライブラリの初期化
Firebase SDK と Firebase Admin SDK を初期化します。
Firebase SDK
import { initializeApp } from "firebase/app";
import type { FirebaseOptions } from "firebase/app";
const firebaseConfig: FirebaseOptions = {
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
databaseURL: process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL,
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
measurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID,
};
export const firebaseApp = initializeApp(firebaseConfig);
Firebase Admin SDK
import * as admin from "firebase-admin";
import type { ServiceAccount } from "firebase-admin";
const cert: ServiceAccount = {
projectId: process.env.FIREBASE_PROJECT_ID,
clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
privateKey: process.env.FIREBASE_PRIVATE_KEY.replace(/\\n/g, "\n"),
};
export const firebaseAdmin =
admin.apps[0] ??
admin.initializeApp({
credential: admin.credential.cert(cert),
});
NextAuth の設定
公式ドキュメントの Example Code に従い SessionProvider
を導入します。
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;
認証フロー
先に認証フローを把握しておきましょう。
- ユーザーはアプリのログインボタンを押し、Firebase Authentication がユーザー認証を完了する。この時点では、Firebase Authentication へのユーザー登録はされているがアプリには何も反映されていない。
- Firebase SDK は登録ユーザーの ID トークンをアプリに渡す。
- アプリは受け取った ID トークンを JWT で Cookie に保存し、アプリでのユーザー認証を完了する。
ログインページ
ここでは Firebase Authentication でログインし、ID トークンを得ることが目的です。ログインプロバイダには GitHub と Google を選びました。
import { signIn } from "next-auth/react";
import {
getAuth,
signInWithPopup,
GithubAuthProvider,
GoogleAuthProvider,
} from "firebase/auth";
import type { AuthProvider } from "firebase/auth";
import { firebaseApp } from "@/lib/firebase";
export default function singIn() {
const auth = getAuth(firebaseApp);
const githubProvider = new GithubAuthProvider();
const googleProvider = new GoogleAuthProvider();
const handleOAuthSignIn = (provider: AuthProvider) => {
signInWithPopup(auth, provider)
// 認証に成功したら ID トークンを NextAuth に渡す
.then((credential) => credential.user.getIdToken(true))
.then((idToken) => {
signIn("credentials", { idToken });
})
.catch((err) => console.error(err));
};
return (
<>
<p>Choose your sign-in method:</p>
<button onClick={() => handleOAuthSignIn(githubProvider)}>GitHub</button>
<br />
<button onClick={() => handleOAuthSignIn(googleProvider)}>Google</button>
</>
);
}
重要なのは handleOAuthSignIn()
の中の以下の処理です。
signIn("credentials", { idToken });
signIn()
関数に渡す引数は 2 つあり、
- 文字列
"credentials"
: NextAuth で使用するプロバイダの ID
NextAuth は外部認証システムの情報を用いる際にはCredentialsProvider
というプロバイダを用い、CredentialsProvider
の ID は"credentials"
です。 - オブジェクト
{ idToken: idToken }
:CredentialsProvider
がユーザー認証にするのに必要な値
NextAuth API ルート
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import { firebaseAdmin } from "@/lib/firebaseAdmin";
export default NextAuth({
providers: [
CredentialsProvider({
authorize: async (credentials, req) => {
const { idToken } = credentials;
if (idToken != null) {
try {
const decoded = await firebaseAdmin.auth().verifyIdToken(idToken);
return { ...decoded };
} catch (error) {
console.log("Failed to verify ID token:", error);
}
}
return null;
},
}),
],
callbacks: {
jwt: async ({ token, user }) => {
if (user) {
token = user;
}
return token;
}
}
});
CredentialsProvider
先のログインページで出た
signIn("credentials", { idToken });
の { idToken }
は authorize(credentials, req)
の第 1 引数に credentials
として渡されるオブジェクトです。
authorize()
では firebase-admin
により ID トークンを JSON にデコードして検証が通ればそのトークンを返し、検証に失敗したらエラーとして null
を返します。
JWT コールバック
callbacks
オプションの jwt()
はログイン時など、JWT が作成・更新されたとき呼ばれるコールバックです。
jwt({ token, user })
の引数のオブジェクトプロパティのうち、token
は JWT を、user
は authorize()
で返ってくるオブジェクトを表します。つまりここでの user
の実体は DecodedIdToken
です。
上のコードでは NextAuth の発行する JWT のペイロードをデコードされた ID トークンに置き換えていますが、例えば user
の持っている情報のうち一部のみが必要でればそのように書き換えることも可能です。
callbacks: {
jwt: async ({ token, user }) => {
if (user) {
token.sub = user.sub;
}
return token;
}
}
作成された JWT は next-auth.session-token
として Cookie に保存され、jwt.io などでこの Cookie をデコードすると、ペイロードは HS512 で署名された Firebase Authentication の ID トークンに一致することが確認できます。
※ HS512 は NextAuth のデフォルトでの署名アルゴリズム
認証は以上となります。
参考記事
- NextAuth.js Documentation
- Firebase の存在をフロントエンドから隠蔽するために
- next.js + vercel + firebase authentication で JWT の検証を行う + Graphql
- Using NextAuth.js with Magic links (CredentialsProvider で分散型認証システム Magic と連携する手順)
Discussion
こんばんは。ご存知かもしれませんが、firebase用のadapterを使うとよりシンプルに書けるかもしれません。
自分で使ったことはないこととまだstableでないため、使用感などお伝えできませんが、参考になればと思いコメントしました👍
コメントありがとうございます。
実は自分も最初はその adapter で実装できると思っていました。が、実際に使用してみると分かるとおり、NextAuth は Firebase Authentication とは別に Firestore にユーザー管理用のコレクションを作ってしまいます(コンソールで確認すると
accounts
,sessions
,users
が自動で作成されます)。adapter の初期化に firestore を要求してくるのはこういう訳ですね。
確かに今回は認証のみにフォーカスした内容でDBまでは不要でしたね :pray:
スクショまで貼っていただきありがとうございます。firebase を使うときに記事内容参考にさせて頂こうと思います😊
I dont understand japanese but I could understand all the code, thanks for your colaboration.