✍️
【Next.js】Supabase+Googleログインを実装する
今回は、Supabaseを使ったGoogleログイン機能の実装方法をまとめています。
Supabase ログイン
実装までの流れ
- 事前準備
- ライブラリをインストールする
- 環境変数に設定する
- Supabaseクライアントを設定する
- コールバック関数を作る
- ログイン/ログアウト関数を作る
- ログインページを作る
- 認証情報を判定/キャッシュする関数を作る
- 「ミドルウェア」で認証情報を確認する
- 「ページ内」で認証情報を確認する
事前に用意するもの
-
Google Cloud Platform
OAuth 2.0 クライアント ID
OAuth 2.0 クライアント シークレット
-
Supabase
Supabase URL
Supabase anon key
【0】事前準備
- プロジェクトの「Authentication」→「Providers」でGoogleをEnableにする
- GCPの「OAuth 2.0 クライアントID」「OAuth 2.0 クライアント シークレット」を
Client ID
とClient Secret
に設定する -
Callback URL
を取得する - GCPのOAuth 2.0 設定画面で「承認済みのリダイレクト URI」に
Callback URL
を追加する
【1】ライブラリをインストールする
$ npm install @supabase/supabase-js @supabase/ssr
【2】環境変数に設定する
# .env.local
SUPABASE_URL=https://xxxxx.supabase.co # プロジェクトURL
SUPABASE_ANON_KEY=xxxxxxxxxxxxxxxxxxxx # 公開用のAPIキー
SUPABASE_AUTH_URL=http://localhost:3001 # 認証後にリダイレクトするURL
【3】Supabaseクライアントを設定する
lib/supabase-auth/server.ts
// lib/supabase-auth/server.ts
"use server";
import { CookieOptions, createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
export async function createClient() {
const cookieStore = cookies()
return createServerClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return cookieStore.get(name)?.value;
},
set(name: string, value: string, options: CookieOptions) {
try {
if (typeof window === 'undefined') {
cookieStore.set({ name, value, ...options });
}
} catch (error) {
// Server Component内でのCookie設定エラーを無視
// ミドルウェアでCookieが処理される
}
},
remove(name: string, options: CookieOptions) {
try {
if (typeof window === 'undefined') {
cookieStore.set({ name, value: '', ...options, maxAge: 0 });
}
} catch (error) {
// Server Component内でのCookie削除エラーを無視
}
}
},
}
)
}
【4】コールバック関数を作る
api/auth/callback/route.ts
import { createClient } from '@/lib/supabase-auth/server'
import { NextResponse } from 'next/server'
// The client you created from the Server-Side Auth instructions
export async function GET(request: Request) {
const { searchParams, origin } = new URL(request.url)
const code = searchParams.get('code')
const next = searchParams.get('next') ?? '/'
if (code) {
const supabase = await createClient()
const { error } = await supabase.auth.exchangeCodeForSession(code)
if (!error) {
const forwardedHost = request.headers.get('x-forwarded-host') // original origin before load balancer
const isLocalEnv = process.env.NODE_ENV === 'development'
if (isLocalEnv) {
// we can be sure that there is no load balancer in between, so no need to watch for X-Forwarded-Host
return NextResponse.redirect(`${origin}${next}`)
} else if (forwardedHost) {
return NextResponse.redirect(`https://${forwardedHost}${next}`)
} else {
return NextResponse.redirect(`${origin}${next}`)
}
}
}
// return the user to an error page with instructions
return NextResponse.redirect(`${origin}/auth/auth-code-error`)
}
【5】ログイン/ログアウト関数を作る
lib/supabase-auth/authGoogle.ts
// lib/supabase-auth/authGoogle.ts
"use server";
import { createClient } from "@/lib/supabase-auth/server";
import { redirect } from "next/navigation";
// ---------------------------------------------
// Googleログイン
// ---------------------------------------------
export async function signInWithGoogle() {
// クライアントを作成
const supabase = await createClient();
const { data: { url }, error } = await supabase.auth.signInWithOAuth({
provider: "google",
options: {
redirectTo: `${process.env.SUPABASE_AUTH_URL}/api/auth/callback`,
queryParams: {
access_type: 'offline',
prompt: 'consent',
},
},
});
if (error) console.error('Googleログインエラー:', error.message)
if (!error && url) redirect(url);
}
// ---------------------------------------------
// Googleログアウト
// ---------------------------------------------
export async function signOut() {
// クライアントを作成
const supabase = await createClient();
const { error } = await supabase.auth.signOut();
if (error) console.error('Googleログアウトエラー:', error.message)
if (!error) return true;
return false;
}
【6】ログインページを作る
login/page.tsx
// login/page.tsx
"use client";
import React from "react";
import { signInWithGoogle, signOut } from "@/lib/supabase-auth/authGoogle";
import { useRouter } from "next/navigation";
export default function LoginPage() {
// Googleログイン
const handleGoogleLogin = async () => {
await signInWithGoogle();
}
// Googleログアウト
const router = useRouter();
const handleGoogleLogout = async () => {
const result = await signOut();
if (result) router.refresh();
}
return (
<div>
<button onClick={handleGoogleLogin}>ログイン</button>
<button onClick={handleGoogleLogout}>ログアウト</button>
</div>
)
}
【7】認証情報を判定/キャッシュする関数を作る
lib/supabase-auth/auth.ts
// lib/supabase-auth/auth.ts
import { cache } from 'react';
import { redirect } from 'next/navigation';
import { createClient } from './server';
// ---------------------------------------------
// 管理者として許可するメールアドレスのリスト
// ---------------------------------------------
export const adminUsers = [
"example@example.com",
];
// ---------------------------------------------
// 認証チェック
// ---------------------------------------------
const validateAuthWithRedirect = async () => {
/* 未認証であればredirect、 認証できればユーザー情報を返す */
// ユーザーを取得
const supabase = await createClient();
const { data: { user }, error } = await supabase.auth.getUser();
// 認証失敗: ユーザーが存在しない場合 / 権限がない場合
if (!user || !user.email || !adminUsers.includes(user.email)) {
redirect("/login");
};
return user;
};
// 認証チェックをキャッシュ
export const cachedValidateAuthWithRedirect = cache(validateAuthWithRedirect);
【8】「ミドルウェア」で認証情報を確認する
middleware.ts
// middleware.ts
import { type NextRequest } from 'next/server'
import { updateSession } from '@/lib/supabase-auth/middleware'
export async function middleware(request: NextRequest) {
return await updateSession(request)
}
export const config = {
matcher: [
// 以下のパスは除外されます:
// - api/auth/*
// - _next/static, _next/image, favicon.ico, 画像関連ファイル
'/((?!api/auth/.*|_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
],
}
lib/supabase-auth/middleware.ts
// lib/supabase-auth/middleware.ts
import { CookieOptions, createServerClient } from "@supabase/ssr";
import { NextResponse, type NextRequest } from "next/server";
// 1. クッキー情報からUserデータを取得する
// 2. 未ログインの場合(Userデータが存在しない場合)
export async function updateSession(request: NextRequest) {
// レスポンスを作成
let response = NextResponse.next({ request });
const pathname = request.nextUrl.pathname;
response.headers.set("x-current-path", pathname); // パス情報をヘッダーに設定
// ---------------------------------------------
// Userデータを取得
// ---------------------------------------------
// クライアントを作成
const supabase = createServerClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return request.cookies.get(name)?.value;
},
set(name: string, value: string, options: CookieOptions) {
// リクエストとレスポンスの両方にCookieを設定
request.cookies.set({
name,
value,
...options,
});
response = NextResponse.next({
request: {
headers: request.headers,
},
});
response.cookies.set({
name,
value,
...options,
});
},
remove(name: string, options: CookieOptions) {
// Cookieを削除
request.cookies.set({
name,
value: '',
...options,
});
response = NextResponse.next({
request: {
headers: request.headers,
},
});
response.cookies.set({
name,
value: '',
...options,
});
},
},
}
);
// ユーザーを取得
const { data: { user } } = await supabase.auth.getUser();
// 重要: createServerClient と supabase.auth.getUser() の間にロジックを
// 書かないでください。単純なミスでも、ユーザーがランダムにログアウトされる
// 問題のデバッグが非常に困難になる可能性があります。
// ---------------------------------------------
// 未ログインユーザーの場合: リダイレクト処理
// ---------------------------------------------
if (
!user &&
!pathname.startsWith('/login') &&
!pathname.startsWith('/auth')
) {
const url = request.nextUrl.clone();
url.pathname = '/login';
return NextResponse.redirect(url);
}
// ---------------------------------------------
// ログインユーザーの場合: レスポンスを返す
// ---------------------------------------------
response.headers.set("x-current-path", pathname);
return response;
}
【9】「ページ内」で認証情報を確認する
ログインページ/login
以外全てのページで認証情報を確認する場合の例です。
layout.tsx
// layout.tsx
import React from "react";
import "./globals.css";
import { headers } from "next/headers";
import { cachedValidateAuthWithRedirect } from "@/lib/supabase-auth/auth";
export default async function RootLayout({children,}: {children: React.ReactNode;}) {
// ---------------------------------------------
// リクエストURLを取得
// ---------------------------------------------
const headersList = headers();
const pathname = headersList.get("x-current-path");
const isLoginPage = pathname?.startsWith("/login");
// ---------------------------------------------
// 認証確認: ログインページ以外
// ---------------------------------------------
if (!isLoginPage) {
await cachedValidateAuthWithRedirect();
}
return (
<html lang="ja" className="h-full">
<body>
{isLoginPage
? <>{children}</>
: <main className="relative min-h-screen">
<MainLayout>
{children}
</MainLayout>
</main>
}
</body>
</html>
);
}
Discussion