NextAuth.js v4 + Amazon Cognitoを試してみる

※参考記事: https://qiita.com/junkor-1011/items/38557ef4188749fe14f9

## 概要

やったこと：

- Next.js(TypeScript)にNextAuthを入れ、別途用意したCognitoのユーザープールと連携してログイン・ログアウトの機能が働くことまで確認した
    * 少しやってみた程度で、あまり高度なことはやっていない
    * なので、当然本番環境での実行などは全く考えていないので注意

動機、目的など：

- Next.js(TypeScript)でawsのcognitoを使った認証を試したかった
    * cognitoを使うのはAWS上で環境構築を行う必要があるため
    * とはいえ、他のプラットフォーム、プロバイダーを使う場合でも概ねそのまま通用するのが望ましい
- 既存のライブラリでデファクトスタンダードっぽいものがあればそれを使いたかった
    * AWS上でやるということだけを考えるなら[AWS AmplifyのAuthを使う](https://docs.amplify.aws/lib/auth/getting-started/q/platform/js/)という選択肢もありそうだった。が、極力AWS、更にはAWS上の特定のサービスにがっちりくっついた構成は避けたかった
    * →[NextAuth](https://next-auth.js.org/)に行き着く
- NextAuthは2021年末くらいにv3系からv4系にアップデートされており、最新版のv4系の記事が少ないため備忘録として書くことにした
    * 検索して出てくる記事はv3系のものが多く、特にcognitoを使ったものはv4系で見つけるのが困難だった（執筆当時）


## 実行環境

- Ubuntu 20.04LTS on WSL2
- nodejs v16.14.2 + yarn 1.22.18
- (+ AWSアカウント)
    * ap-northeast-1（東京リージョン）を使用

## 準備

### cognitoのユーザープール準備

とりあえずなので、AWSマネジメントコンソールで手作業で行う。

(追記 2022-08-02)[AWS CDK v2でユーザープールを作成する記事](https://qiita.com/junkor-1011/items/38557ef4188749fe14f9)を書いたので、手作業よりcdkが良い場合はそちらを参考のこと。※そのままだと若干設定は変わってくるので注意


- サービスの"Cognito"を選択し、"ユーザープールの管理"を開く
- おそらく画面右上に出てくる"ユーザープールを作成する"を開いて作成画面に行く
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680173/bb4704bc-f55f-6e88-ee6d-6fc39244bdff.png)
↑今回はデフォルト設定を少しいじるだけなので、適当なプール名を入力したら"デフォルトを確認する"をクリックする

- アプリクライアントが必要になるので、画面左のメニューから"アプリクライアント"をクリックし、アプリクライアントを追加する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680173/692b5b78-0d39-3eea-f973-34e64a536ace.png)
↑アプリクライアント名を適当に入力し、"アプリクライアントの作成"をクリックして作成する

- 最後に"確認"から"プールの作成"をクリックし、ユーザープールの作成を完了する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680173/49205230-2ac5-70c8-bc51-c5e407d4f3bc.png)

- プールの作成が完了したら、"アプリの統合"の"アプリクライアントの設定"を編集する必要がある
- [公式の説明ページ](https://next-auth.js.org/providers/cognito)も頼りに、以下のように設定する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680173/a3ce139e-9781-8e6e-06c8-152be2a4e3da.png)
- コールバックURLは `http://localhost:3000/api/auth/callback/cognito` とする
    * ローカル環境での開発用のものなので注意
- 許可されているOAuthフローは`Authorization code grant`にし、許可されているOAuthスコープは`email`, `openid`, `profile`を選択している
    * （選択している内容については正直、公式の例がこうしているからここでもそうしている、という程度なので、詳しいオプション内容については追って調査が必要）


また、Cognitoの認証を行う際に予め設定したドメインが必要になるので、"アプリの統合"の"ドメイン名"を開き、適当なドメインを設定しておく：
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680173/dab79be1-e68e-937d-ce36-6d23206a4c4c.png)



最後に、NextAuth側が作成したCognitoユーザープールを参照するためには、ユーザープールおよびアプリクライアントに関する情報をいくつか控えておく必要がある

