🔐

NextAuth.js と AWS Cognito で作る認証機能

2024/08/02に公開

こんにちは。フクロウラボの今泉です。
今回は新しいプロダクトで認証機能を実装する必要があり、 NextAuth と AWS Cognito の組み合わせで実装したので備忘として記事にします。
NextAuth と AWS Cognito の連携のポイントに焦点を当て、それぞれの詳細に関しては、検索すると良記事がたくさん見つかると思うので深く掘り下げないことにします。

NextAuth とは

Next.js で認証機能を簡単に導入するための認証ライブラリです。
最近では v5 が beta 版になり、v5 からは名前を Auth.js としてあくまで Next.js だけではなく、様々な jsフレームワークで利用できると謳っているようです。
https://authjs.dev/

様々な方法で認証機能を組むことができ、認証状態の管理が簡単にできるのが特徴です。

NextAuth を選定した理由

上述の通り、Next.js (jsフレームワーク) と親和性が高く認証機能を簡単に組めることが大きな理由ですが、それとは別に Cognito との連携も簡単にできるのが大きな決め手でした。
フクロウラボではインフラを主に AWS で組んでおり、認証プロパイダーに Cognito を使用するのが前提要件としてありました。
また、そういった外部プロパイダーとの連携をしつつも、処理の各フェーズで独自のロジックを組むことも可能で、拡張性の観点でもメリットを感じました。
Cognito 以外でも Google や X などのプロバイダーにも対応しており、もちろんユーザー名、パスワードで独自の資格情報を扱うことも可能です。

構成

認証の流れは至ってシンプルです。

サインインとサインアップは Cognito のホストされた UI を利用しており、このUI から実際に Cognito の API を実行しています。
Next での実装の前に Cognito の設定と環境変数の準備をします。

Cognito の設定

アプリケーションクライアント > ホストされた UI の編集

  1. 許可されているコールバック URL に追加します。
    domain/api/auth/callback/cognito
  2. 許可されているサインアウトURLに 1 と同様の URL を追加します。

URL をみて分かるように、ここでは Next の API エンドポイントを許可する設定を行なっています。

環境変数の設定

  1. CognitoUserPoolId
     → プロバイダーの設定、ログアウト処理に必要
  2. CognitoUserPoolId
     → プロバイダーの設定、ログアウト処理に必要
  3. CognitoClientSecret
     → プロバイダーの設定、ログアウト処理に必要
  4. CognitoLogoutUrl
     → federated log out を実行する際のエンドポイント
    https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/logout-endpoint.html
  5. AuthUrl
    → NextAuth を実行する際のエンドポイント
    domain/api/auth
  6. AuthSecret
    → openssl コマンドで生成した任意の値

処理の流れ

Next の middleware でNextAuthを実行します。

middleware.ts
export { auth as middleware } from "auth";

NextAuthはコールバックで、要件に応じて様々なロジックを追加することが可能です。今回はポイントのみ後述します。
https://next-auth.js.org/configuration/callbacks

今回は auth.ts として middleware に直接書かずに import しています。NextAuthの返り値は以下です。

auth.ts
export const { auth, handlers, signIn, signOut } = NextAuth(config);

Next の RouteHandler を実装します。

api/auth/[...nextauth]/route.ts
import { handlers } from "auth";
export const { GET, POST } = handlers;

