Amazon Web Services ブログ
AWS Amplify と Auth0 を使って認証付き Next.js アプリを構築する方法
この記事では、AWS Amplify で、OpenID Connect Identity Provider (OIDC) を使用して Next.js アプリケーションを認証および認可する方法を説明します。AWS Amplify は、クラウドで動作するフルスタックアプリケーションを開発およびデプロイするためのフロントエンド開発者向けの製品です。AWS Amplify アプリケーションの機能を強化するために、Auth0 が提供する ID 管理プラットフォームを活用します。Okta の Auth0 は、認証と認可の両方をサポートする ID 管理プラットフォームです。
Next.js アプリを使って Auth0 と AWS Amplify を統合して実装し、Amplify Hosting を使ってデプロイする方法を紹介します。
ソリューションの概要
このウェブアプリケーションでは、ユーザはメールアドレス / パスワードまたは Google などのソーシャルアカウントを使ってサインアップとログインができます。認証されたユーザは、ToDo リストアイテムを作成したり削除したりできます。バックエンドでは、このアプリケーションは Amazon Cognito での認証、AWS AppSync と Amazon DynamoDB でのデータ管理に Amplify を活用しており、ユーザにはシームレスでセキュリティ保護された体験を提供します。
次のアーキテクチャ図は、このソリューションで使用される AWS サービスを示しています。

以下の Amazon Cognito の概念について、こちらのブログで説明します。
- ユーザープールは、Web およびモバイルアプリの認証と認可のためのユーザーディレクトリです
- ID プールは、Cognito ユーザープールや SAML を含む異なる認証プロバイダ間で ID を連携させるために使用できます。また、OIDC プロバイダを介して、他の AWS サービスに一時的にアクセスする権限が付与されます
認証の流れを要約すると、次のようになります。
- ユーザーがウェブアプリケーションからログインを開始すると、認証のため Auth0 ポータルにリダイレクトされます。
- ユーザーは、新しいプロフィールか、Google などのソーシャルログインのいずれかを使用して、サインインできます。
- 認証に成功すると、Auth0 は認可コードを含めて Amazon Cognito にリダイレクトされます。
- 次に、Cognito はその認可コードを Auth0 のアクセストークンと ID トークンと交換します。
- Cognito は ID トークンの情報を使用して、Cognito ユーザープールにユーザープロフィールを作成します。
- 最後に、Cognito はアクセストークンと ID トークンの両方をアプリケーションに返し、これらを使って保護されたリソースにアクセスできるようになります。
前提条件
- AWS アカウント: 次のサービスは AWS 無料利用枠の対象です。 
         - AWS Amplify
- Amazon DynamoDB
- AWS AppSync
- Amazon Cognito
 
- Okta アカウントの Auth0
- Node.js v18.18 以降
- GitHub アカウント
- npm v9 以降
- Git
- テキストエディタ: このガイドでは VSCode を使用しますが、お好みの IDE をご利用いただけます。
- AWS 認証情報を設定したターミナル: このガイドに従って、ローカル開発用に AWS をマシンにセットアップします。
Auth0 アプリケーションの作成
Auth0 で設定されたサービスへのアクセス権を持つアプリケーションクライアントを表す Auth0 アプリケーションの設定から始めましょう。
- Auth0 アカウントにサインアップ(すでにAuth0アカウントをお持ちの場合は、そのアカウントにログイン)
- 左のナビゲーションバーにある Auth0 ダッシュボードの Applications タブをクリックします。 
- Create Application ボタンをクリックして、新しい Auth0 アプリケーションを作成します。
- アプリケーションの名前を指定します (ここでは Amplify-Auth0-Demo とします)。
  
- Next.jsアプリを使用するので、Regular Web Application オプションを選択します。 
アプリケーションが作成されると、ダッシュボードページにリダイレクトされ、Settings タブに Client ID、Client Secret、Domain などの設定情報が表示されます。次の手順でこれらの値を使用するので、このページはそのままにしておいてください。

