Next.jsとAWSで認証つきサイトを爆速でホストする(Next.js + Cognito + S3 + Cloudfront)

# はじめに
[NRI OpenStandia Advent Calendar 2023](https://qiita.com/advent-calendar/2023/nri-openstandia)の21日目は Next.jsとAWSで認証付きサイトを爆速でホストする方法を紹介いたします！
趣旨としては、真面目にやると大変な認証のバックエンドもフロントエンドもCognitoに任せて、簡単に認証付きサイトホストしてしまおう！ということです。
今回はNext.jsプロジェクトをStatic ExportsしたものをS3 + CloudFrontでホスティングすることで、パブリックにアクセス可能なWebページを構築します。
# 本記事のゴール
- Next.jsで作成したWebアクセス可能な認証つきサイトをホスティングすること
- Topページにアクセスした際、未ログインなら認証が求められ、ログイン成功後Topページが閲覧可能になること
# 使用するもの
- 開発フレームワーク：Next.js (v14.0.4)　App Router
- 認証：AWS Cognito
- Webサイトホスティング：AWS S3 + AWS CloudFront
# 1. Next.jsの環境構築
## 1.1 Node.jsとnpmのインストール
[Node.jsのダウンロードページ](https://nodejs.org/en/download/)からインストーラをダウンロードします。
インストーラを起動しインストール完了後、ターミナルで以下を実行し、インストールできているか確認します。
正しくインストールされていれば、Node.jsとnpmそれぞれのバージョンが表示されます。
```terminal
node -v
> v20.0.9
npm -v
> 10.2.1
```
:::note 
**Tips**　(本筋とは関係ないですが...)
[Volta](https://volta.sh/)のようなNode.jsバージョン管理ツールを使用すると、自由にNode.jsのバージョンを切り替えられてオススメ！ ([参考](https://zenn.dev/taichifukumoto/articles/how-to-use-volta))
:::
## 1.2 ローカルにNext.jsのプロジェクト作成
プロジェクトを作成したい任意のディレクトリ上で以下を実行します。
```terminal
npx create-next-app@latest
```
表示される質問に答えて構築を完了します。
ここで表示される各質問については[create-next-appで訊かれていること](https://zenn.dev/ikkik/articles/51d97ff70bd0da)を参考にしてください。
```terminal
> What is your project named? my-app
> Would you like to use TypeScript? No / Yes
> Would you like to use ESLint? No / Yes
> Would you like to use Tailwind CSS? No / Yes
> Would you like to use `src/` directory? No / Yes
> Would you like to use App Router? (recommended) No / Yes
> Would you like to customize the default import alias (@/*)? No / Yes
> What import alias would you like configured? @/*
```
作成されたディレクトリに移動してlocalhostを起動します。
```terminal
npm run dev
```
[localhost:3000](http://localhost:3000/)にアクセスしてページが表示されていればNext.jsの環境構築はOKです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/771948/533d959f-b18f-059c-1570-50ee15fc772c.png)

# 2. ログイン画面を作成
ここで目指すゴールは、先ほどのTop画面にアクセスした際ログインがまだであれば、ログイン画面が表示されることです。
AWSにはAWS Amplifyという、モバイルアプリやWebアプリの開発を迅速化するためのフルスタックの開発フレームワークサービスが存在します。
Amplifyを使用するとAmplify経由でS3のホスティングができたりするのですが、今回はS3とCloudFrontを自前で用意するためAmplifyそのものは使わず、Amplifyのライブラリに含まれるCognitoとの連携パーツだけを使用します。
## 2.1 Next.js側の準備
[Amplifyのライブラリ](https://docs.amplify.aws/nextjs/)をインストールします。(本記事は2023年12月時点で最新のv6.0.7)
```
npm install @aws-amplify/ui-react aws-amplify
```
```src/app/layout.tsx```で```@aws-amplify/ui-react/styles.css```のimportし、Amplifyで用意されているログイン画面のstyleをimportします。
```diff_typescript:src/app/layout.tsx
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import {Amplify} from 'aws-amplify'
import './globals.css'
+import '@aws-amplify/ui-react/styles.css';
```

次にTopページ```src/app/page.tsx```に認証を追加します。
```diff_typescript:src/app/page.tsx
+'use client'
+
+import { withAuthenticator } from "@aws-amplify/ui-react";
import Image from 'next/image'
import styles from './page.module.css'

- export default function Home() {
+ function Home() {
  return (
    <main className={styles.main}>
        <div className={styles.description}>
            <p>中略...</p>
        </div>
    </main>
  )
}
+export default withAuthenticator(Home)
```
```export default withAuthenticator(Home)```のように```export default```記述時にコンポーネントを```withAuthenticator```でラップすることで、認証が求められるようになります。
Topページ以外のページについても```withAuthenticator```でラップすることで同様に認証を求めることができます。
ここで再度[localhost:3000](http://localhost:3000/)にアクセスしてみましょう。
すると、先ほどと違い認証が求められるようになっていますね！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/771948/4fd2c781-a6c5-ee98-64ac-356b4334065a.png)
しかし、まだAWS側でCognitoの設定をしていないためログインはできません。

:::note
**Note**
ログイン画面のカスタマイズ方法はこちらをご参照ください。
- 日本語化&文言変更：[AmplifyでCognitoの認証画面をカスタマイズする(React)](https://blog.denet.co.jp/amplify-cognito-custom-react/)
- その他詳細なデザイン変更(ヘッダーフッターなど)：[Amplify Authenticator Customization](https://ui.docs.amplify.aws/react/connected-components/authenticator/customization)
- 設定変更(サインインのみなど)：[Amplify Authenticator Configuration](https://ui.docs.amplify.aws/react/connected-components/authenticator/configuration)
:::
## 2.2 AWS Cognitoでユーザを作成
[AWS Cognito](https://aws.amazon.com/jp/cognito/)はAWS上に認証システムが構築されており、サーバレスに認証を実装することができるサービスです。

Cognitoの設定方法の詳細については[AWS公式Cognitoチュートリアル](https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/tutorials.html)をご参照ください。

最初に「ユーザープールを作成」をクリックし、ユーザープールの作成を行います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/771948/34cff4ac-078f-6bf3-cc1b-915891eb4ed5.png)
ユーザープールとは、ウェブおよびモバイルアプリケーションの認証と承認のためのユーザーディレクトリを指し、このプールに各アカウントが追加されていくことになります。
画面に従ってユーザープールの作成を進め、「Step5：アプリケーションを統合 」のアプリケーションクライアントは「パブリッククライアント」を選択します。
ユーザープールの作成が完了したら、作成したユーザープールに移動し「ユーザーの作成」からユーザーを追加します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/771948/b81fa3bb-6317-fa74-c9ef-c96454bf415f.png)
今回はユーザー名がadminのユーザーを作成しました。
## 2.3 Next.jsとCogintoを連携させる
```src/app/page.tsx```に```Amplify.configure```を追加し、```userPoolClientId```と```userPoolId```の情報を入力します。
```userPoolClientId```はAWS上の該当ユーザープールページの下部「アプリケーションクライアントのリスト」に記載されており、```userPoolId```は同ページ上部の「ユーザープールの概要」に記載があります．
```diff_typescript:src/app/page.tsx
'use client'
+import {Amplify} from 'aws-amplify'
import { withAuthenticator } from "@aws-amplify/ui-react";
import Image from 'next/image'
import styles from './page.module.css'

+Amplify.configure({
+ Auth: {
+   Cognito: {
+     userPoolClientId: '1234567abcdefg',
+     userPoolId: 'ap-northeast-1_ABCDEFG',
+  }
+ }
+})

function Home() {
  return (
    <main className={styles.main}>
        <div className={styles.description}>
            <p>中略...</p>
        </div>
    </main>
  )
}
export default withAuthenticator(Home)
```

:::note warn
**Attention**
amplifyライブラリv6から過去とconfigureの設定方法が違うようです。本記事では[公式リファレンス](https://docs.amplify.aws/nextjs/tools/libraries/configure-categories/#authentication-amazon-cognito)を参照しています。(2023年12月時点)
:::
以上の変更により、Cognitoで作成したadminユーザーでサインインできるようになりました！
※初回サインイン時はパスワードの変更を求められます。
ここまでで、ローカル上でCoginitoによる認証付きのNext.jsプロジェクトを作成することができました。
# 3. Next.jsプロジェクトをAWSでホスティング
ローカル環境で認証付きのNext.jsプロジェクトができたので、続いてAWSでホスティングを行います。
## 3.1 完成したプロジェクトのbuild
通常、Next.jsの実行にはNode.jsのランタイムが必要ですが、今回はS3 + CloudFrontでのWebサイトホスティングを目的としているので、Next.jsプロジェクトをStatic Exportsで静的ファイルとして出力します。
```next.config.js```を以下に変更します。
```diff_javascript:next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
+   output: 'export',
+   trailingSlash: true,
}

module.exports = nextConfig
```
- [output](https://nextjs.org/docs/pages/building-your-application/deploying/static-exports)：buildした際にoutディレクトリに出力を行う　※Next.js v13.3以降はこの方法
- [trailingSlash](https://nextjs.org/docs/pages/api-reference/next-config-js/trailingSlash)：```/about```のようなアクセスに対して、```/about/idex.html```にリダイレクトしてくれるようになる

ターミナルでbuildしてS3にアップロードするファイルを出力します。
```terminal
npm run build
```
ファイルはデフォルトでoutディレクトリに出力されます。
## 3.2 AWS S3を作成
続いてAWS側の構築を進めます。
基本的にデフォルトの設定のままバケットを作成します。
今回はクライアントからアクセスを受けるのはCloudFrontなので、「パブリックアクセス」は全てブロック、「静的サイトホスティング」も無効で良いです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/771948/19231ceb-bbb0-8c76-60a3-a09beab11942.png)
S3バケットの作成が完了したら、先ほどのbuildで作成されたoutディレクトリの全てのファイルをS3バケットにアップロードします。
## 3.3 CloudFrontを作成
最後にクライアントからアクセスを受けるCloudFrontを作成します。
「ディストリビューションを作成」でディストリビューションを作成します。
オリジンドメインには先ほど作成したS3バケットを選択し、「オリジンパス」は```index.html```とします。
「オリジンアクセス」は「Origin access control settings (recommended)」を選択し、「Origin access control」は新規に作成します。
その他の項目についてはデフォルトで良いです。
ディストリビューション作成後、先ほど作成したS3バケットのバケットポリシーを変更します。
作成したディストリビューションの「オリジン」タブからオリジンを編集し、編集画面内のバケットポリシーをコピーします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/771948/b6731152-4d2f-3c09-2ed5-2ecc0c65b159.png)
「S3バケットアクセス許可に移動」からS3のアクセス許可画面に遷移し、バケットポリシーをコピーしたものに変更します。
ここまでで全ての構築が完了しました！

:::note
**Note**
もしTopページ以外のページも作成していて、遷移したい場合はCloudFrontの「エラーページ」設定が必要になります。詳細は[こちら](https://zenn.dev/redgosho/articles/13b93d2a8eac0f)をご参照ください。
:::
# 4. 実際にアクセスできるか確認
最後に認証付きサイトがホスティングできているか確認しましょう！
作成したCloudFrontのディストリビューションに記載されている「ディストリビューションドメイン名」にアクセスすると、サイトにアクセスすることができるはずです。
初回アクセス時に先ほどの認証画面が表示されて、認証情報入力→ログイン 後にTopページが表示されていれば成功です！
![image.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/771948/376418cc-cec4-42f1-52eb-680078a09e1b.gif)
ここまでで認証付きサイトが無事ホスティングされていることを確認できました。

# おわりに
今回はNext.jsとAWSで認証付きサイトをホスティングする方法を紹介しました。
Cognitoが認証部分を担ってくれているのと、AmplifyライブラリのCognito連携と画面部品を使用することで、簡単に認証付きサイトをホスティングできたのではないでしょうか？
CognitoはGoogleログインなど他サービスの認証プロバイダーにも対応しており、リッチな認証を簡単に実装できるのが嬉しいですね。
# 参考
- [Node.js公式サイト](https://nodejs.org/en)
- [Volta公式サイト](https://volta.sh/)
- [Node.jsのバージョン管理にVoltaを推したい](https://zenn.dev/taichifukumoto/articles/how-to-use-volta)
- [create-next-appで訊かれていること](https://zenn.dev/ikkik/articles/51d97ff70bd0da)
- [AmplifyでCognitoの認証画面をカスタマイズする(React)](https://blog.denet.co.jp/amplify-cognito-custom-react/)
- [Amplify Dev Center(Authenticator)](https://ui.docs.amplify.aws/react/connected-components/authenticator)
- [Amplify Documentation for Next.js](https://docs.amplify.aws/nextjs/)
- [AWS Cognitoチュートリアル](https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/tutorials.html)
- [Next.js Docs](https://nextjs.org/docs)
- [CloudFront+S3構成のSPA Webアプリでルートページ以外に直で行ったときにAccessDeniedになった件について](https://zenn.dev/redgosho/articles/13b93d2a8eac0f)