【AppRouter】Supabase のAuth Helpersを触ってみる

2023/09/10に公開

はじめに

Next.js の AppRouter と Supabase の Auth Helpers による認証機能の実装について気になったのでまとめようと思います。対象者はこれから AppRouter と Supabase を連携させて何か認証機能を搭載したアプリケーションを作ってみたい方です。

また、本記事は主に下記サイトの内容を参考に作成しております。
https://supabase.com/docs/guides/auth/auth-helpers/nextjs
https://egghead.io/courses/build-a-twitter-clone-with-the-next-js-app-router-and-supabase-19bebadb

Auth Helpers(認証ヘルパー)とは?

Auth Helpersとは、 Supabase の認証機能を操作するための便利な関数やユーティリティをまとめたものです。これを使うことで比較的簡単に認証機能を実装することができます。

Next.js の他に SvelteKit, Remix, Nuxt といった JS フレームワークをサポートしているようです。
https://github.com/supabase/auth-helpers

以下 Auth Helpers の例です。

  • signUp(ユーザを新規登録するための関数)
  • signIn(ユーザがログインするための関数、メールやパスワードでの認証をサポート)
  • signOut(ユーザをログアウトさせるための関数)
  • getSession(現在のユーザ情報を取得するための関数)

補足ですが、現在 Auth Helpers はベータ版となっています。

The Auth Helpers are in beta. They are usable in their current state, but it's likely that there will be breaking changes.

https://supabase.com/docs/guides/auth/auth-helpers

App Router の場合、どのように使う?

次に Auth Helpers を App Routerの環境 でどのように使えばよいのか見ていきます。

ドキュメントによれば、Auth Helpers はセッションを localStorage ではなく Cookie に保持する仕組みになっていることからCookie ベースの認証機能を提供していることが分かります。

また Next.js の AppRouter からはClient Components,Server Components, Server Actions, Route Handlers, Middlewareなどクライアントとサーバーの両方を意識した開発ができるようになっているため、このあたりも考慮した作りになっていそうですね。

The Next.js Auth Helpers package configures Supabase Auth to store the user's session in a cookie, rather than localStorage. This makes it available across the client and server of the App Router - Client Components, Server Components, Server Actions, Route Handlers and Middleware. The session is automatically sent along with any requests to Supabase.

https://supabase.com/docs/guides/auth/auth-helpers/nextjs

Supabase クライアント を作成する

実際にアプリ側から Supabase に接続するにはどうすればよいのでしょうか?
App Router の場合、Next.js Auth Helpers は、Supabase のクライアントにアクセスする 5 つの関数を提供しています。

  1. createClientComponentClient
    • クライアントコンポーネント側から使用するメソッド
  2. createServerComponentClient
    • サーバーコンポーネント側から使用するメソッド
  3. createServerActionClient
    • サーバーアクション で使用するメソッド
  4. createRouteHandlerClient
    • ルートハンドラー で使用するメソッド
  5. createMiddlewareClient
    • ミドルウェアで使用するメソッド

create〇〇〇Client の形式は共通で、アプリ内で呼び出したい場所に応じて〇〇〇の部分を変更する書き方となっています。
まさに App Router の環境に適応した命名になっているため、分かりやすいですね!