Amplify Gen2 To-do アプリケーションのクローン
次に、AWS サンプル GitHub リポジトリからテンプレートを使用して新しいリポジトリを作成し、Next.js アプリを使えるようにする必要があります。
- リポジトリをローカルにクローンし、IDE でプロジェクトを開きます。
git clone https://github.com/aws-samples/amplify-next-template.git 
- バックエンドとクライアントライブラリを定義するのに必要なパッケージを含め、プロジェクト内にすべての依存関係をローカルにインストールします。
npm install dotenv react react-intl 認証リソースの変更
リポジトリの内容を見てみましょう。app/ フォルダにフロントエンドが、amplify/ フォルダに Amplify Gen 2 のバックエンドがあります。
amplify/auth/resource.ts で、既存のコードを以下のコードで上書きしてください。
import {defineAuth, secret} from "@aws-amplify/backend";
import 'dotenv/config';
export const auth = defineAuth({
  loginWith: {
    email: {
      verificationEmailSubject: 'Welcome to the Blog post app 👋 Verify your email!'
    },
    externalProviders: {
      oidc: [
        {
          name: 'Auth0',
          clientId: secret('AUTH0_CLIENT_ID'),
          clientSecret: secret('AUTH0_CLIENT_SECRET'),
          issuerUrl: process.env.ISSUER_URL || '',
          scopes: ['openid', 'profile', 'email']
        },
      ],
      callbackUrls: [process.env.CALLBACK_URL || ''],
      logoutUrls: [process.env.LOGOUT_URL || ''],
    },
  },
});
このコードでは、メールログインに対応した認証バックエンドを設定し、OIDC プロバイダーとして Auth0 と統合しています。バックエンドには、フェデレーテッドログイン機能付きの Cognito ユーザープールがセットアップされています。
設定をまとめると以下のようになります:
- issuerUrl: OpenID トークンを提供するアイデンティティプロバイダーの URL
- callbackUrls: OIDC プロバイダーで認証した後にユーザーがリダイレクトされる URL
- logoutUrls: アプリケーションからログアウトした後にユーザーがリダイレクトされる URL
- scopes: クライアントアプリケーションがアイデンティティプロバイダーに対して要求しているアクセスレベルを指定
シークレットと環境変数のローカル設定
Amplify には、AWS Systems Manager Parameter Store からシークレットを設定・取得する secret 関数が用意されています。これを使って Auth0 の認証情報を設定します。
Amplify にシークレットを設定するには、ターミナルで次のコマンドを実行してください。
npx ampx sandbox secret set AUTH0_CLIENT_IDAUTH0_CLIENT_ID の入力を求められます。Auth0 アプリケーションからクライアント ID をコピーし、シークレット値として入力してください。完了すると、「Successfully created version 1 of secret AUTH0_CLIENT_ID」 という確認メッセージが表示されます。
前のステップを AUTH0_CLIENT_SECRET に対して繰り返してください。
npx ampx sandbox secret set AUTH0_CLIENT_SECRET完了すると、「Successfully created version 1 of secret AUTH0_CLIENT_SECRET」というメッセージが表示されます。
次に、LOGOUT_URL、CALLBACK_URL、ISSUER_URL の環境変数を設定する必要があります。プロジェクトのルートディレクトリで、.env ファイルを作成してください。以下の環境変数を別々の行に追加してください。
ISSUER_URL = <AUTH0_DOMAIN_URL>
CALLBACK_URL = http://localhost:3000 
LOGOUT_URL = http://localhost:3000 
AUTH0_DOMAIN_URL をあなたの Auth0 アプリケーションの Auth0 の Domain URL と置き換えてください。URL は https:// プレフィックスを含める必要があります。これは、Auth0 アプリケーションの作成のセクション、4 枚目のスクリーンショットで示されています。
DynamoDB データソースの AppSync での設定
amplify/data/resource.ts の既存のコードを次のように置き換えてください。
import { type ClientSchema, a, defineData } from "@aws-amplify/backend";
const schema = a.schema({
  Todo: a
  .model({
    content: a.string(),
  })
  .authorization((allow) => [allow.authenticated()]),
});
export type Schema = ClientSchema;
export const data = defineData({
  schema,
  authorizationModes: {
    defaultAuthorizationMode: 'userPool'
  },
});
defineData 関数は、AWS AppSync API を通して公開される DynamoDB の ToDo テーブルのデータモデルを定義します。API の承認ルールは、Cognito ユーザープールから認証されたユーザーのみが ToDo アイテムの作成と削除を許可するように設定されています。
Next.js のクライアントサイドコード
次に、Next.js フロントエンドコードを見ていきます。app ディレクトリ内に translations.js という新しいファイルを作成し、次のコードを貼り付けてください。
// translations.js
export const messages = {
  en: {
    welcome: "Welcome, {username}",
    myTodos: "My todos",
    newTodo: "+ new",
    signOut: "Sign Out",
    signInWithAuth0: "Sign in with Auth0",
    appHosted: "App successfully hosted. Try creating a new todo.",
  },
  // Add other languages as needed
  es: {
    welcome: "Bienvenido, {username}",
    myTodos: "Mis tareas",
    newTodo: "+ nueva",
    signOut: "Cerrar sesión",
    signInWithAuth0: "Iniciar sesión con Auth0",
    appHosted: "Aplicación alojada con éxito. Intenta crear una nueva tarea.",
  },
};
ユーザー体験を向上させるため、このアプリは react-intl を使ってローカライズされたメッセージを管理しています。translations.js ファイルで定義されているこれらのメッセージにより、ユーザーはお好みの言語でアプリを操作できるようになります。たとえば、「Sign in with Auth0」ボタンやその他の重要なインターフェース要素が、ユーザーのロケールに基づいて自動的に翻訳されるため、ユーザビリティが向上し、さまざまな言語設定でアプリを魅力的なものにします。
app/page.tsx ファイルの中で、既存のコードを以下のコードに置き換えてください。
"use client";
import {useState, useEffect} from "react";
import { getCurrentUser, signInWithRedirect, signOut, fetchUserAttributes } from 'aws-amplify/auth';
import {generateClient} from "aws-amplify/data";
import type {Schema} from "@/amplify/data/resource";
import "./../app/app.css";
import {Amplify} from "aws-amplify";
import outputs from "@/amplify_outputs.json";
import "@aws-amplify/ui-react/styles.css";
import {messages} from './translations';
import { IntlProvider, FormattedMessage, useIntl } from "react-intl"; 
Amplify.configure(outputs);
const client = generateClient();
function App() {
    const [todos, setTodos] = useState<Array>([]);
    const [username, setUsername] = useState(null);
    const [locale, setLocale] = useState('en'); // Default to English
    const intl = useIntl();
    function listTodos() {
        client.models.Todo.observeQuery().subscribe({
            next: (data) => setTodos([...data.items]),
        });
    }
    function deleteTodo(id: string) {
        client.models.Todo.delete({id})
    }
    async function checkUserAuthentication() {
        try {
            const currentUser = await getCurrentUser();
            if (currentUser) {
                const attributes = await fetchUserAttributes();        
                const displayName = attributes.email || currentUser.username || currentUser.userId;
                setUsername(displayName);
                return true;
            }
        } catch (error){
            console.error("Error getting current user:", error);
            setUsername(null);
            return false;
        }
    }
    useEffect(() => {
        const fetchTodos = async () => {
            const isAuthenticated = await checkUserAuthentication();
            if (isAuthenticated) {
                listTodos();
            }
        }
        fetchTodos();
    }, []);
    function createTodo() {
        client.models.Todo.create({
            content: window.prompt("Todo content"),
        });
    }
    const handleSignOut = async () => {
        await signOut();
        setUsername(null);
    }
    return (
        <main>
            {username ?
                <div>
                    <h1><FormattedMessage id="welcome" values={{ username }} /></h1>
                    <h1><FormattedMessage id="myTodos" /></h1>
                    <button onClick={createTodo}>+ new</button>
                    <ul>
                        {todos.map((todo) => (
                            <li key={todo.id} onClick={() => deleteTodo(todo.id)}>
                                {todo.content}
                            </li>
                        ))}
                    </ul>
                    <div>
                        <FormattedMessage id="appHosted" />
                        <br/>
                        <button onClick={handleSignOut}>
                        <FormattedMessage id="signOut" />
                        </button>
                    </div>
                </div> :
                <button onClick={() => signInWithRedirect({
                    provider: {custom: 'Auth0'}
                })}>
                    <FormattedMessage id="signInWithAuth0" />
                </button>
            }
        </main>
    );
}
export default function IntlApp() {
    return (
        <IntlProvider messages={messages['en']} locale="en" defaultLocale="en">
            <App />
        </IntlProvider>
    );
}
アプリは、ロード時に自動的にユーザーの認証ステータスをチェックし、認証されている場合はユーザーの To-Do リストをフェッチして表示します。そうでない場合は、「Sign in with Auth0」ボタンを表示します。このボタンをクリックすると、signInWithRedirect 関数がトリガーされ、ユーザーは認証またはアカウント作成のために Auth0 ポータルにリダイレクトされます。認証が成功すると、ユーザーは Next.js アプリにリダイレクトされます。
認証が済むと、アプリは getCurrentUser を使ってユーザー情報を取得し、次に fetchUserAttributes を使ってユーザーのメールアドレスを取得します。これにより、アプリは、ToDo リストと新規 ToDo 作成フォームを備えたパーソナライズされたダッシュボードを表示できます。インターフェースには、ログアウトボタンも用意されており、ユーザーは終了時に安全にログアウトできます。
クラウドサンドボックス環境の作成
クラウドサンドボックス環境でリソースをデプロイする前に、Amplify は AWS アカウントとリージョンに対して 1 回限りのブートストラップセットアップを完了する必要があります。現在のプロジェクトターミナルで、以下のコマンドを実行してください。
npx ampx sandbox初めてのセットアップ時に、npx ampx sandbox は AWS マネジメントコンソールへのサインインを求められます。ルートユーザーとしてサインインするか、AdministratorAccess を持つユーザーとしてサインインする必要があります。サインイン後、Amplify コンソールにリダイレクトされます。Create new app ページで Initialize setup now を選択してください。ブートストラップ処理には数分かかる可能性があります。

