最近はいろんな開発者向けサービスが出てきてすごく便利ですよね。
Vercel や Supabase の存在もあり、簡易なアプリケーションであれば AWS すら使わずインフラを構築しなくて済む世界が来てとても楽です。
ただし、ブラックボックス化された結果 「なんかよく分からないけど、チュートリアルの通りにコード書いたら動いたぞ」 というのは非常に危険だなと考えており、Next.js で Supabase Auth を使った際にまさにその状況に陥りました。
ということで、本記事では Supabase Auth が Next.js (SSR) でどの様に動作しているのか、仕組みを説明したいと思います。
Supabase Auth
Supabase Auth > Getting Started > Next.js チュートリアルに従って以下の npx コマンドを実行すると、Supabase を使って DB, Auth (認証) が構築されたサンプルアプリケーションが生成されます。
npx create-next-app -e with-supabase
ログイン画面.
コードを見ると supabase-js (SDK) を使って、たったこれだけでログインやユーザー情報の取得 (認証の有無) ができます。凄いですね。
// サインアップ
await supabase.auth.signUp({ email, password, options: { emailRedirectTo: `${origin}/auth/callback` }});
// パスワードログイン
await supabase.auth.signInWithPassword({ email, password });
// ユーザー情報の取得 (認証の有無の確認)
const user = await supabase.auth.getUser();
何が疑問か?
前述の supabase-js (SDK) を使った認証セッション情報は標準では Local storage に保存されます。つまり、 Next.js の SSR や Route Handler では認証セッション情報は参照できない筈 です。
実は Supabase/Next.js チュートリアルのアプリケーションではこの問題を解決するために @supabase/ssr Package を使用しています。
ここで仕組み
@supabase/ssr Package をどう導入するかは ↓ に書いてあるので説明しません。
図解
@supabase/ssr Package を使うと、下図の通り Cookie を用いてクライアント・サーバーサイド間の認証セッション情報を伝送する様になります。
詳細
コード上は、supabase client の生成コードだけを @supabase/supabase-js
のものから @supabase/ssr
へ変更します。それ以外のコードには変更は必要ありません。
supabase client
- import { createClient } from '@supabase/supabase-js'
- // Create a single supabase client for interacting with your database
- const supabase = createClient(
- process.env.NEXT_PUBLIC_SUPABASE_URL!,
- process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
- )
これを @supabase/ssr を使ったコードに変更します。クライアントとサーバーサイドで異なるコードを使用します。
import { createBrowserClient } from '@supabase/ssr'
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
)
}
// const supabase = createClient()
サーバーサイドはこうなります。
クライアントコードに比べ若干複雑なのは、サーバーサイドでの Cookie に関する処理が利用しているフレームワーク(※今回は Next.js) によって異なるのを吸収できる作りになっているためです。
import { createServerClient, type CookieOptions } from '@supabase/ssr'
import { cookies } from 'next/headers'
export function createClient() {
const cookieStore = cookies()
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
// ↓これは Next.js 特有のコード.
return cookieStore.getAll()
},
setAll(cookiesToSet) {
// ↓これは Next.js 特有のコード.
try {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
)
} catch {
// The `setAll` method was called from a Server Component.
// This can be ignored if you have middleware refreshing
// user sessions.
}
},
},
}
)
}
// const supabase = createClient()
getAll()
, setAll()
の実装を開発者が利用する SSR フレームワークに併せて任意に変更できるようになっています。
middleware
Next.js の middleware.ts
でも同様の実装を行います。
というのも、Server Component では Cookie の書き込みは出来ないので middleware で Set-Cookie
ヘッダを発行することになります。
OAuth の場合、認証トークンは頻繁に更新 (refresh) されますので、これをクライアントに伝える意図で Set-Cookie
する必要があります。
import { createServerClient } from "@supabase/ssr";
import { type NextRequest, NextResponse } from "next/server";
export async function middleware(request: NextRequest) {
// Create an unmodified response
let response = NextResponse.next({
request: {
headers: request.headers,
},
});
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
// ↓middleware では cookie 参照の API が違うので先ほどのコードは使えない.
return request.cookies.getAll();
},
setAll(cookiesToSet) {
// ↓middleware では cookie 参照の API が違うので先ほどのコードは使えない.
cookiesToSet.forEach(({ name, value }) =>
request.cookies.set(name, value),
);
response = NextResponse.next({
request,
});
cookiesToSet.forEach(({ name, value, options }) =>
response.cookies.set(name, value, options),
);
},
},
},
);
// This will refresh session if expired - required for Server Components
// https://supabase.com/docs/guides/auth/server-side/nextjs
const user = await supabase.auth.getUser();
// Redirect to /sign-in page.
if (user.error) {
return NextResponse.redirect(new URL("/sign-in", request.url));
}
return response;
}
export const config = {
matcher: [
"/((?!_next/static|_next/image|favicon.ico|sign-in|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
],
};