- "全般設定"から、プールID: `ap-northease-1_XXXXXXXXXX`のような値をメモ
    * https://next-auth.js.org/providers/cognito に書いてあるところの`issuer`に使う
- "アプリクライアント"の"詳細を表示"をクリックし、アプリクライアントIDとアプリクライアントのシークレットをそれぞれメモ

### Next.jsのセットアップ

まずはNext.jsのプロジェクトを立ち上げる必要がある。
yarnを使う場合、

```bash
yarn create next-app <プロジェクト名> --typescript
```
のようにすれば、`<プロジェクト名>`としたディレクトリ（パス）にNext.jsのプロジェクトが生成されている。
（詳細は[公式のGetting Start](https://nextjs.org/docs#automatic-setup)を参照）

ついで、
```bash
yarn add next-auth
```
などとして、NextAuthを入れる。

念のため、yarnでインストールしたパッケージの主要なもの（dependencies）は以下の感じ：

- "next": "12.1.5"
- "next-auth": "4.3.3"
- "react": "17.0.2"
- "react-dom": "17.0.2"

なお、執筆時点（20224月下旬）で入るReactのバージョンは18.0.0だが、諸々の都合で17.0.2にしたかったので今回の検証では自分で入れ直している。

また、本質的でないが`pages`と`styles`も別途`src`ディレクトリを作成し、その下に移している。

#### next-auth用のapi設定

（exportしない）Nextjsは`pages/api/`以下のソースによってAPI機能を使うことができるが、
NextAuthは`api/auth/auth/***`といったAPIを使用して動作するため、`pages/api/auth/[...nextauth].ts`というファイルを作成・編集する必要がある。

Cognitoを使う場合、最低限以下のような記述が必要：

```typescript:[...nextauth].ts
import NextAuth from 'next-auth';
import CognitoProvider from 'next-auth/providers/cognito';

export default NextAuth({
  providers: [
    CognitoProvider({
      clientId: process.env.COGNITO_CLIENT_ID || '',
      clientSecret: process.env.COGNITO_CLIENT_SECRET || '',
      issuer: process.env.COGNITO_ISSUER,
    }),
  ],
});
```
（[公式の説明](https://next-auth.js.org/providers/cognito)も参照）

ポイントとしては、`process.env.***`によって環境変数を読み込ませている。
また、`clientId`と`clientSecret`は必須項目なので、ナイーブに書くとTypeScriptの型関連のエラーが出る。ワークアラウンドとして、`process.env.COGNITO_*** || ''`のような書き方をすることで、とりあえずstringとして値が設定されるようにしている。

ローカルで動作させる際（今回）はプロジェクトのトップのディレクトリ上に`.env.local`を作成し、そこに適宜必要な値を記入する.

```sh:.env.local
COGNITO_CLIENT_ID=XXXXXXXXXXXXXXXXXXXXXXX
COGNITO_CLIENT_SECRET=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
COGNITO_ISSUER=https://cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_XXXXXXXXXXX
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=secret-string
```

`COGNITO_***`となっている3つは、先程cognitoの設定画面で控えておいた3つの値を使うことで埋められる。
`CLIENT_ID`と`CLIENT_SECRET`はアプリクライアントのIDとシークレット、`ISSUER`は使用するリージョン名とプールIDの値から組み立てる。

また、個人的にハマったところだが、それ以外にも設定しないとErrorやWarningが出る部分がある。
（詳しいところは公式の[Environments Variables](https://next-auth.js.org/configuration/options#environment-variables)のあたりを参照）

まず、`NEXTAUTH_SECRET`(string)が無いとエラーが発生して動作しない。
本番環境だと取り扱いが重要な項目だと思われるが、とりあえず手元で簡単に動かす分には適当な文字列を`.env.local`に入れておく。

また、`NEXTAUTH_URL`も設定しないと
```
[next-auth][warn][NEXTAUTH_URL]
ext-auth.js.org/warnings#nextauth_url
```
のような警告が多数出るので、動作環境（=localhost）の情報を書いておく。

次に、ページに認証をかけるための記述をしていく。

まず、`pages/_app.tsx`を編集して、例えば以下のような形にする

```typescript:_app.tsx
import '../styles/globals.css';
import { SessionProvider } from 'next-auth/react';

import type { AppProps } from 'next/app';

const MyApp = ({ Component, pageProps: { session, ...pageProps } }: AppProps) => {
  return (
    <SessionProvider session={pageProps.session} refetchInterval={5 * 60}>
      <Component {...pageProps} />
    </SessionProvider>
  );
};

export default MyApp;
```
本質的な変化はもともとあった`Component`をNextAuthの`SessionProvider`で挟んでいるところ。
（↑は[ここ](https://next-auth.js.org/getting-started/upgrade-v4#sessionprovider)を参考に書いている）

次に、認証が必要なページとして、`pages/protected.tsx`を作成する。
必要な処理はセッション情報の取得、サインイン、サインアウトあたりになるので、

- https://next-auth.js.org/getting-started/client#usesession
- https://next-auth.js.org/getting-started/client#signin
- https://next-auth.js.org/getting-started/client#signin

あたりを参照しつつ、例えば以下のような感じで作成する：
```typescript:protected.tsx
import {useSession, signIn, signOut} from "next-auth/react";

import type React from 'react';

const Home: React.VFC = () => {
  const { data: session, status } = useSession();

  if (status === 'loading') {
    return null;
  }

  if (session) {
    return (
      <>
        Signed in as {session?.user?.email} <br/>
        <button onClick={() => signOut()}>Sign Out</button>
      </>
    );
  }

  return (
    <>
      Not signed in <br/>
      <button onClick={() => signIn()}>Sign in</button>
    </>
  )
}
export default Home;
```
参考までに、
- ログイン・ログアウト用のボタン
- ログイン時はログイン情報としてメールアドレスを表示

といった機能にしている。

## 動作

```bash
yarn build
yarn start
```
のようにするか、より簡易的で良ければ
```bash
yarn dev
```

のようにすることで、普通の環境では`localhost:3000`にnext.jsのアプリが立ち上がっている。

今回認証の対象として作ったページは、 http://localhost:3000/protected
でアクセスできる。

- 非認証時
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680173/c3b27967-5a7c-0127-c72a-5bbf3f00f093.png)

- SignIn押下
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680173/430b2a4c-725e-b3d3-df63-2ae91f052d38.png)
複数プロバイダー（GoogleアカウントやGitHubなど）から選べるようにすると、同列に色々と増えるのだと思われる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680173/f7effed1-a627-079d-9b24-e589855dcb25.png)
`Sign in with Cognito`を押すと、今回は特にカスタマイズなどしていないので、AWSが用意したサインイン用のUIが表示される。(Sign upからログイン用のユーザーを作れる)

適当なユーザーを作ってログインすると、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680173/a1403a07-af57-d324-1f4d-0d03e82035df.png)
のようにログイン情報としてメールアドレスが表示され、
`Sign Out`を押すとログイン前の状態に戻る.


## 改善点など

- セッション情報としてメールアドレスを出しているが、実はログインユーザー名など、今回作成したソースではいろいろな必要そうな情報が取得できていない。
    * https://next-auth.js.org/configuration/callbacks を参考に、`jwt`および`session`コールバックを記述していくことで可能そうであることまでは大体確認できている。（ログイン時に取得できるprofile情報から`cognito:username`といったものを取得する必要がありそう）
    * とはいえ、書き方のお作法的な部分だったり、あるいはセキュリティ的な観点で注意点があるかもしれないので、引き続き調べている必要がある。
 - cognitoというよりほぼNextAuthの使い方の問題になりそうな気がしているが、expiredしたセッション情報の更新といった実用上必要な機能の実現には色々と調査が必要


## 参考

- [公式](https://next-auth.js.org/)
- https://zenn.dev/tatsurom/articles/next-auth-cognito
    * NextAuthのバージョンがv3系列なので読み替えが必要