🔐

【Next.js App Router&Cognito】Google認証を独自UIで作ってみた【日本語化対応】

2024/10/26に公開

はじめに

Next.jsとCognitoを使ったGoogle認証を実装する際、デフォルトのCognitoのUIでは認証画面が英語で表示されるという課題がありました。
また、日本語化の解決策としてAWS Amplifyを使う方法が紹介されていますが、最新のバージョンでは対応していない可能性がありました。
本記事では、これらの課題を踏まえ、NextAuth.jsとAWS SDKを用いた認証の実装方法を紹介します。

↓デフォルトだとこういう画像が表示されます
スクリーンショット 2024-10-26 7.01.04.png

サンプルリポジトリ

https://github.com/mikaijun/nextjs-google-auth-cognito

前提事項

  • AWSのアカウント作成済であること
  • Google Cloud コンソール アカウント作成済であること

※記事公開時点でのNext.jsのバージョンは14系です。15系にするとnext-authをインストールするときにエラーが出るかもしれません

Cognito設定

CognitoでGoogle認証する方法は下記記事がとてもわかりやすかったので記事通りに行えば問題ないと思います。

https://qiita.com/Naoki1126/items/2dbc8ac4eb07310adc0f

上記記事と違う箇所は承認済リダイレクトURLにアプリケーションのURIを追加する必要があります。
スクリーンショット 2024-10-26 13.02.33.png

補足

Googleのプロジェクト作成後のサイドバーで、OAuth同意画面がどこにあるか迷うことがあるため、以下の画像を参考にしてください。サイドバーの「APIとサービス」をクリックし、「OAuth同意画面」メニューが表示されます。

スクリーンショット 2024-10-26 6.52.30.png

環境構築変数確認

COGNITO_REGION=ap-northeast-1
COGNITO_USER_POOL_ID=
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
NEXT_PUBLIC_BASE_URL=http://localhost:3000

COGNITO_USER_POOL_ID

スクリーンショット 2024-10-26 7.08.09.png

GOOGLE_CLIENT_IDGOOGLE_CLIENT_SECRET

OAuthクライアント作成時に表示されるモーダルまたはダウンロードしたJSONで確認できます。
スクリーンショット 2024-10-26 6.58.39.png

または認証情報サイドバーから確認できます
スクリーンショット 2024-10-26 7.07.19.png

スクリーンショット 2024-10-26 7.07.27.png

フロントエンド環境構築

下記コマンドでサクッと作ります。

npx create-next-app@latest
npm install @aws-sdk/client-cognito-identity-provider aws-sdk next-auth --save

※next-authで依存エラーが発生した場合Next.jsやReact.jsのverを変更します。
package.jsonでバージョンを変更し、npm installし直します。
package.json

  "dependencies": {
    "next": "14.2.16",
    "react": "^18",
    "react-dom": "^18"
  }

コーティング

まずはnext-authの設定。
下記記事の通り外部ファイルに切り出します。

https://qiita.com/ment_RE/items/becacbdc8b1cebe81ceb

src/lib/auth.ts

import type { NextAuthOptions } from "next-auth"
import NextAuth, { getServerSession } from 'next-auth';
import GoogleProvider from 'next-auth/providers/google';
import {
  CognitoIdentityProviderClient,
  AdminCreateUserCommand,
  AdminGetUserCommand,
  AdminCreateUserRequest,
} from '@aws-sdk/client-cognito-identity-provider';
import crypto from 'crypto';

const cognitoClient = new CognitoIdentityProviderClient({
  region: process.env.COGNITO_REGION,
});

/**
 * Cognitoにユーザーが存在するか確認する
*/
async function checkUserExists(email: string): Promise<boolean> {
  try {
    await cognitoClient.send(
      new AdminGetUserCommand({
        UserPoolId: process.env.COGNITO_USER_POOL_ID!,
        Username: email,
      })
    );
    return true;
  } catch (error) {
    // NOTE: ユーザーが存在しない場合はUserNotFoundExceptionが発生する。この時は後続処理を続行するためfalseを返す
    if ((error as { name: string }).name === 'UserNotFoundException') {
      return false;
    } else {
      console.error('Failed to check user existence in Cognito:', error);
      throw error;
    }
  }
}

