🦔

NextAuth Beta版v5を使ってみた

2024/06/03に公開

はじめに

今回、エンジニア研修でNextAuthを用いた認証を使うことになったので、どうせならということでBeta版のv.5を使用した。
実装するにあたって、意外と記事が少なかったのでここ残そうと思い記述する。

NextAuthとは?

https://next-auth.js.org/
Next.jsを用いた認証ライブラリになっており、このライブラリを使うことで認証の実装を簡単に行うことができる。
Google, Githubなどの主要プロバイダーを用いた認証も簡単に行うことができる。

NextAuth v5

https://authjs.dev/getting-started/migrating-to-v5
NextAuthのBeta版を使った一番の理由は今回作成したアプリではNextのApp Routerを使用したとこが大きく挙げられる。また、session情報の取り出しやmiddlewareの処理が従来のv.4より簡単に行うことができる。

バージョン情報

今回使用したバージョン

"next": "14.1.4",
"next-auth": "5.0.0-beta.16",
"react": "18.2.0",
"@aws-sdk/client-cognito-identity-provider": "3.554.0",

NextAuth v5のinstall方法

npm i next-auth

上記のコマンドを実行してNextAuthをinstallしてしまうと、v.4がinstallされてしまうので、
以下のコマンドを使ってinstallする。

npm install next-auth@beta

実装

今回は、AWSのcognitoを用いた認証方法を用いて開発を行ったので、その方法での実装例になる。
基本的にGoogle, githubを使う場合でも、後ほど紹介するProviderを変えるだけで実装できる。
また、dev環境, 本番環境の両方で動かせるような実装を紹介する。

AWSの設定

cognitoの中でgoogleなどの認証は今回は用いず、emailで行う。
cognitoのユーザープールの作成

  1. CognitoユーザープールのサインインオプションでEmailを選択する。
  2. パスワードポリシーはデフォルトでもカスタムでも問題ない。
  3. 多要素認証はオプションMFAで行う。
  4. Eメールは、CognitoでEメールを送信を選択。
  5. iamロールは適したものを選択。または作成。
  6. CognitoのホストされたUIを使用にチェックを追加。UIを作るところまで手が回らなかったため。
  7. コールバック関数でlocalで行う際は以下のurlを設定する。
    http://localhost:3000/api/auth/callback/cognito
    
    ここでホストされたUIを表示のボタンを押して、挙動を確認する。

cognitoのIDプールの作成

  1. ユーザーアクセスで認証されたアクセスのcognitoを選択する。
  2. ユーザープールは上記で作成したユーザープールを選択。

実装コード

next.jsのApp Routerでcreate appを行う。

root/
├─ app
├─  ├─ api
|   |  └─auth
|   |      └─[...nextauth]
|   |            ├─options.ts
|   |            └─ route.ts
|   ├─ page.tsx
|   └─ layout.tsx
├─ .env
├─ middleware.ts
└─ etc...

まず、環境変数の設定から

.env
export COGNITO_REGION="AWSで登録したregion"
export COGNITO_CLIENT_ID="cognitoのクライアントid"
export COGNITO_CLIENT_SECRET="cognitoのクライアントシークレット"
export COGNITO_ISSUER="https://cognito-idp.${NEXT_PUBLIC_COGNITO_REGION}.amazonaws.com/${NEXT_PUBLIC_COGNITO_USER_POOL_ID}"
export COGNITO_USER_POOL_ID="cognitoのユーザープールid"
export NEXTAUTH_SECRET="${openssl rand 32 | base64}で生成された文字列"
export NEXTAUTH_URL="http://localhost:3000"
options.ts
import NextAuth, { NextAuthConfig, Session } from 'next-auth';
import { JWT } from 'next-auth/jwt';
import CogntioProvider from 'next-auth/providers/cognito';

export interface CustomSession extends Session {
  accessToken?: string;
}

interface ExtendedToken extends JWT {
  accessToken?: string;
}

export const options: NextAuthConfig = {
  secret: process.env.NEXTAUTH_SECRET
  providers: [
    CogntioProvider({
      clientId: process.env.COGNITO_CLIENT_ID,
      clientSecret: process.env.COGNITO_CLIENT_SECRET,
      issuer: process.env.COGNITO_ISSUER,
    }),
  ],
  callbacks: {
    authorized({ request, auth }) {
      try {
        return Promise.resolve(!!auth);
      } catch (error) {
        return Promise.resolve(false);
      }
    },
    async jwt({ token, account, profile, user }) {
      const extendedToken: ExtendedToken = token as ExtendedToken;
      if (account) {
        extendedToken.accessToken = account.access_token;
      }
      return { ...extendedToken, ...user };
    },
    async session({ session, token, user }) {
      const extendedToken: ExtendedToken = token as ExtendedToken;
      const customSession: CustomSession = { ...session, accessToken: extendedToken.accessToken };
      return customSession;
    },
  },
  session: {
    strategy: 'jwt',
  },
  trustHost: true, // クラスタ内の通信を全て許可
  pages: {
    signIn: '/login',
  },
};

