0から始めるNextAuth
はじめに
こんにちは、株式会社サードスコープでインターンとして働いているkonnoです。既に入社から10ヶ月が経過しました。最近は、サードスコープの技術ブログ開発に携わっており、その中でNextAuthを使用することになりました。その使い方を紹介します。
この記事の目標
この記事を読めば、NextAuthを知らない人が0からログイン機能を実装することができます(今回はGoogleを利用したログインを例にします)。また、ログイン機能だけでなく、middlewareの設定もできるようになります。
middlewareとは、ログインしていないユーザーが閲覧できないページを設定できる機能のことです。
NextAuthとは?
Next.jsで簡単にソーシャルログイン機能を実装することができるライブラリです(パスワード認証も実装可能ですが、公式では推奨されていません)。
NextAuthとAuth0の比較
NextAuthは完全にオープンソースで、ソーシャルログインのオプションを自由に追加したり、自前のユーザーデータベースと連携したりすることが可能です。
一方、Auth0はフルフィーチャーの認証と認可プラットフォームで、多くのWebフレームワークやプログラミング言語に対応しています。Auth0は多機能であり、ユーザーマネージメント、多要素認証、アクティブディレクトリ連携など、エンタープライズレベルの要件をサポートします。
環境構築
使用する技術スタック
- Next.js(ver 13)
- TypeScript
- Prisma
Googleの認証情報取得
実装する上で、クライアントIDとクライアントシークレットが必須なので、以下の記事を参考に取得してください。
NextAuth.jsでNext.js13にGoogle認証機能を実装
NextAuthインストール
$ yarn add next-auth
環境変数の設定
SECRETという環境変数が必須なので、以下のコマンドをターミナルで入力し、値をコピーしておきます
$ openssl rand -base64 32
.envファイルを作成して、以下のように追記してください。
GOOGLE_CLIENT_ID=GoogleのクライアントID
GOOGLE_CLIENT_SECRET=Googleのクライアントシークレット
SECRET=open sslで取得した値
プロバイダーの設定
まず、pages/api/auth/[…nextauth].tsを作成してください(必ずこのディレクトリ構造で!)。このファイルに基本的な設定を記述していきます。
import { PrismaAdapter } from "@next-auth/prisma-adapter";
import { PrismaClient } from "@prisma/client";
import GitHubProvider from "next-auth/providers/github";
import GoogleProvider from "next-auth/providers/google";
import NextAuth from "next-auth/next";
import { NextAuthOptions } from "next-auth";
const prisma = new PrismaClient();
export const authOptions: NextAuthOptions = {
adapter: PrismaAdapter(prisma),
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID || "",
clientSecret: process.env.GOOGLE_CLIENT_SECRET || "",
}),
],
secret: process.env.SECRET,
session: {
strategy: "jwt",
maxAge: 30 * 24 * 60 * 60, // 30 days
},
pages: {
signIn: "/sign_in",
},
callbacks: {
async session({ session }) {
// ここに処理を書く
return session;
},
},
};
export default NextAuth(authOptions);
adapter: PrismaAdapterを使用します。アダプターを自作することも可能です。
providers: 使用したいサービスのclientIdとclientSecretを記述します。上記のコードではtypeエラー回避 のため一旦””を許していますが、要件によって記述を変更してください。
secret: 設定しないと機能しないので記述
session: jwtを使うかデータベースでsession管理するかを指定します。データベースを使用したい場合はstrategy: “database”と記述します。また、トークンがいつまで有効かをmaxAgeで設定できます。
pages: 自分で作ったログイン画面を使用するか、デフォルトで用意されているログイン画面を使用するかを指定。今回はpages/sign_in.tsxにログイン画面を作成するため、上記のように指定しました。
callbacks: ログインした時やログアウトした時、session情報を取得しようとした時の処理をカスタマイズできます(何も記述しなくても実装可)。
※とりあえずログイン機能作りたいよって人はcallbacksに何も記述しなくて大丈夫です。
複数の認証を実装したい場合
以下のように記述します。
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID || "", //
clientSecret: process.env.GOOGLE_CLIENT_SECRET || "",
}),
GitHubProvider({
clientId: process.env.GITHUB_CLIENT_ID || "",
clientSecret: process.env.GITHUB_CLIENT_SECRET || "",
}),
],
スキーマ作成
npx prisma initコマンドを入力し、prisma/schema.prismaを作成。
prisma/schema.prismaを以下のように記述し、npx prisma generateコマンドを入力し反映します。
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Account {
id String @id @default(cuid())
userId String
type String
provider String
providerAccountId String
refresh_token String? @db.Text
access_token String? @db.Text
expires_at Int?
token_type String?
scope String?
id_token String? @db.Text
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
@@index([userId])
}
model User {
id String @id @default(cuid())
name String?
email String? @unique
emailVerified DateTime?
image String?
accounts Account[]
}
フロントの実装
画面の実装
pages/_app.tsx
まずは、アプリ全体でsessionを利用できるようにpages/_app.tsxに以下の記述を追加
import { SessionProvider } from "next-auth/react"
export default function App({
Component,
pageProps: { session, ...pageProps },
}) {
return (
<SessionProvider session={session}>
<Component {...pageProps} />
</SessionProvider>
)
}
ログイン画面作成
pages/sign_in.tsxを作成し、下記のように記述します。
import { signIn } from "next-auth/react";
const SignIn = () => {
return (
<>
<h1>ログイン画面</h1>
<button onClick={() => signIn("google", { callbackUrl: "/hoge" })}>
SignIn With Google
</button>
</>
);
};
export default SignIn;
ログイン後の画面作成
ログアウトもできるようにします。pages/hoge.tsxを作成し、下記のように記述します。
import { signOut } from "next-auth/react";
const Hoge = () => {
return (
<>
<h1>ログインできました</h1>
<button onClick={() => signOut({ callbackUrl: "/sign_in" })}>SignOut</button>
</>
);
};
export default Hoge;
signIn(): 第一引数にプロバイダーのidを記述(基本的には小文字でサービス名を指定すればOK)。第二引数にcallbackURLにはログイン後にどのページにリダイレクトするかを指定できます。
signOut(): signIn同様callbackURLにリダイレクト先を指定できます(指定しなかった場合はベースURLに飛ぶ)。
実際にログインしてみる
ログイン前

