Next.js と Auth.js で実装する Google Cloud Identity Platform 認証

Next.js と Auth.js を使って、Google Cloud Identity Platform (GCIP) による ID/PW 認証を検証します。GCIP の特徴や制約、Auth.js を用いた認証フローの構築手順を紹介します。

ogawaFebruary 8, 2025

Google Cloud Identity Platform (GCIP) の特徴

GCIPFirebase 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 を使用してユーザーがメールアドレスでログインできるようにする を参考に、次の手順でユーザーを作成しておきます。

  1. Identity Platform の有効化
  2. メールログインを構成する
  3. ユーザーを作成する

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_KEYAPI キーを管理 の手順で取得します。

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

おわり 🎉