キッサ カタダ

テクノロジー

Remix + Supabase AuthでGoogleログインを実装する方法

2024-09-02T16:26:51.049Zに公開
2024-09-08T13:02:17.415Z
#Remix
#Supabase
<h2 id="h8d027c8ed3">はじめに</h2><p>Supabase熱いですね〜!最近では国内でもプロダクトにSupabaseを採用した事例も聞くようになってきて、いよいよ国内でもSupabaseが本格的に流行りそうです。</p><p>SupabaseとVercelとのパートナーシップも始まり、それ即ちNext.jsとSupabaseを使ったプロダクト開発がますますしやすくなるということになります…!</p><p>そんな中でも個人的にはNext.jsよりもRemixを使って開発がしたい気持ちが抑えられません…!(逆張り精神とかではありません)</p><p>ただ、Supabase公式ドキュメントにはNext.jsのサンプルはあれどRemixはなかったり、Cloudflareランタイムで動かすとなるとクライアントの作り方も工夫しなければなりません。</p><p>そんな中、Remix製のアプリケーションとSupabase Authを組み合わせてGoogleログインを実装したので、知見の共有も兼ねて記事にしました!</p><p>実際にGoogleログインを実装したプロダクトは、開発中の「ReserveEase」というプロダクトです!よかったら覗いてみてください。</p><p><a href="https://www.reserve-ease.com/">https://www.reserve-ease.com/</a></p><h2 id="ha89e0e0a70">Remix + Supabase AuthでGoogleログインを実装する方法</h2><p>ざっくり手順は以下の通りです。</p><ol><li>Google CloudのダッシュボードからOAuthの認証情報を作成する</li><li>SupabaseのダッシュボードからGoogleログインを有効化する</li><li>Remixの実装</li></ol><p>1と2の手順はすでに記事を書いてくださっている先人がいらっしゃるので、そちらをご覧ください。</p><p><a href="https://supabase.com/docs/guides/auth/social-login/auth-google?queryGroups=environment&amp;environment=server&amp;queryGroups=framework&amp;framework=remix">https://supabase.com/docs/guides/auth/social-login/auth-google?queryGroups=environment&amp;environment=server&amp;queryGroups=framework&amp;framework=remix</a></p><p><a href="https://qiita.com/kaho_eng/items/a37ff001ea9eae226183">https://qiita.com/kaho_eng/items/a37ff001ea9eae226183</a></p><p><a href="https://note.com/libproc/n/n7d417158843d">https://note.com/libproc/n/n7d417158843d</a></p><p>こういうGUI上の手順をスクショと一緒にわかりやすく説明するの、めんどくさいのにすごい…ありがたい…。</p><p>それでは本題の、手順3. Remixの実装を解説していきます!</p><h3 id="hef7634fef5">必要なパッケージをインストール</h3><div data-filename="fish"><pre><code class="language-shell">$ bun install @supabase/supabase-js @supabase/ssr</code></pre></div><p><code>@supabase/supabase-js</code>はSupabaseをJSアプリケーションから使えるようにするためのSDKパッケージです。</p><p><code>@supabase/ssr</code>は、SSR(サーバーサイドレンダリング)するフレームワークに合わせて実装されたSDKパッケージで、RemixやNext.jsでは基本的にこちらのパッケージを使用します。</p><p>クライアントサイドで実行されるコードの場合は<code>@supabase/supabase-js</code>を使用して、サーバーサイドで実行されるコードの場合は<code>@supabase/ssr</code>を使うようなイメージです。</p><h3 id="h733b8a428b">サーバーサイド用のSupabaseクライアントを作成して共通化</h3><p>今回はRemixでSupabase Authを使っていくので、@supabase/ssrを使ってサーバーサイドで使えるSupabaseクライアントを生成します。</p><p>ちなみに@supabase/supabase-jsでクライアントを生成してしまうと、Cookieにアクセスできないためセッションを保存できなかったり、そもそもSupabase Authの挙動が変わったりするので注意が必要です。</p><p>下記コードはCloudflareランタイム上で動作することを想定して実装しています。</p><div data-filename="app/lib/supabase.server.ts"><pre><code class="language-typescript">import { AppLoadContext } from &apos;@remix-run/cloudflare&apos; import { createServerClient } from &apos;@supabase/ssr&apos; export const createSupabaseClient = ( request: Request, context: AppLoadContext, ) =&gt; { const headers = new Headers() const supabase = createServerClient( context.cloudflare.env.SUPABASE_URL!, context.cloudflare.env.SUPABASE_ANON_KEY!, { cookies: { get: (key) =&gt; { return request.headers .get(&apos;Cookie&apos;) ?.split(&apos;;&apos;) .find((c) =&gt; c.trim().startsWith(`${key}=`)) ?.split(&apos;=&apos;)[1] }, set: (key, value, options) =&gt; { headers.append( &apos;Set-Cookie&apos;, `${key}=${value}${ options ? `; ${Object.entries(options) .map(([k, v]) =&gt; `${k}=${v}`) .join(&apos;; &apos;)}` : &apos;&apos; }`, ) }, remove: (key, options) =&gt; { headers.append( &apos;Set-Cookie&apos;, `${key}=; Max-Age=0${ options ? `; ${Object.entries(options) .map(([k, v]) =&gt; `${k}=${v}`) .join(&apos;; &apos;)}` : &apos;&apos; }`, ) }, }, }, ) return { supabase, headers } }</code></pre></div><h3 id="h1dafb69979">Googleでログインボタンを作成して、Googleログイン画面へのリダイレクト処理を実装</h3><p>Googleでログインボタンを作る際に、Googleがボタンのスタイルについてガイドラインを設定しているため必ず確認してください。</p><p><a href="https://developers.google.com/identity/branding-guidelines?hl=ja">https://developers.google.com/identity/branding-guidelines?hl=ja</a></p><p>公式サイトからロゴのSVGを拾ってきて、私の場合は下記のように実装しました。実物は「はじめに」の項でリンクを貼った開発中のプロダクトに飛んで確認してみてください!</p><p>ちなみにshadcn/uiを使用しています。</p><div data-filename="app/components/google-login-form.tsx"><pre><code class="language-typescript">export const action: ActionFunction = async ({ request, context, }: ActionFunctionArgs) =&gt; { const { supabase, headers } = createSupabaseClient(request, context) const { data, error } = await supabase.auth.signInWithOAuth({ provider: &apos;google&apos;, options: { redirectTo: `${context.cloudflare.env.BASE_URL}/auth/google/callback`, }, }) if (error) { console.error(`ERROR: ${error.message}`) return new Response(null, { status: 400, headers }) } return redirect(data.url ?? &apos;/&apos;, { headers }) } export default function GoogleLoginForm() { &lt;Form method=&apos;post&apos;&gt; &lt;Button type=&apos;submit&apos; className=&apos;w-full&apos;&gt; &lt;img src=&apos;/assets/google.svg&apos; alt=&apos;google logo&apos; className=&apos;mr-2&apos; /&gt; Googleでログイン &lt;/Button&gt; &lt;/Form&gt; }</code></pre></div><h3 id="hb0df7855a0">AuthCallbackを実装して、Googleから受け取った認証コードをSupabaseに渡して認証する</h3><p>上記のフォームからaction関数が実行されると、Googleログインの画面に遷移します。</p><p>ユーザーがログインに使用するGoogleアカウントを選択すると、<code>await supabase.auth.signInWithOAuth</code>のredirectToオプションに渡したURLにリダイレクトしてきます。</p><p>今回はリダイレクトしてきた時に実行するコールバック処理を実装します。</p><div data-filename="app/routes/auth.google.callback.tsx"><pre><code class="language-typescript">import { LoaderFunctionArgs, redirect } from &apos;@remix-run/cloudflare&apos; import { createSupabaseClient } from &apos;~/lib/supabase.server&apos; export async function loader({ request, context }: LoaderFunctionArgs) { const { supabase, headers } = createSupabaseClient(request, context) const requestUrl = new URL(request.url) const code = requestUrl.searchParams.get(&apos;code&apos;) if (code) { const { error } = await supabase.auth.exchangeCodeForSession(code) if (!error) { return redirect(&apos;/dashboard&apos;, { headers }) } console.error(&apos;Error exchanging code for session:&apos;, error) } return new Response(null, { status: 400, headers }) }</code></pre></div><h3 id="h010fa3b198">Supabaseから渡されたアクセストークンをCookieに保存する</h3><p>上記のloader関数で実行している<code>exchangeCodeForSession</code>の処理が成功するとCookieにアクセストークンを保存してくれます。</p><p>これにより、セッションを保存してログイン状態を保持することができるのです。</p><h2 id="he105013b11">Remixを使った場合のハマりポイント</h2><p>まずは大前提として、Supabaseが用意している公式ドキュメントとレポジトリサンプルはNext.jsとSvelteKitしか存在しないため、かなり手探りで実装していくことになります。</p><p>以下は基本的にNext.js向けのドキュメントを参考にしつつ、実際にハマった部分とその対処法です。</p><h3 id="h39b75fb329">AuthApiError: invalid request: both auth code and code verifier should be non-empty</h3><p>Authコールバック内で<code>exchangeCodeForSession</code>メソッドを呼ぶとき、Cookieにcode verifierが保存されていない、もしくは読み取りができないとこのエラーを吐きます。</p><p>下記の公式ドキュメントを参考にしてSupabaseクライアントを実装すると、正しくCookieに読み取り、書き取りができない状態になってしまいます。</p><div data-filename="app/lib/supabase.server.ts"><pre><code class="language-typescript">// Supabase公式ドキュメントに沿って実装した、code verifierを操作できない例 cookies: { getAll() { return parseCookieHeader(request.headers.get(&apos;Cookie&apos;) ?? &apos;&apos;) }, setAll(cookiesToSet) { cookiesToSet.forEach(({ name, value, options }) =&gt; headers.append(&apos;Set-Cookie&apos;, serializeCookieHeader(name, value, options)) ) }, } // 正しくCookieオブジェクトを操作できる例 cookies: { get: (key) =&gt; { return request.headers .get(&apos;Cookie&apos;) ?.split(&apos;;&apos;) .find((c) =&gt; c.trim().startsWith(`${key}=`)) ?.split(&apos;=&apos;)[1] }, set: (key, value, options) =&gt; { headers.append( &apos;Set-Cookie&apos;, `${key}=${value}${ options ? `; ${Object.entries(options) .map(([k, v]) =&gt; `${k}=${v}`) .join(&apos;; &apos;)}` : &apos;&apos; }`, ) }, remove: (key, options) =&gt; { headers.append( &apos;Set-Cookie&apos;, `${key}=; Max-Age=0${ options ? `; ${Object.entries(options) .map(([k, v]) =&gt; `${k}=${v}`) .join(&apos;; &apos;)}` : &apos;&apos; }`, ) }, }</code></pre></div><p>GitHubにもissueが上がっていて、issue自体は結論が出ているわけでなく、Supabase開発チームがSSRフレームワーク向けに用意したガイドを読んで一旦頑張って、的なニュアンスで議論は止まっていました。</p><p>念の為、こちらのissueは<code>@supabase/ssr</code>パッケージではなく、前身の<code>@supabase/auth-helpers</code>パッケージであることに注意が必要です。</p><p><a href="https://github.com/supabase/auth-helpers/issues/545">https://github.com/supabase/auth-helpers/issues/545</a></p><h3 id="h8c7273738b">クライアント用のSupabaseクライアントでリダイレクトさせると色々面倒になる</h3><p>今回はサーバーサイド用のSupabaseクライアントを作成して、認証処理はすべてサーバーサイドで実行するように実装しました。</p><p>仮にクライアントのSupabaseクライアントからリダイレクトすると、アプリケーションにリダイレクトしてきた時のパスにハッシュパラメータが付与された状態でリダイレクトされてきます。</p><p>ハッシュパラメータはサーバーサイドで処理ができず。クライアントサイドでしか処理できないので、コールバック処理もクライアントサイドに寄せる必要があり、コードの記述量も増えるのであんまりいいことがありません。</p><p>ここはおとなしく、<code>@supabase/ssr</code>パッケージを使用してサーバーサイドで処理した方がシンプルに書けますし、セキュアな情報を扱うためサーバーサイドで実行した方が安心です。</p><h2 id="h9be0c3393d">おわりに</h2><p>Remixを使うことで、Web標準のAPIを意識した実装ができるのでとても嬉しいんですが、Next.jsと比べると公式ドキュメントやサンプルが少なく、ネットに落ちてる情報もかなり少ないため苦労するなと感じました。</p><p>それでも筋トレになると信じてしばらくはRemix使って開発進めていこうと思います!</p><p>皆さんも良いRemix + Supabaseライフを〜。</p>
RK

カタダ リョウタ

キッサカタダマスター兼フロントエンド寄りのソフトウェアエンジニア。

多趣味に生きてます。

RK

カタダ リョウタ

キッサカタダマスター兼フロントエンド寄りのソフトウェアエンジニア。

多趣味に生きてます。

目次