Next.js と Auth.js で実装する Google Cloud Identity Platform 認証
Next.js と Auth.js を使って、Google Cloud Identity Platform (GCIP) による ID/PW 認証を検証します。GCIP の特徴や制約、Auth.js を用いた認証フローの構築手順を紹介します。
Google Cloud Identity Platform (GCIP) の特徴
GCIP は Firebase Authentication の拡張版として便利な認証基盤を提供します。
しかし、単体では OIDC/SAML の IdP として利用できず、認可機能も備えていないため、用途によっては対応が難しい場面があります。 シンプルな認証基盤としての利便性は高いものの、柔軟なカスタマイズを求める場合は、その制約を理解した上での運用が必要です。
次からは私が気になった特徴をいくつかピックアップしてみます。
GCIP は OIDC/SAML の Identity Provider (IdP) には対応していない
一般的な認証プラットフォームは OIDC や SAML を利用した認証プロトコルを提供しますが、GCIP は OIDC/SAML の IdP としての機能を持っていません。
例えば、OIDC の /authorize
や /token
エンドポイントを提供していないため、IdP として直接利用することはできません。
GCIP を活用するには、次のようにアプリケーションに Firebase SDK を組み込み、認証を行う形が基本となります。
GCIP は OIDC/SAML の Service Provider (SP) に対応している
わかりづらいですが、GCIP は IdP としての機能は提供しませんが、SP としての機能は利用できます。 つまり、Google や Microsoft などの外部 IdP を用意すれば、その認証結果を GCIP に連携し、GCIP のユーザー管理や認証機能を活用できます。 ただし、その場合においても、Firebase SDK を利用する必要があります。
GCIP は認証はできるが、認可はできない
GCIP では認可制御 (Authorization) は提供されません。
そのため、"このユーザーに何が許可されているか?" はアプリケーション側で管理する必要があります。
具体的には、GCIP は ID Token
を発行できますが、Access Token
は発行できません。
GCIP が提供する組み込みログイン UI (FirebaseUI) はメンテナンスされていない
FirebaseUI は、ログイン UI を簡単にアプリケーションに組み込める公式の UI ライブラリです。 しかし、現在は積極的にメンテナンスされておらず、最新の技術トレンドへの対応が遅れています。 そのため、最新の React 環境や Next.js との組み合わせでは、組み込みが難しくなる可能性があります。 今後の拡張性や保守性を考えると、独自に UI を実装する方が適切な選択肢となるでしょう。
Next.js と Auth.js による実装
今回のアーキテクチャは次のようになります。
それでは、実際に Next.js と Auth.js を使って GCIP による認証を実装してみます。
GCIP の設定
Identity Platform を使用してユーザーがメールアドレスでログインできるようにする を参考に、次の手順でユーザーを作成しておきます。
- Identity Platform の有効化
- メールログインを構成する
- ユーザーを作成する
Next.js のプロジェクト作成
今回は bun を使って Next.js のプロジェクトを作成します。
→ bun create next-app@15
✔ What is your project named? … gcip-playground
✔ Would you like to use TypeScript? … Yes
✔ Would you like to use ESLint? … No
✔ Would you like to use Tailwind CSS? … No
✔ Would you like your code inside a `src/` directory? … Yes
✔ Would you like to use App Router? (recommended) … Yes
✔ Would you like to use Turbopack for `next dev`? … Yes
✔ Would you like to customize the import alias (`@/*` by default)? … No
Success! Created gcip-playground at /path/to/gcip-playground
必要なパッケージのインストールと設定
ここからは作成された gcip-playground
ディレクトリに移動して作業を進めます。
bun add next-auth@beta # Auth.js
bun add firebase@11 # Firebase SDK
bun add @chakra-ui/react@3 @emotion/react@11 # UI Library
Auth.js を利用するために必要な AUTH_SECRET
を生成します。
→ bunx auth secret
📝 Created /path/to/gcip-playground/.env.local with `AUTH_SECRET`.
Chakra UI の Snippet をプロジェクトに追加します。
bunx @chakra-ui/cli snippet add
env.local
に Firebase の設定を追加します。
API_KEY
は API キーを管理 の手順で取得します。
AUTH_SECRET="{YOUR_AUTH_SECRET}"
FIREBASE_API_KEY="{YOUR_FIREBASE_API_KEY}"
FIREBASE_AUTH_DOMAIN="{YOUR_FIREBASE_AUTH_DOMAIN}"
src/auth.ts
を作成して、Auth.js のプロバイダーを構成します。
Firebase SDK による認証が必要となるため Auth.js の Credentials
プロバイダーを使用します。
import NextAuth from "next-auth";
import Credentials from "next-auth/providers/credentials";
import { initializeApp } from "firebase/app";
import { getAuth, signInWithEmailAndPassword } from "firebase/auth";
export const { auth, handlers, signIn, signOut } = NextAuth({
providers: [
Credentials({
id: "credentials",
credentials: {
email: {},
password: {},
},
authorize: async (credentials) => {
const auth = getAuth(
initializeApp({
apiKey: process.env.FIREBASE_API_KEY,
authDomain: process.env.FIREBASE_AUTH_DOMAIN,
}),
);
const userCredential = await signInWithEmailAndPassword(
auth,
credentials.email as string,
credentials.password as string,
);
return {
id: userCredential.user.uid,
name: userCredential.user.displayName,
email: userCredential.user.email,
};
},
}),
],
callbacks: {
async redirect({ url, baseUrl }) {
return "/protected";
},
async session({ session, token }) {
session.user.name = token.name!;
session.user.email = token.email!;
return session;
},
},
});
Component の作成
src/app/layout.tsx
を編集します。
import { Provider } from "@/components/ui/provider";
import { Box, Container, Heading, Stack } from "@chakra-ui/react";
export default function Layout(props: { children: React.ReactNode }) {
const { children } = props;
return (
<html suppressHydrationWarning>
<body>
<Provider>
<Container maxW="4xl" mt="50px">
<Stack gap="6" align="flex-start">
<Heading size="3xl">
GCIP Authentication w/ Next.js & Auth.js
</Heading>
<Box w="100%">{children}</Box>
</Stack>
</Container>
</Provider>
</body>
</html>
);
}
src/app/page.tsx
を編集します。
import { Link } from "@chakra-ui/react";
export default function Page() {
return <Link href="/sign-in">Sign-in</Link>;
}
src/app/sign-in/page.tsx
を作成して、ログイン UI を実装します。
import { Button, Fieldset, Input } from "@chakra-ui/react";
import { Field } from "@/components/ui/field";
import { signIn } from "@/auth";
export default function Page() {
return (
<form
action={async (formData) => {
"use server";
await signIn("credentials", formData);
}}
>
<Fieldset.Root maxW="md">
<Fieldset.Content>
<Field label="Email address">
<Input name="email" type="email" />
</Field>
<Field label="Password">
<Input name="password" type="password" />
</Field>
</Fieldset.Content>
<Button type="submit" alignSelf="flex-start">
Sign-in
</Button>
</Fieldset.Root>
</form>
);
}
src/app/(auth)/layout.tsx
を作成して、(auth)
配下のファイルは認証が必要なページとして扱います。
import { auth } from "@/auth";
import { redirect } from "next/navigation";
import { ReactNode } from "react";
export default async function Layout({ children }: { children: ReactNode }) {
const session = await auth();
if (!session) {
redirect("/sign-in");
}
return <>{children}</>;
}
src/app/(auth)/protected/page.tsx
を作成して、認証情報を表示します。
import { auth } from "@/auth";
import { Text } from "@chakra-ui/react";
export default async function Page() {
const session = await auth();
return <Text>Hello, {session?.user?.email}</Text>;
}
アプリケーションを起動して動作確認
次のコマンドを実行してアプリケーションを起動します。
bun --bun run dev
http://localhost:3000/sign-in にアクセスして、GCIP に登録したユーザーの ID/PW でログインします。
ログインが成功すると、http://localhost:3000/protected にリダイレクトされ、認証情報が表示されます。
ソースコードはここにあります。
https://github.com/ogawa-takeshi/gcip-playground
おわり 🎉