1
0

Next.js で Supabase Auth がどんな仕組みで動いてるか調べました

Last updated at Posted at 2024-09-09

最近はいろんな開発者向けサービスが出てきてすごく便利ですよね。

VercelSupabase の存在もあり、簡易なアプリケーションであれば AWS すら使わずインフラを構築しなくて済む世界が来てとても楽です。

ただし、ブラックボックス化された結果 「なんかよく分からないけど、チュートリアルの通りにコード書いたら動いたぞ」 というのは非常に危険だなと考えており、Next.jsSupabase Auth を使った際にまさにその状況に陥りました。

ということで、本記事では Supabase AuthNext.js (SSR) でどの様に動作しているのか、仕組みを説明したいと思います。

Supabase Auth

Supabase Auth > Getting Started > Next.js チュートリアルに従って以下の npx コマンドを実行すると、Supabase を使って DB, Auth (認証) が構築されたサンプルアプリケーションが生成されます。

npx create-next-app -e with-supabase

ログイン画面.

image.png

コードを見ると 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 をどう導入するかは ↓ に書いてあるので説明しません。

https://supabase.com/docs/guides/auth/server-side/nextjs

図解

@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 を使ったコードに変更します。クライアントとサーバーサイドで異なるコードを使用します。

supabase/client.ts
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) によって異なるのを吸収できる作りになっているためです。

supabase/server.ts
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 する必要があります。

middleware.ts
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)$).*)",
  ],
};
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0