logo

next-authをNextJS x NestJS で利用してみる

2023-09-16
a year ago

開発環境

  • Nextjs 13.4.10
  • next-auth 4.23.1

前提

  • next-authの認証はDB保存形式も選択できますが、今回はJWTを利用します。
  • プロバイダーはGithubを利用し、メールアドレス認証も実装します。
  • セキュリティに関わるところですので、部分的にでも参考になると嬉しいです。
  • NextJSではAppRouterを利用しています。
  • 今回は対象キーを利用して認証を行います。

本題

ざっくりと処理の流れは以下のようになります。

1. JWTをリクエスト(Next → next-auth)
2. CookieにJWTをセット(Next)
3. JWTと共にサーバーへリクエスト(Next → Nest)
4. JWTの検証OKなら必要な情報をレスポンス(Nest → Next)

NextJS、next-authの実装

①ライブラリインストール

$ yarn add next-auth

②ランダムな文字列生成

$ openssl rand -base64 32

.envに環境変数を設定

NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=                      // ②で生成した文字列を設定
GITHUB_CLIENT_ID=                     // 
GITHUB_CLIENT_SECRET=                 // 

GithubProviderを利用する際は、環境変数の設定が必要ですが、本記事では説明を割愛しますので公式ドキュメントのリンクを貼っておきます。

④Route Handler の設定

import NextAuth from 'next-auth';

import { options } from '@/lib/next-auth';

const handler = NextAuth(options);

export { handler as GET, handler as POST };
app/api/auth/[...nextauth]/route.ts

⑤認証に関わる詳細の設定

login関数ではサーバーにemail、passwordの組み合わせに該当するユーザーをリクエストしています。

import CredentialsProvider from 'next-auth/providers/credentials';
import GitHubProvider from 'next-auth/providers/github';

import {
  GITHUB_CLIENT_ID,
  GITHUB_CLIENT_SECRET,
  NEXTAUTH_SECRET,
} from '@/config/constants';
import { login } from '@/features/auth';

import type { NextAuthOptions } from 'next-auth';

export const options: NextAuthOptions = {
  secret: NEXTAUTH_SECRET,
  debug: true,
  session: { strategy: 'jwt' },
  providers: [
    GitHubProvider({
      clientId: GITHUB_CLIENT_ID,
      clientSecret: GITHUB_CLIENT_SECRET,
    }),
    CredentialsProvider({
      name: 'メールアドレス',
      credentials: {
        email: {
          label: 'メールアドレス',
          type: 'email',
          placeholder: 'user@example.com',
        },
        password: { label: 'パスワード', type: 'password' },
      },
      // メール認証
      async authorize(credentials) {
        if (!credentials) return null;
        try {
          return await login({ ...credentials });
        } catch (error) {
          return null;
        }
      },
    }),
  ],
  callbacks: {
    jwt: async ({ token, account }) => {
      if (account) {
        token.accessToken = account.access_token;
      }
      return token;
    },
    session: async ({ session, token }) => {
      return {
        ...session,
        token,
        user: { ...session.user },
      };
    },
  },
};
lib/next-auth.ts

⑥Session Providerの設定

気を付けるポイントとしてはクライアントで実行されるので'use client'を宣言する必要があります。

'use client';

import type { ReactNode } from 'react';

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

type AppProviderProps = {
  children: ReactNode;
};

export const AppProvider = ({ children }: AppProviderProps) => {
  return <SessionProvider>{children}</SessionProvider>;
};
providers/app.tsx

上記のプロバイダーを root の layout で利用します。

import type { ReactNode } from 'react';

import type { Metadata } from 'next';

import '@/styles/globals.css';
import { AppProvider } from '@/providers/app';

export const metadata: Metadata = {
  title: 'sample',
  description: 'sample',
};

export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="en">
      <body>
        <AppProvider>{children}</AppProvider>
      </body>
    </html>
  );
}
app/layout.tsx

以上の設定をすることで各コンポーネントで以下のようにセッションを取得することができます。

getServerSessionは Nextのサーバーで実行されます。

import type { ReactNode } from 'react';

import { getServerSession } from 'next-auth';

import { Footer } from '@/components/footer';
import { Header } from '@/components/header';
import { options } from '@/lib/next-auth';

export default async function SampleLayout({
  children,
}: {
  children: ReactNode;
}) {
  const session = await getServerSession(options);

  return (
    <>
      <Header />
      <main>{children}</main>
      <Footer />
    </>
  );
}

また、ログインはnext-authが用意しているsignInを利用することで簡単に実装することができます。

'use client';

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

export default function LoginPage() {
  return (
    <div className="flex flex-col items-center justify-between gap-12 p-24">
      <h1>ログインページ</h1>
      <button
        onClick={() =>
          signIn('github', { callbackUrl: 'http://localhost:3000' })
        }
      >
        GitHub
      </button>
      <button
        onClick={() =>
          signIn('credentials', {
            email: 'hoge@example.com',
            password: 'hogehoge',
            redirect: false,
            callbackUrl: 'http://localhost:3000',
          })
        }
      >
        メールアドレス
      </button>
    </div>
  );
}

NestJS、next-authの実装

next-auth/jwtdecodeを使うことで署名の検証ができます。

①ライブラリインストール

$ yarn add next-auth

.envに環境変数を設定

NEXTAUTH_SECRET=                      // Nextで生成した文字列を同様に設定

共通で利用するGuardを定義します。

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { decode } from 'next-auth/jwt';

@Injectable()
export class AuthGuard implements CanActivate {
  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    const authorization = request.headers?.authorization;
    if (!authorization) return false;
    const token = authorization.split(' ')[1];
    if (!token) return false;
    const secret = process.env.NEXTAUTH_SECRET ?? '';
    if (!secret) return false;
    try {
      const decoded = await decode({ token, secret });
      if (!decoded) return false;
      request.user = decoded;
      return true;
    } catch (error) {
      console.error(error);
      return false;
    }
  }
}
src/features/auth/guards/auth.guard.ts

③定義したGuardControllerで利用します。

@UseGuards(AuthGuard)

おまけ

axiosを利用してサーバーにリクエストする際に、AuthorizationヘッダにJWTを付与するサンプルの紹介です。

リクエスト前にセッション情報を取得し、encodeしたものをヘッダーに付与するようにしています。

import Axios, { InternalAxiosRequestConfig } from 'axios';
import { encode } from 'next-auth/jwt';
import { getSession } from 'next-auth/react';

import { API_URL, NEXTAUTH_SECRET } from '@/config/constants';

async function authRequestInterceptor(config: InternalAxiosRequestConfig) {
  const session = await getSession();

  if (session) {
    const encoded = await encode({
      token: session.token,
      secret: NEXTAUTH_SECRET,
    });

    config.headers.Authorization = `Bearer ${encoded}`;
  }
  config.headers.Accept = 'application/json';
  return config;
}

export const apiClient = Axios.create({
  baseURL: API_URL,
  headers: {
    'Content-Type': 'application/json',
  },
});

apiClient.interceptors.request.use(authRequestInterceptor);
apiClient.interceptors.response.use(
  (response) => {
    return response.data;
  },
  (error) => {
    return Promise.reject(error);
  },
);

参照