Next.js アプリに Cognito の認証を良い感じに設定する

6 min read読了の目安(約6000字

Next.js はほとんど React アプリ開発のスタンダードになっているようですが、認証機能の実装に少し困りました。 要件は以下です🧐

  • AWS Cognito を使いたい
  • なるべく早く構築したい
  • セキュリティを犠牲にしたくない

今回は cognito を Next.js に良い感じに埋め込む方法をやってみたいと思います!!

細かい部分の解説まではしませんので、コードを確認してみたい方はこちらをどうぞ🙌

https://github.com/tatsuro-m/next-auth-cognito

やる

Cognito 設定

ほとんどテンプレート通りなのでざっくりいきます!

  1. ユーザープールを作成しますが、「デフォルトを確認する」でOKです。
  2. 基本はデフォルトですが、アプリクライアントだけは設定が必要なのでここで済ませましょう。名前を入力するだけでOKです。
  3. ユーザープールが正常に作成されればOKです。
  4. 認証関係の画面には Hosted UIを利用するのでアプリクライアントを統合します。 ログイン時のコールバックURL に https://localhost:3000/api/auth/callback/cognito と入力します。
    画面上部にも表示されますが、この URL はあくまでも開発環境のものであり、本番環境にデプロイする場合には変更する必要があります。
  5. 必要なフローを設定します。 OAuth スコープのところで適切な権限を与えるのを忘れないでください。
  6. ドメインも設定しておきましょう。利用できると表示されるものなら何でもOKです。

これで cognito の設定は完了です。

Next.js 設定

まずは適当に Next.js アプリの雛形を用意しましょう。

https://nextjs.org/docs/api-reference/create-next-app
$ yarn create next-app

そして、

https://next-auth.js.org/
というライブラリを使います。名前の通り、 Next.js に認証機能を統合するのに便利なライブラリです。 かなり人気の、デファクトっぽいライブラリのようです。 cognito の他にも Google, App, GitHub
での認証などメジャーどころは大体押さえているようです。

雛形を作成したら、 next-auth をインストールします🙌

$ yarn add next-auth
  1. cognito の画面から必要な環境変数を集めて設定します。
.env.local
COGNITO_CLIENT_ID=****************************
COGNITO_CLIENT_SECRET=****************************
COGNITO_DOMAIN=***.auth.ap-northeast-1.amazoncognito.com

そのままですが、

  • アプリクライアントID
  • アプリクライアントシークレット
  • ドメイン(先ほど画面から自分で設定したもの) が必要です。
  1. cognito-auth の設定ファイルに cognito プロバイダとして登録します。
pages/api/auth/[...nextauth].js
import NextAuth from 'next-auth';
import Providers from "next-auth/providers";

export default NextAuth({
  providers: [
    Providers.Cognito({
      clientId: process.env.COGNITO_CLIENT_ID,
      clientSecret: process.env.COGNITO_CLIENT_SECRET,
      domain: process.env.COGNITO_DOMAIN,
    })
  ]
})
  1. ログイン画面への導線を作ります。今回は index.js に直書きしちゃいます!
pages/index.js
import {useSession, signIn, signOut} from "next-auth/client";
import Link from "next/link";

export default function Home() {
  const [session, loading] = useSession();

  if (loading) {
    return null;
  }

  if (session) {
    return (
      <>
        Signed in as {session.user.email} <br/>
        <button onClick={() => signOut()}>Sign Out</button>
        <br/>
        <Link href="/setting">
          <a>設定ページへ</a>
        </Link>
      </>
    );
  }

  return (
    <>
      Not signed in <br/>
      <button onClick={() => signIn()}>Sign in</button>
    </>
  )
}

next-auth が提供するフックを上手く利用してログインしているかどうかの条件分岐やサインインページへのリンクを作成しています。

  1. _app.js を修正します。
pages/_app.js
import '../styles/globals.css'
import {Provider} from "next-auth/client";

function MyApp({Component, pageProps}) {
  return (
    <Provider session={pageProps.session}>
      <Component {...pageProps} />
    </Provider>
  );
}

export default MyApp;
  1. サーバーを起動して動作確認しましょう!
$ yarn run dev


「Sign In」 ボタンを押すと Hosted UI からサインアップすることができます😁 本人確認のメールも実際に送信されます。 ここは特に問題無いですね。

これで完了です!

Vercel にデプロイする際に注意すること

vercel へのデプロイ手順は非常に簡単なので解説はしませんが、少し注意点もあったのでメモしておきます。
基本的には環境ごとにユーザープールを分けると思いますが、今回は使い回すので適宜読み替えてください。

vercel のプロジェクト設定画面から環境変数を設定します。内容としては先ほど設定したものが必要になります。

.env.local
COGNITO_CLIENT_ID=****************************
COGNITO_CLIENT_SECRET=****************************
COGNITO_DOMAIN=***.auth.ap-northeast-1.amazoncognito.com

次に、先ほど http://localhost:3000/api/auth/callback/cognito と設定したログイン時のコールバック URL を書き換えます。
本番のドメインにする必要があるので、 https://{app-name}.vercel.app/api/auth/callback/cognito のような名前になると思います。

しかしここまでやっても上手く vercel 上で Hosted UI が表示されませんでした。。。
少しハマりましたが、他にも next-auth の環境変数 NEXTAUTH_URL が必要でした😅

https://next-auth.js.org/configuration/options
設定する値としてはシンプルに vercel の URL です。
NEXTAUTH_URL=https://example.com

これで上手く動くようになりました!

ログインしていない時のリダイレクト

どのアプリケーションにも「ログインしていないユーザーには見せたくない画面」というのがあると思います。
ログインしていない状態でアクセスするとリダイレクトさせることが多いですよね。

これをどのように実装するかというので色々調べたのですが、 Zenn の開発者でもある catnose さんの以下の記事が非常に参考になりました。

https://zenn.dev/catnose99/articles/2169dae14b58b6#3.-ログインが必要なページ用のカスタムフックを作る

こちらを参考にしてカスタムフックを作成し、ログインが必要なページに埋め込むというのをやりました。

hooks/useRequireLogin.js
import {useEffect} from 'react';
import {useRouter} from 'next/router';
import {useSession} from "next-auth/client";

export function useRequireLogin() {
  const [session, loading] = useSession();
  const router = useRouter();

  useEffect(() => {
    if (loading) return; // まだ確認中
    if (!session) router.push("/"); // 未ログインだったのでリダイレクト
  }, [loading, session])
}

アプリ全体の設定を行う _app.js では特に何もせず、ログインが必要なページにフックを挿入します。

pages/setting.js
import {useRequireLogin} from "../hooks/useRequireLogin";

export default function Setting() {
  useRequireLogin();

  return (
    <>
      <h1>ログインしています!</h1>
      <p>設定画面</p>
    </>
  )
}

もっと良いやり方あると思いますが、今回はこれで。。。


参考 URL


かなりざっくりですが、どなたかの参考になれば幸いです。