セットアップが完了したら、バックエンドをテストする準備ができました。リソースのデプロイを開始するには、sandbox コマンドを再度実行してください。
最初のデプロイには、新しい AWS リソースがプロビジョニングされるため 5 ~ 10 分かかる可能性があります。デプロイが完了すると、プロジェクトに amplify_outputs.json ファイルが生成され、使用可能なバックエンドリソース全てが含まれます。
Cognito ユーザープールのドメインを Auth0 に設定する
認証プロセスを機能させるには、ユーザーが正常に認証された場合、Auth0 アプリケーションが Cognito にトークンを返すように構成する必要があります。
amplify_outputs.json で Cognito Domain の出力を探し、その値をコピーします。または、この値を AWS マネジメントコンソール から直接取得することもできます。
- Amazon Cognito コンソールに移動して、サンドボックスによってデプロイされた ユーザープール を選択してください。
- App integration セクションに移動し、ドメインの下にある Cognito のドメインをコピーしてください。

Auth0 アプリケーション (Amplify-app) に移動し、Settings タブを開きます。Allowed Callback URLs の項目で、https://<COGNITO_DOMAIN>/oauth2/idpresponse を入力します。ここで、<COGNITO_DOMAIN> の部分はご自身の Cognito のドメインに置き換えてください。変更を反映させるには、Save change を選択します。
アプリケーションの実行
ローカルでテストを始めるために必要なものがすべて揃いました! IDE で新しいターミナルセッションを開き、以下のコマンドを実行してローカルホストの開発サーバーを起動してください:
npm run dev
ブラウザから localhost を開いてください。例: http://localhost:3000

