Next.js (App Router) にAuth.jsでGoogle認証機能を実装する

要件

  • Googleアカウントでログイン・ログアウトする
  • ログイン状態に応じて画面の要素を出し分ける・リダイレクトさせる (認可)
  • ログインしたユーザーとセッション情報をDBに保存する

環境・技術

  • Next.js 14 (App Router)
  • Drizzle ORM (PostgreSQL)

実装

0. ORMの導入

下記記事のようにDrizzle ORMの設定が完了している前提で進める。

【Next.js】Drizzle ORMを導入してマイグレーションするまで

1. GCPの設定

まずクライアントIDの作成 | Google Cloudに沿ってGoogleのOAuthクライアントIDとシークレットキーを取得する。

2. 環境変数を設定

.env.local
GOOGLE_CLIENT_ID=xxx
GOOGLE_CLIENT_SECRET=yyy
NEXTAUTH_SECRET=zzz
NEXTAUTH_URL=http://localhost:3000

先程GCPで取得したクライアントIDとシークレットを環境変数に設定する。

Terminal window
openssl rand -base64 32

NEXTAUTH_SECRETはメール認証トークンのハッシュ化とJWTの暗号化に必要なもので、上記のコマンドで生成できる。

3. ライブラリのインストール

Terminal window
npm i next-auth@beta @auth/drizzle-adapter @auth/core

4. providerとadapterを設定

src/infra/auth/index.ts
import type { NextAuthConfig } from 'next-auth';
import NextAuth from 'next-auth';
import GoogleProvider from 'next-auth/providers/google';
import { DrizzleAdapter } from '@auth/drizzle-adapter';
import { db } from '@/infra/db';
const authConfig = {
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
}),
],
adapter: DrizzleAdapter(db),
} satisfies NextAuthConfig;
export const { handlers, auth, signOut } = NextAuth(authConfig);

認証にまつわる設定を実装する。

  • 今回はGoogle認証のみを利用するのでprovidersGoogleProviderを指定
  • adapterDrizzleAdapterを指定するだけで、Drizzle ORMを介して対象のテーブルに読み書きしてくれる

以降はここでexportした{ handlers, auth, signOut }を利用していく。

5. 認証用のAPI Routeを作成

src/app/api/auth/[...nextauth]/route.ts
import { handlers } from '@/infra/auth';
export const { GET, POST } = handlers;

6. テーブルを作成

6-1. スキーマを宣言

Drizzle ORMを介して対象のテーブルに読み書きするためには、adapter側で想定しているテーブル・カラムを用意する必要がある。

そこで@auth/drizzle-adapterの公式ドキュメントに記載されている通りにスキーマを宣言する (一部カラムの型や制約はカスタムしている) 。

src/infra/db/schemas/user.schema.ts
import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
export const UserSchema = pgTable('user', {
id: uuid('id').defaultRandom().notNull().primaryKey(),
name: text('name'),
email: text('email').notNull().unique(),
emailVerified: timestamp('emailVerified', { mode: 'date' }),
image: text('image'),
});
src/infra/db/schemas/account.schema.ts
import { integer, pgTable, primaryKey, text, uuid } from 'drizzle-orm/pg-core';
import type { AdapterAccount } from '@auth/core/adapters';
import { UserSchema } from '@/infra/db/schemas/user.schema';
export const AccountSchema = pgTable(
'account',
{
userId: uuid('userId')
.notNull()
.references(() => UserSchema.id, { onDelete: 'cascade' }),
type: text('type').$type<AdapterAccount['type']>().notNull(),
provider: text('provider').notNull(),
providerAccountId: text('providerAccountId').notNull(),
refresh_token: text('refresh_token'),
access_token: text('access_token'),
expires_at: integer('expires_at'),
token_type: text('token_type'),
scope: text('scope'),
id_token: text('id_token'),
session_state: text('session_state'),
},
(account) => ({
compoundKey: primaryKey({
columns: [account.provider, account.providerAccountId],
}),
}),
);
src/infra/db/schemas/session.schema.ts
import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
import { UserSchema } from '@/infra/db/schemas/user.schema';
export const SessionSchema = pgTable('session', {
sessionToken: text('sessionToken').notNull().primaryKey(),
userId: uuid('userId')
.notNull()
.references(() => UserSchema.id, { onDelete: 'cascade' }),
expires: timestamp('expires', { mode: 'date' }).notNull(),
});

