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 };
⑤認証に関わる詳細の設定
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 },
};
},
},
};
⑥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>;
};
上記のプロバイダーを 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>
);
}
以上の設定をすることで各コンポーネントで以下のようにセッションを取得することができます。
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/jwtのdecodeを使うことで署名の検証ができます。
①ライブラリインストール
$ 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;
}
}
}
③定義したGuardをControllerで利用します。
@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);
},
);