SignIn With Googleを押すと…

見事ログインできました!SignOutボタンを押すと/sign_inにリダイレクトされるのを確認できると思います。
sessionユーザーの情報取得方法
クライアント側での取得方法(useSession())
const {data: session} = useSession();
、、、
<div>{session.user?.name}</div>
<div>{session.user?.image}</div>
<div>{session.user?.email}</div>
サーバー側での取得方法(getServerSession())
3つの引数が必要です(req, res, […nextauth].tsで設定したプロバイダー)。
export const getServerSideProps: GetServerSideProps = async (context) => {
const session = await getServerSession(context.req, context.res, authOptions);
const user = session?.user;
return {
props: { user },
};
};
番外編1(middlewareの設定)
middlewareとは?
ログインしていないuserが他のページのパスを直接指定して入ってこれないように設定できる機能です。middlewareはNext.jsの機能であり、srcディレクトリにmiddleware.tsという名前でファイルを作成し、そこにコードを記述することで動作します。
具体的な記述
早速実装してみましょう。src/middleware.tsを作成します。
import { withAuth } from "next-auth/middleware";
export default withAuth({
secret: process.env.SECRET,
});
export const config = {
matcher: ["/dashboard", "/article"],
};
withAuth(): この中にsecretを記述しないとエラーが起きます。
matcher: 配列の中にパス名を記述します。上記の記述は、/dashboardと/articleページを閲覧するには、ログインしないと閲覧できないということになります。
matcher: ["/((?!sign_in).*)"],
また、正規表現も使えるため上記のように記述すると/sign_inページ以外は全て認証が必要という設定になります。
注意点
- 「auth/[…nextauth].tsでpageのsignInを設定している場合(つまり、カスタムログイン画面を使用している場合)は、matcherにログイン画面のパスを除外する設定を記述しないと、無限ログインループが始まってしまうので注意が必要です。
- 一方、pageのsignInを設定していない場合(NextAuthで用意されたログイン画面を使用している場合)は、デフォルトで無限ログインが起こらないように設定されていますので、特に設定の変更は必要ありません。
- jwtのみでしかmiddlewareを設定できない点にも注意が必要です(sessionをデータベースで管理する場合は対応していません)。
番外編2(sessionユーザーのid・jwtのアクセストークンを取得)
sessionユーザーのidを取得
sessionからユーザーのidは取得できないの?」と思った方もいるかもしれませんが、、NextAuthではデフォルトでは型が設定されていないので取得できません(実際のところ、値としては存在しています)。
idを取得するには型を拡張してあげる必要があるので、型を拡張していきます。
types/next-auth.d.tsファイルを作成し、以下のコードを記述すると型を拡張することができます。
import type { DefaultUser } from "next-auth";
declare module "next-auth" {
interface Session {
user: DefaultUser & {
id: string;
};
}
}
そして、pages/api/auth/[…nextauth].tsファイルのcallbacksに以下の記述を追加します。
callbacks: {
async session({ session, token }) {
if (session.user != null && token.sub != null) {
session.user.id = token.sub;
}
return session;
},
},
tokenのsubにuserのidが格納されているため、ssession.user.idに代入する処理をしています。これでsessionユーザーのidを取得することができるようになりました。
const {data: session} = useSession();
、、、
<div>{session.user?.id}</div>
jwtのアクセストークンをuser情報に追加
上記のidと同じく、jwtのアクセストークンもデフォルトの型では定義されていないため、型を拡張してあげます。
[types/next-auth.d.ts]ファイルに記述していきます(ない場合は作成してください)。
import type { DefaultUser } from "next-auth";
import { JWT } from "next-auth/jwt";
declare module "next-auth" {
interface Session {
user: DefaultUser & {
accessToken: string;
};
}
}
declare module "next-auth/jwt" {
interface JWT {
accessToken?: string;
}
}
次に[…nextauth].tsのcallbacksにsessionのアクセストークンを代入する記述をしていきます。
callbacks: {
async session({ session, token }) {
session.user.accessToken = token.accessToken;
return session;
},
async jwt({ token, account }) {
if (account) {
token.accessToken = account.access_token
}
return token
}
},
これでsessionユーザーにアクセストークン情報を追加できました。
本番環境での注意点
本番環境で気をつけることは以下の2つです。
- クライアントIDとクライアントシークレットを取得した際に登録したcallbackURLやルートURLは、本番用のURLに変更するか、新たに作成して、それを本番用の環境変数に設定することが必要。
- NEXTAUTH_URLという環境変数を追加すること。(以下例)
NEXTAUTH_URL='http://localhost:3000'
なお、vercelにデプロイする場合はNEXTAUTH_URLではなく、
NEXT_PUBLIC_VERCEL_URL=https://hogehoge.vercel.app
と記述しないとダメだそうです!