6-2. マイグレーション

Terminal window
npm run db:generate
npm run db:push

7. プロフィール画面を実装

ログイン中のアカウント情報を表示する/meを実装する。

src/app/me/page.tsx
import { auth, signOut } from '@/infra/auth';
import { redirect } from 'next/navigation';
export default async function MyPage(): Promise<JSX.Element> {
const session = await auth();
if (!session?.user) {
redirect('/api/auth/signin');
}
const { user } = session;
return (
<>
<div>{user.name}</div>
<div>{user.email}</div>
<img src={user.image} alt={user.name} width="50" height="50" />
<form
action={async () => {
'use server';
await signOut({ redirectTo: '/' });
}}
>
<button type="submit">logout</button>
</form>
</>
);
}
  • src/infra/auth/index.tsでエクスポートしたauth()でセッション情報を取得し、ユーザー情報を表示
  • 「logout」ボタンを押下するとServer Action内でsignOut()を呼ぶ
  • 未ログインであればログインページにリダイレクト

8. 動作確認

Googleのログインボタンが表示された

/api/auth/signinにアクセスするとログインボタンが表示された。

Googleアカウントのログイン画面が表示される

押下するとGoogleアカウントのログイン画面が表示されるのでアカウントを選択。

対象ユーザーの情報が表示された

/meにアクセスすると、対象ユーザーの情報が表示された。

Userテーブルを参照してみるとレコードも挿入されていることが確認できる

npm run db:previewでDrizzle Studioを起動し、UserAccountSessionテーブルを参照してみるとレコードも挿入されていることが確認できる。

また「logout」ボタンを押下し/meにアクセスするとトップページにリダイレクトされ、Sessionテーブルのレコードも削除された。

発展

ミドルウェアで認可する

今回の/meのようなページのみを保護対象にしたいので、ミドルウェアを用いて認可 (ガード) 機能を実装する。

src/middleware.ts
import { auth } from '@/infra/auth';
export default auth;
export const config = {
matcher: ['/((?!api/auth).*)'],
};

ミドルウェアの設定として認証周りのRouteを対象から除外し、あとはauthをdefault exportするだけ。

src/infra/auth/index.ts
import type { NextAuthConfig } from 'next-auth';
import NextAuth from 'next-auth';
import GoogleProvider from 'next-auth/providers/google';
import { DrizzleAdapter } from '@auth/drizzle-adapter';
import { db } from '@/infra/db';
import { NextResponse } from 'next/server';
const PROTECTED_PATHS = ['/me'];
export const authConfig = {
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
}),
],
adapter: DrizzleAdapter(db),
callbacks: {
authorized({ auth, request: { nextUrl } }) {
const isLoggedIn = !!auth?.user;
if (isLoggedIn) return true;
const isProtectedPath = PROTECTED_PATHS.some((path) =>
nextUrl.pathname.startsWith(path),
);
if (!isProtectedPath) return true;
return NextResponse.redirect(new URL('/api/auth/signin', nextUrl));
},
},
} satisfies NextAuthConfig;
export const { handlers, auth, signOut } = NextAuth(authConfig);

未ログインで/meにアクセスすると/api/auth/signinにリダイレクトされるようになった。

session.userに任意のプロパティを含める

src/infra/auth/index.ts
...
adapter: DrizzleAdapter(db),
callbacks: {
async session({ session, user }): Promise<Session> {
if (!session.user) return session;
session.user.id = user.id;
return session;
},
},
} satisfies NextAuthConfig;

デフォルトだとsession.userオブジェクトにidが含まれないが、session()をオーバーライドして含めることも可能。

src/infra/auth/next-auth.d.ts
import type { DefaultSession } from 'next-auth';
declare module 'next-auth' {
interface Session {
user: {
id: string;
} & DefaultSession['user'];
}
}

またSessionの型を拡張してidを含めることで型も補完される。

12

参考
  1. Next.js 14 Auth with Auth.js and Drizzle ORM

  2. NextAuth.jsを使ったGoogle認証機能+データベース(Prisma)の設定の理解 | アールエフェクト