リクエストが来た際にまず middleware でNextAuthが実行され、 RouteHandler でNextAuthの返り値である hendlers から GET と POST のエンドポイントを引っ張っています。 これで /api/auth/* のエンドポイントができました。

※ キャッチオールセグメント[...nextauth]で書くことで /api/auth/* のリクエストで実行されます。

サインイン

カスタムサインイン画面でサインインボタンを押下した際に NextAuth のsignInを実行します。
signIn("cognito", { redirectTo: "/" });
序盤で設定した Cognito のホストされた UI にリダイレクトします。
ここで先述したエンドポイントの許可設定が必要になるわけですね。

サインアウト

サインイン同様、サインアウトも NextAuth のsignOutを実行するのですが、これはあくまで NextAuth の認証情報のみが破棄されるだけで、Cognito の認証情報は破棄されません。
この場合、ログイン画面からsignInを実行しても Cognito の認証情報は残っているためホストされた UI にリダイレクトされません。(Cognito の挙動)
今回、アカウントの切り替えを可能にする必要がありましたが、このままでは別のアカウントに切り替えることができないので、NextAuth の callbacks の中で処理を書きました。
signOutが実行された際にコールバックを実行したいので、callbacks の redirect で Cognito の federated log out を実行します。

auth.ts
callbacks: {
  :
  :
  redirect({ baseUrl, url }) {
    if (url.startsWith(baseUrl)) {
      return url;
    }

    // cognito の logout エンドポイント を実行
    if (
      url === "signOut" &&
      typeof process.env.COGNITO_LOGOUT_URL !== "undefined"
    ) {
      const params = new URLSearchParams({
        client_id: process.env.COGNITO_CLIENT_ID || "",
        logout_uri: `${process.env.BASE_URL}`,
        redirect_uri: `${process.env.BASE_URL}`,
        response_type: "code",
      });

      return `${process.env.COGNITO_LOGOUT_URL}?${params.toString()}`;
    }

    if (url.startsWith("/")) {
      return new URL(url, baseUrl).toString();
    }

    return baseUrl;
  },
}

Cognito の認証情報をセッションに登録

いざ認証機能ができたら次は認証情報を参照したり、認可実装のフェーズに移行すると思います。
今回は、認可を実装するにあたって、Cognito sub の値や紐づくユーザーグループを参照する必要がありました。
コンポーネントからこういった認証情報を参照する方法の前に、NextAuth で引き回すセッション情報の中に Cognito の値を追加します。
認証後 Cognito から渡る access_token の中に sub や ユーザーグループの値が入っています。 この値を取得したいので、jwt が更新される度に実行する callbacks の jwt に処理を書いていきます。

auth.ts
callbacks: {
  async jwt({ account, token, user }) {
    const expansionToken: ExpansionToken = token as ExtendedToken;

    if (account) {
      // accessToken を追加
      expansionToken.accessToken = account.access_token;
    }

    return { ...expansionToken, ...user };
  },
}

認証情報の参照

NextAuth は認証機能の実装だけではなく、認証情報の管理も簡単に実装することが可能です。
前述したセッション情報はサーバーサイド、クライアントサイドどちらからも参照が可能です。

クライアントサイド

例えば Layout など、セッションを引き回したいツリーの上位で<SessionProvider>でコンポーネントをラップします。

layout.tsx
<SessionProvider>
  <MyComponent />
</SessionProvider>

SessionProvider 以下で読み込まれるコンポーネントで次のように カスタムhooks を利用してセッションの参照ができます。

MyComponent.tsx
import { useSession } from "next-auth/react"
import { jwtDecode } from "jwt-decode";

const { data: session, status } = useSession();
// session から の accessToken を取り出す
const cognitoAccessToken = session?.accessToken;
// ライブラリではなく next-auth/jwt の getToken を使うでも可
const decoded = jwtDecode<CustomJwtPayload>(session?.accessToken || "");
const userGroups = decoded?.["cognito:groups"];

サーバーサイド

サーバーサイドでセッションを取得したい場合はgetServerSessionを使用します。
https://next-auth.js.org/tutorials/securing-pages-and-api-routes
session から accessToken を参照する方法はクライアントサイドと変わりないためコードは割愛します。

最後に

実装当時、NextAuth と AWS Cognito の事例をあまり見つけることができず、躓いた箇所も多々ありましたが、記事としてまとめてみるとやはり簡単でシンプルに認証機能の実装ができたなぁと改めて実感しました。
様々な記事を参考にさせて頂き、この場をお借りして感謝を述べたいと思います。
簡単になりましたが、この記事も誰かのお役に立てれば幸いです。

フクロウラボ エンジニアブログ

Discussion