export const { handlers, auth, signIn, signOut } = NextAuth(options);

accessTokenを使用する際は、型定義がされていないので自分で拡張するしかなさそうだったので、
interfaceで追加。

  • secret: トークンのハッシュ、Cookie の署名/暗号化、暗号キーの生成の際に使われる。設定を行わないと本番環境でのエラーが引き起こされる。
  • providers: ここで使用するプロバイダーの設定を行う。今回はcognitoを使用したので、cognitoでの設定を行う。googleやgithubを用いる場合はここで設定する。こちらを参照。
  • callbacks: アクションが起こった際に、処理して欲しい事柄を記載することできる。
  • session: auth関数を呼び出した際に取得できるuser情報。アクセストークンを使う場合はここで追加。
  • trustHost: httpsからの通信を許可するために必要。本番環境に移した際に、ここを設定していなくて無駄に時間を浪費した。
  • pages: カスタムのサインイン、サインアウト、エラーページを作成する場合に使用するURLを指定できる。
  • auth: auth関数を呼び出すことにより、userのsession情報の取得ができる。
  • signIn: signIn関数を呼ぶだけでサインインの処理が行われる。
  • signOut: signOut関数を呼ぶだけでサインアウトが行われる。
route.ts
import { handlers } from './options';

export const { GET, POST } = handlers;

ルートハンドラーを使用してNextAuth.jsの初期化を行う。

middleware.ts
import { auth as middleware } from '@/app/api/auth/[...nextauth]/options';

export { middleware };

export const config = {
  matcher: ['/((?!api|_next/static|_next/image|.png|login).*)'],
};

middleware.tsでの最大の特徴はoptions.tsでexportしたauthを用いることで、認証済みかどうかを判断することができる。
user情報の取れない場合には、cognitoの設定で行った画面にリダイレクトされる。

次にサインインを行うためのformの実装

SignInForm.tsx
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
import React from 'react';
import { signIn } from '@/app/api/auth/[...nextauth]/options';
import { Button } from '@/features/ui/button';

export default function SignInForm() {
  return (
    <form
      action={async () => {
        'use server';
        await signIn('cognito');
        redirect('/');
      }}
      className="flex flex-col items-center"
    >
      <h2 className="text-center text-2xl font-semibold">
        はじめまして
      </h2>
      <Button variant="ghost" size="bold" className="mt-8 rounded-2xl text-white-white">
        ログイン
      </Button>
    </form>
  );
}

formのaction内で

import { signIn } from '@/app/api/auth/[...nextauth]/options';

await signIn('cognito');

上記の処理を行うだけでsignInを実行できる。

サインアウトも同じように行うことができる。

SignOutForm.tsx
'use client';

import { signOut } from 'next-auth/react';
import React from 'react';
import { Button } from '@/features/ui/button';

export default function SignOutForm() {
  return (
    <Button variant="delete" size="sm" className="m-2" onClick={() => signOut()}>
      ログアウト
    </Button>
  );
}

こちらも、

import { signOut } from 'next-auth/react';

上記の関数を呼び出して、actionを行うことでサインアウトできる。

userのsession情報を取りたい際は、

page.tsx
import { redirect } from 'next/navigation';
import { auth, CustomSession } from '@/app/api/auth/[...nextauth]/options';

export default async function Home() {
  const session: CustomSession | null = await auth();

  const response = await getUser(session?.accessToken ?? '');

  if (response.status === 404) {
    redirect(`/set-up-profile`);
  } else if (response.status === 200) {
      redirect(`/home`);
  } else {
    console.log('error');
    redirect(`/login`);
  }
}

function getUser(token: string) {
  return fetch(`${process.env.BACKEND_ENDPOINT}/current_user`, {
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${session?.accessToken}`,
    },
  });
}

上記のコードのように

const session: CustomSession | null = await auth();

このコード一行で、userのsession情報の取得が行える。
v4の時は、useSessionを使っての処理だったので、'use client'に切り分ける処理が必要になる。

v4
'use client'
import { useSession, signIn, signOut } from "next-auth/react"

export default function Component() {
  const { data: session } = useSession()
  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>
    </>
  )
}

このように比べるとv5ではかなりスッキリした感じがする。

まとめ

今回、NextAuthのBeta版を使用してみて、感じたことはauth関数のおかげで実装コストがグッと減った感じがした。それは、session情報を取るところやミドルウェアでの実装でものすごく感じた。
App Routerでの恩恵はかなり大きいのではないかと思う。

参考文献

https://authjs.dev/getting-started/migrating-to-v5
https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/what-is-amazon-cognito.html
https://qiita.com/Mikeinu/items/cf9e468c86a1aab4f03a

Discussion