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. 環境変数を設定
GOOGLE_CLIENT_ID=xxxGOOGLE_CLIENT_SECRET=yyyNEXTAUTH_SECRET=zzzNEXTAUTH_URL=http://localhost:3000先程GCPで取得したクライアントIDとシークレットを環境変数に設定する。
openssl rand -base64 32NEXTAUTH_SECRETはメール認証トークンのハッシュ化とJWTの暗号化に必要なもので、上記のコマンドで生成できる。
3. ライブラリのインストール
npm i next-auth@beta @auth/drizzle-adapter @auth/core4. providerとadapterを設定
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認証のみを利用するので
providersにGoogleProviderを指定 adapterにDrizzleAdapterを指定するだけで、Drizzle ORMを介して対象のテーブルに読み書きしてくれる
以降はここでexportした{ handlers, auth, signOut }を利用していく。
5. 認証用のAPI Routeを作成
import { handlers } from '@/infra/auth';
export const { GET, POST } = handlers;6. テーブルを作成
6-1. スキーマを宣言
Drizzle ORMを介して対象のテーブルに読み書きするためには、adapter側で想定しているテーブル・カラムを用意する必要がある。
そこで@auth/drizzle-adapterの公式ドキュメントに記載されている通りにスキーマを宣言する (一部カラムの型や制約はカスタムしている) 。
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'),});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], }), }),);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. マイグレーション
npm run db:generatenpm run db:push7. プロフィール画面を実装
ログイン中のアカウント情報を表示する/meを実装する。
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. 動作確認

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

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

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

npm run db:previewでDrizzle Studioを起動し、User・Account・Sessionテーブルを参照してみるとレコードも挿入されていることが確認できる。
また「logout」ボタンを押下し/meにアクセスするとトップページにリダイレクトされ、Sessionテーブルのレコードも削除された。
発展
ミドルウェアで認可する
今回の/meのようなページのみを保護対象にしたいので、ミドルウェアを用いて認可 (ガード) 機能を実装する。
import { auth } from '@/infra/auth';
export default auth;
export const config = { matcher: ['/((?!api/auth).*)'],};ミドルウェアの設定として認証周りのRouteを対象から除外し、あとはauthをdefault exportするだけ。
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に任意のプロパティを含める
... 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()をオーバーライドして含めることも可能。
import type { DefaultSession } from 'next-auth';
declare module 'next-auth' { interface Session { user: { id: string; } & DefaultSession['user']; }}またSessionの型を拡張してidを含めることで型も補完される。