Amplify Hosting へのアプリのデプロイ
クラウドサンドボックス環境の設定が完了したら、次の手順は Amplify Hosting を使ってアプリケーションをデプロイすることです。Amplify Hosting は、Git ベースのシンプルなワークフローを持つ静的ウェブアプリケーションと動的ウェブアプリケーションに簡単に利用できる CI/CD を提供します。このセクションでは、変更された Amplify アプリを格納するためのソースリポジトリとして GitHub を使用します。
- GitHub アカウントにログインし、GitHub リポジトリを作成します。
- リポジトリ内の手順に従って、変更をコミットし、アプリケーションをリモートリポジトリにプッシュします。
- AWS Amplify コンソールで、ページの中央にある Create new app を選択します。既に Amplify アプリケーションを持っている場合は、ページの右上にある Create new app が表示されます。
- Deploy your app セクションで、GitHub を選択し、Next を選択します。

注: 現在のリージョンで初めてのアプリをデプロイする場合、デフォルトで AWS Amplify サービスのページから始まります。
Amplify は GitHub のリポジトリへのアクセスを承認するために GitHub Apps 機能を使用します。GitHub App のインストールと承認の詳細については、Amplify の GitHub リポジトリへのアクセス設定を参照してください。
5. Add repository and branch ページで、以下の手順を実行してください。
-  
         - 接続するリポジトリの名前を選択します。