(https://supabase.com/docs/guides/auth/auth-helpers/nextjs#creating-a-supabase-client)

それでは、実際に簡単なプロジェクトを作成しながら、auth-helpers の使い方を見ていきましょう。

環境構築

npx create-next-app -e with-supabase コマンドを使えば、Auth Helpers による、cookie ベースの認証が設定されたテンプレを生成できますが、今回はマニュアルでセットアップします。
ただし、Supabase のプロジェクトは作成できている前提です。

Next.js のプロジェクト作成

terminal
npx create-next-app@latest

Next.js Auth Helpers のパッケージのインストール

terminal
npm i @supabase/auth-helpers-nextjs @supabase/supabase-js

https://github.com/supabase/auth-helpers

環境変数の設定

Supabase プロジェクトに関する、環境変数を.env.localに設定します。

.env.local
NEXT_PUBLIC_SUPABASE_URL=<プロジェクトURL>
NEXT_PUBLIC_SUPABASE_ANON_KEY=<プロジェクトAPIの非公開キー>

ダッシュボードより[Project Setting] > [API]で上記の環境変数を確認することが出来るので、
それらをコピぺしましょう。

テーブル作成とデータ準備

Supabase の SQL Editor で下記のクエリを実行し、テーブルとデータを作成します。

posts テーブルの作成

create table if not exists posts (
  id uuid default gen_random_uuid() primary key,
  created_at timestamp with time zone default timezone('utc'::text, now()) not null,
  title text not null
);

RLS ポリシー の設定

  • RLSの有効化
  • select によるデータ取得を行えるようにポリシーを作成する
alter table posts
  enable row level security;

create policy "anyone can select posts" ON "public"."posts"
as permissive for select
to public
using (true);

データを posts テーブルに挿入

insert into
  posts(title)
values
  ('first post'),
  ('second post'),
  ('third post')

ここまでで下準備は整ったのでいよいよ auth-helpers での実装の方に移っていきましょう。

実装

下記リポジトリが実装したコードになります。
https://github.com/otaki0413/next-app-with-auth-helpers

1. サーバーコンポーネント上でデータ取得する

まず、Supabase のpostsテーブルからデータを取得しapp/page.tsx で表示します。

今回はサーバーコンポーネント側でデータ取得を行うのでcreateServerComponentClient 関数を用いてクライアントを作成します。

app/page.tsx
import { createServerComponentClient } from "@supabase/auth-helpers-nextjs";
import { cookies } from "next/headers";

export default async function Home() {
  // クライアント作成
  const supabase = createServerComponentClient({ cookies });
  // postsテーブルからデータ取得
  const { data: posts } = await supabase.from("posts").select();

  return <pre>{JSON.stringify(posts, null, 2)}</pre>;
}

下記のようにデータが取得できていれば OK です!

2. Github OAuth でユーザー認証できるようにする

今回は、Github によるソーシャルログインを実装します。

Github OAuth の設定

実際に Github OAuth を有効にするには、GitHub OAuth アプリケーションをセットアップして、アプリケーションの認証情報を Supabase ダッシュボードに追加する必要があります。下記サイトが参考になるかと思います。
(https://supabase.com/docs/guides/auth/social-login/auth-github)
(https://egghead.io/lessons/supabase-create-an-oauth-app-with-github)

ここではAuthButton.tsxを作成して、ログインボタンとログアウトボタンを作っています。

今回はボタン押下時に認証処理を行いたいので、use clientを冒頭に記載します。
つまり、クライアントコンポーネントになるのでcreateClientComponentClient関数を用いて Supabase クライアントを作成します。

サインイン処理については、SignInWithOAuth 関数で Github 認証を実装しており、ユーザーがサインインに成功すると、http://localhost:3000/auth/callbackにリダイレクトする想定です。

AuthButton.tsx
"use client";

import { createClientComponentClient } from "@supabase/auth-helpers-nextjs";

export const AuthButton = () => {
  // Supabaseクライアント作成
  const supabase = createClientComponentClient();

  // サインイン処理
  const handleSignIn = async () => {
    // GitHub OAuthで認証する
    await supabase.auth.signInWithOAuth({
      provider: "github",
      options: {
        redirectTo: "http://localhost:3000/auth/callback",
      },
    });
  };
  // サインアウト処理
  const handleSignOut = async () => {
    await supabase.auth.signOut();
  };

  return (
    <>
      <button onClick={handleSignIn}>Login</button>
      <button onClick={handleSignOut}>Logout</button>
    </>
  );
};

作成した AuthButtonコンポーネントを app/page.tsxでインポートします。

app/page.tsx
import { createServerComponentClient } from "@supabase/auth-helpers-nextjs";
import { cookies } from "next/headers";
+ import { AuthButton } from "./_components/AuthButton";

export default async function Home() {
  const supabase = createServerComponentClient({ cookies });
  const { data: posts } = await supabase.from("posts").select();

  return (
    <>
+     <AuthButton />
      <pre>{JSON.stringify(posts, null, 2)}</pre>;
    </>
  );
}

3. Route Handlers の作成

サインインに成功すると、http://localhost:3000/auth/callback にリダイレクトするので、Router Handler を作成する必要があります。

今回は、app ディレクトリ配下にauth/callback/route.tsを作成します。
このルートで実行しているのは、Code Exchange(コード交換) です。

まず、リクエスト URL からクエリパラメータcodeを指定して、認証コードを取得します。
認証コード が存在する場合に、createRouteHandlerClient関数でクライアントを作成し、
exchangeCodeForSession関数で アプリ側と Supabase との間でセッションを確立しています。

exchangeCodeForSession

文字だけみると「認証コードをセッションに交換する」を指しますが、
実際は「認証コードを使用してセッションを確立する」が正確です。

app/auth/callback/route.ts
import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs";
import { cookies } from "next/headers";
import { NextRequest, NextResponse } from "next/server";

// Code Exchange用のルートハンドラ
export async function GET(request: NextRequest) {
  const requestUrl = new URL(request.url);
  const code = requestUrl.searchParams.get("code");

  // 認証コードを使用して、Supabaseとのセッションを確立する
  if (code) {
    const supabase = createRouteHandlerClient({ cookies });
    await supabase.auth.exchangeCodeForSession(code);
  }

  // サインイン後にリダイレクトするURLを指定
  return NextResponse.redirect(requestUrl.origin);
}

Github OAuth に成功すると、Supabase の[authentication] > [Users]のところに認証されたユーザーの情報が表示されるかと思います。

ここまでの内容をまとめると、GitHub 認証を通じてセッションが確立され、ユーザの認証とアプリケーションと Supabase の間でセキュアに通信が行えるになりました。セッション情報には認証情報が管理されるため、今後 Supabase へアクセスする際にはセッション情報が使用されます。

https://nextjs.org/docs/app/building-your-application/routing/route-handlers#cookies

4. ミドルウェアの実装

ここまでの実装で 1 つ問題があるためそれを解決しようと思います。

問題点

Cookie の有効期限が切れると、ユーザがページを更新するときに Cookie が削除されてログアウト状態になる

これはサーバーコンポーネントが、Cookie を読みとり可能だが、更新する方法は持っていないためです。

Next.js Server Components allow you to read a cookie but not write back to it. Middleware on the other hand allow you to both read and write to cookies.

serverComponentClient.tsの実装を見ても、現状サーバーコンポーネント側からだとcookiesを設定できない旨がコメントで記載されています。
https://github.com/supabase/auth-helpers/blob/main/packages/nextjs/src/serverComponentClient.ts

解決策

Middleware 関数を作成する

今回はミドルウェア関数で解決しようと思います。middleware.tsをプロジェクトのルート直下に配置することで、ルートが読み込まれる直前に何かしらの処理を実行することができます。

今回の場合だと、サーバーコンポーネントが読み込まれて、Supabase からデータ取得する時点までにセッションを有効化させておきたいのです!

そこで getSession 関数を実行して、有効期限が切れたセッションを更新させます。

middleware.ts
import { createMiddlewareClient } from "@supabase/auth-helpers-nextjs";
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

// ルートが読み込まれる直前に実行
export async function middleware(req: NextRequest) {
  const res = NextResponse.next();
  // クライアント作成
  const supabase = createMiddlewareClient({ req, res });
  // 有効期限が切れたセッションを更新
  await supabase.auth.getSession();
  return res;
}

この middleware 関数は、サーバーコンポーネントのルートをロードするレスポンスを返します。これにより下記サーバーコンポーネント(app/page.tsx)の cookies 関数には新しいセッションを含む更新後の Cookie が含まれることが保証されるのです。つまり、ユーザーがログアウトするのは、ログアウトボタン押下時のみです。

app/page.tsx
import { createServerComponentClient } from "@supabase/auth-helpers-nextjs";
import { cookies } from "next/headers";
import { AuthButton } from "./_components/AuthButton";

export default async function Home() {
  // 更新後のsessionを含んだCookieがこの時点では入っている
  const supabase = createServerComponentClient({ cookies });
  const { data: posts } = await supabase.from("posts").select();

  return (
    <>
      <AuthButton />
      <pre>{JSON.stringify(posts, null, 2)}</pre>;
    </>
  );
}

https://nextjs.org/docs/app/building-your-application/routing/middleware

5. 認証されたユーザーのみがアクセスできるようにする

現在、Supabase の posts テーブルには誰でもアクセスできるようになっているので、
このタイミングでアクセス制限をかけておきます。

Supabase の RLS ポリシーをpublicからauthenticatedに変更するだけです。

RLS ポリシーの変更

create policy "authenticated user can select posts" ON "public"."posts"
as permissive for select
- to public
+ to authenticated
using (true);

6. Session 有無に応じて UI を動的レンダリングする

最後に、ログイン・ログアウト両方のボタンが表示されているので、Session に応じて切り替えられるようにします。

前提ですが、App Router の環境ではクライアントコンポーネントの初回レンダリングは、サーバー上で実行されます。これをサーバーサイドレンダリング(SSR)と呼びます。

まず、セッションを非同期で取得する用のサーバーコンポーネントを作成します。
非同期処理を実行する場合、関数の先頭にasyncをつけると非同期サーバーコンポーネントとして機能します。コンポーネント名はとりあえず分かりやすく AuthButtonServer.tsx という命名にしておきます。

実装は以下のようになります。

サーバーコンポーネント側(AuthButtonServer.tsx)

  • 非同期処理を使用するので、asyncを付ける
  • getSessionでセッション情報を取得し、クライアントコンポーネント(AuthButton.tsx)へ Props として渡す
AuthButtonServer.tsx
import { createServerComponentClient } from "@supabase/auth-helpers-nextjs";
import { cookies } from "next/headers";
import { AuthButton }from "./AuthButton";

export default async function AuthButtonServer() {
  const supabase = createServerComponentClient({ cookies });

  const {
   data: { session },
  } = await supabase.auth.getSession();

 return <AuthButton session={session} />;
}

クライアントコンポーネント側(AuthButton.tsx)

  • 親から渡ってきた Props で sessionを受け取る
  • session の有無に応じて、ログイン・ログアウトボタンの表示を切り替える
AuthButton.tsx
"use client";

import { useRouter } from "next/navigation";
import {
  Session,
  createClientComponentClient,
} from "@supabase/auth-helpers-nextjs";

+ export const AuthButton = ({ session }: { session: Session | null }) => {
  const supabase = createClientComponentClient();
  const router = useRouter();

  // サインアウト処理
  const handleSignOut = async () => {
    await supabase.auth.signOut();
    router.refresh();
  };

  // サインイン処理
  const handleSignIn = async () => {
    await supabase.auth.signInWithOAuth({
      provider: "github",
      options: {
        redirectTo: "http://localhost:3000/auth/callback",
      },
    });
  };

+ return session ? (
+   // Sessionがあるかどうかで認証ボタンを切り替える
+   <button onClick={handleSignOut}>Logout</button>
+ ) : (
+   <button onClick={handleSignIn}>Login</button>
+ );
 }

https://nextjs.org/docs/app/building-your-application/rendering

おわりに

今回 Supabase の Auth Helpers を AppRouter の環境で使って、簡単な認証機能を実装しました。Supabase を使えばデータベースや認証機能を簡単に実装できるので、非常に便利なツールだなど思いました。しかし、筆者自身、認証周りについてまだまだ理解が浅く Cookie や Session が何をしているか曖昧な部分はあります。そのため、今後の課題として認証周りをもっと勉強したいと思います!

また、Supabase は今非常に勢いのあるサービスなので、いろんなアプリを作りながら筆者自身ももっと知見を深めていきたいなと思っています。もし興味のある方はぜひ触ってみて下さい。

ここまで読んで頂きありがとうございました!!

参考サイト

https://supabase.com/docs/guides/auth/auth-helpers/nextjs
https://egghead.io/courses/build-a-twitter-clone-with-the-next-js-app-router-and-supabase-19bebadb
https://nextjs.org/

GitHubで編集を提案

Discussion