/**
 * Cognitoにユーザーを作成する
*/
async function createUserInCognito(email: string, name: string) {
  const params: AdminCreateUserRequest = {
    UserPoolId: process.env.COGNITO_USER_POOL_ID!,
    Username: email,
    UserAttributes: [
      {
        Name: 'email',
        Value: email,
      },
      {
        Name: 'name',
        Value: name,
      },
      {
        Name: 'email_verified',
        Value: 'true',
      },
    ],
    // NOTE: Googleのユーザーはパスワードメールを受け取らないため、パスワードリセットを強制する
    MessageAction: 'SUPPRESS',
  };

  try {
    const command = new AdminCreateUserCommand(params);
    await cognitoClient.send(command);
  } catch (error) {
    throw error;
  }
}

export const generateSecretHash = (username: string): string => {
  return crypto
    .createHmac('SHA256', process.env.COGNITO_CLIENT_SECRET!)
    .update(username + process.env.COGNITO_CLIENT_ID!)
    .digest('base64');
};

const authOptions: NextAuthOptions = {
  secret: "secret",
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    }),
  ],
  callbacks: {
    async signIn({ user, account }) {
      if (account?.provider === 'google') {
        try {
          const userExists = await checkUserExists(user.email!);

          if (!userExists) {
            await createUserInCognito(user.email!, user.name || '');
          }
        } catch (error) {
          console.error('Failed to create user in Cognito:', error);
          return false;
        }
      }
      return true;
    },
  },
}

export const getServerAuthSession = () => getServerSession(authOptions);
export const handlers = NextAuth(authOptions);

次にAPIルートを作成します
src/app/api/auth/[...nextauth]/route.ts

import { handlers } from "@/lib/auth";
export { handlers as GET, handlers as POST };

ログイン画面実装していきます。
src/component/LoginForm.tsx

"use client";

import { signIn } from 'next-auth/react';

const LoginForm = () => {
  const handleGoogleSignIn = async () => {
    try {
      await signIn('google', {
        callbackUrl: process.env.NEXT_PUBLIC_BASE_URL,
        redirect: false,
      });
    } catch (error) {
      console.error('Google Sign in failed', error);
    }
  };

  return (
    <button
      onClick={handleGoogleSignIn}
      style={{
        padding: '10px 20px',
        backgroundColor: '#4285F4',
        color: '#fff',
        border: 'none',
        borderRadius: '5px',
        fontSize: '16px',
        cursor: 'pointer',
        transition: 'background-color 0.3s ease',
      }}
    >
      Googleでログイン
    </button>
  );
};

export default LoginForm;

/にアクセスした時のコンポーネント定義。
認証済の場合はユーザー情報を表示させます。
src/app/page.tsx

import React from 'react';
import { getServerAuthSession } from '@/lib/auth';
import { Session } from 'next-auth';
import LoginForm from '@/component/LoginForm';

export default async function Home() {
    const session: Session | null = await getServerAuthSession();

    return (
        <main>
            {session ? (
                <div>
                    <p>メールアドレス: {session.user?.email}</p>
                    <p>名前: {session.user?.name}</p>
                </div>
            ) : (
                <LoginForm />
            )}
        </main>
    );
}

LayoutにSessionProviderを定義。
(本来はlayout.tsxをクライアントコンポーネントしないと思いますが、簡略化するために今回はクライアントコンポーネントにしました)

src/app/layout.tsx

"use client";

import { SessionProvider } from 'next-auth/react';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="jp">
      <body>
        <SessionProvider>
          {children}
        </SessionProvider>
      </body>
    </html>
  );
}

最後に環境変数を定義
詳細は 環境構築変数確認 参照

COGNITO_REGION=ap-northeast-1
COGNITO_USER_POOL_ID=ap-northeast-1_xxxxxxxxxxx
GOOGLE_CLIENT_ID=xxxxxxxxxxx.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=xxxxxxxxxxxxx
NEXT_PUBLIC_BASE_URL=http://localhost:3000

動作確認

ボタンクリックしたらエラーが出る場合はCognito設定のリダイレクトURL設定がうまく行っていない場合があります

スクリーンショット 2024-10-26 12.50.08.png

スクリーンショット 2024-10-26 13.05.28.png

スクリーンショット 2024-10-26 13.08.07.png

スクリーンショット 2024-10-26 13.28.40.png

おわりに

本記事では、Next.jsとCognitoを使ったGoogle認証を独自UIで作ってみました。
この記事ではNext.js14で対応しましたが、Next.jsのバージョンが上がったら他のベストプラクティスも探してみたいです。
この記事がCognitoを使ったGoogle認証の参考になれば幸いです。
最後までお読みいただきありがとうございます!

Discussion