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 32
NEXTAUTH_SECRET
はメール認証トークンのハッシュ化とJWTの暗号化に必要なもので、上記のコマンドで生成できる。
3. ライブラリのインストール
npm i next-auth@beta @auth/drizzle-adapter @auth/core
4. 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:push
7. プロフィール画面を実装
ログイン中のアカウント情報を表示する/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を含めることで型も補完される。