- 接続するリポジトリのブランチ名を選択します。
- Next を選択します。
 

6. App Settings ページで
-  
         - アプリケーション名を入力します。デフォルトでは、Amplify はリポジトリ名を使用します。
- Build Settings セクションで、Frontend build command と Build output directory が正しいことを確認してください。この Next.js アプリの例では、 Build output directory は .nextに設定されています。
- Service role には、Create and use a new service role を選択し、Next をクリックしてください。
 
 
設定を確認し、Save and deploy をクリックしてください。
Amplify Hosting への環境変数とシークレットの追加
- 左ペインの View app を選択し、Hosting セクションを展開してください。
- Environment variables に移動し、CALLBACK_URL、ISSUER_URL、LOGOUT_URL変数を追加してください。
- CALLBACK_URLと- LOGOUT_URLの値として、Amplify App のドメインを入力します。これは Overview セクションにあり、次のようになります。- https://<GITHUB_BRANCH>.<APP_ID>.amplifyapp.com
- ISSUER_URLについては、Auth0 の Domain の値を参照してください。
- 環境変数を保存します。

ホスティングセクションで、Secrets を選択し、Manage Secrets を開きます。Auth0 アプリケーションからの AUTH0_CLIENT_ID と AUTH0_CLIENT_SECRET の両方のキーと対応する値を追加します。

注意: デフォルトでは、Amplify は新しいアプリケーションの作成時に自動デプロイされます。ただし、環境変数とシークレットを設定しないと、デプロイは失敗します。以下の手順を失敗する前に完了できれば、次のセクションに進めます。デプロイが失敗した場合は、環境変数とシークレット値を追加した後に再デプロイできます。
デプロイが完了したら、Auth0 アプリケーションのコールバック URL で Cognito のドメインの設定を行う最終ステップがあります。Cognito のドメインは、Deployments コンソールから amplify_outputs.json をダウンロードし、値を取得することで確認できます。

Cognito ユーザープールのドメインを Auth0 に設定する の手順に従って、ドメインを Auth0 アプリケーションに追加してください。
ソリューションのテスト
ドメイン URL または Visit deployed URL ボタンを使用して Web アプリを開くことができます。

リソースのクリーンアップ
将来的に課金されないように、このソリューションで使用したリソースを削除してください。
Amplify Hosting とサンドボックス
- Amplify コンソールで View app を選択し、左側のペインで App settings セクションを展開します
- General settings を選択し、Delete app ボタンを選んでください
- npx ampx sandbox deleteを実行するか、プロセスを停止して Amplify Sandbox を削除します
Auth0 アプリケーション
- Auth0 のダッシュボードページを開き、Amplify App を選択します
- Settings タブの下で、Delete this application を選択します
- Auth0 アプリケーションの名前を入力し、Delete を選択します
まとめ
おめでとうございます! この記事では、Auth0 を Federated OIDC プロバイダとして AWS Amplify Gen 2 に統合する方法を学びました。Microsoft Entra ID、Clerk など、ほかの外部 ID プロバイダでも同様のアプローチを取れます。Amplify Gen 2 を始めるには、クイックスタートチュートリアルに取り組んでみてください。AWS Amplify Gen 2 ドキュメントも参照し、フィードバックや機能リクエストがあればコミュニティ Discord に参加してください。
本記事は、How to Build Next.js Apps with Authentication using AWS Amplify and Auth0 を翻訳したものです。翻訳は Solutions Architect の 吉村 が担当しました。

