🌟

SupabaseのNotion認証でNotion APIのアクセストークン取得に苦戦した話

2024/03/26に公開

こんにちは。今回は、SvelteKitプロジェクトでSupabase Authを用いてNotion認証を実装した際に、なかなかNotionのAPIアクセストークンが取得できず苦労した経験について共有します。

同じような問題で悩んでいる方の助けになれば幸いです。私の経験が、みなさんの開発の一助となることを願っています。

使用技術

  • SvelteKit
  • Supabase Auth

ディレクトリ構成

  • src/routes/login
  • src/routes/auth/callback

やったこと

まず、+page.svelteでNotionログインのフォームアクションを記述しました。

src/routes/login/+page.svelte
<script lang="ts">
import { enhance } from "$app/forms";
</script>

<form method="POST" action="?/notion" use:enhance>
  <button type="submit">Notionログイン</button>
</form>

次に、+page.server.tssignInWithOAuthメソッドを呼び出し、リダイレクトURLを指定しました。

src/routes/login/+page.server.ts
export const actions = {
    notion: async ({ url, locals: { supabase } }) => {
        const { data, error } = await supabase.auth.signInWithOAuth({
            provider: "notion",
            options: { redirectTo: `${url.origin}/auth/callback` },
        });

        console.log({ data });
        if (data.url) {
            redirect(303, data.url);
        }

        if (error) {
            return fail(500, { message: "Server error. Try again later.", success: false, data });
        }

        return {
            message: "NotionのOAuth認証を完了してください",
            success: true,
        };
    },
};

signInWithOAuthredirectToにパスを指定すると、searchParamscodeが付与されてauth/callbackにリダイレクトされます。

https://localhost:5173/auth/callback?code=e202e8c9-0990-40af-855f-ff8f872b1ec6

auth/callbackでは、リダイレクト時にsearchParamsからcodeを受け取り、exchangeCodeForSessionでセッションに保存します。

src/routes/auth/callback/+server.ts
import { redirect } from "@sveltejs/kit";

export const GET = async (event) => {
    const {
        url,
        locals: { supabase },
    } = event;
    const code = url.searchParams.get("code") as string;

    if (code) {
        const { error } = await supabase.auth.exchangeCodeForSession(code);
        if (!error) {
            redirect(303, "/");
        }
    }
    // return the user to an error page with instructions
    throw redirect(303, "/auth/auth-code-error");
};

認証自体は実装できたので、次はNotionの公式ドキュメントを参考にNotionのAPIアクセストークンの取得を試みました。

src/routes/auth/callback/+server.ts

import { redirect } from "@sveltejs/kit";
const clientId = process.env.OAUTH_CLIENT_ID;
const clientSecret = process.env.OAUTH_CLIENT_SECRET;
const redirectUri = process.env.OAUTH_REDIRECT_URI;

export const GET = async (event) => {
	const {
		url,
		locals: { supabase },
	} = event;
	const code = url.searchParams.get("code") as string;

	if (code) {
		const error } = await supabase.auth.exchangeCodeForSession(code);

+   // encode in base 64
+   const encoded = Buffer.from(`${clientId}:${clientSecret}`).toString("base64");
+   const response = await fetch("https://api.notion.com/v1/oauth/token", {
+		  method: "POST",
+		  headers: {
+		  Accept: "application/json",
+		  "Content-Type": "application/json",
+		  Authorization: `Basic ${encoded}`,
+	  },
+		  body: JSON.stringify({
+			  grant_type: "authorization_code",
+			  code: code, // ここでsearchParamsのコードを渡す
+			  redirect_uri: redirectUri,
+		  }),
+	  });

		if (!error) {
			redirect(303, "/");
		}
	}
	// return the user to an error page with instructions
	throw redirect(303, "/auth/auth-code-error");
};

しかし、何度試しても以下のようなエラーが発生しました。

{
  "error": "invalid_request",
  "error_description": "Invalid code",
  "request_id": "********-****-****-****-************"
}

調査を進めていくと、auth/callbackで受け取っているcodePKCEフローで生成されたcodeであり、Notionから返されるcodeとは別物だということがわかりました。

codeが別物とわかったのでNotion APIのアクセストークン取得方法について探っていたところ、以下の記事に遭遇。

https://community.weweb.io/t/notion-integration/1662

なんと、SupabaseのPKCEフローで検証が成功した場合、セッション内のprovider_tokenにNotionのアクセストークンが格納されているとのこと!

つまり、getSession()で普通に取得できるみたいです。

export const load= async ({ locals: { getSession } }) => {
  const session = await getSession();
  if(session){
    const notionAccessToken = session.provider_token
    console.log(notionAccessToken);
  }
  return { session };
};

Notion のトークンエンドポイントに送信しなくても既にセッションに入っていたようです。

追記

Supabaseで取得できるprovider_tokenOAuth呼び出し直後にのみ存在しセッションが更新されると消えてしまいます。
もしこのトークンを使用する必要がある場合は、自分の責任で保存しておく必要があります。

https://github.com/orgs/supabase/discussions/18399

感想

Notionのトークンエンドポイントに一時コードを渡さなくてもアクセストークンが取得できるのは便利(?)だと思います。

ただ、この取得方法は少し暗黙的すぎる気がします。
明示的にNotionのAPIアクセストークンを取得するための手順が提供されていれば、ここまで悩まなかったのに、、、

Supabase公式ドキュメントを読んでも、provider_tokenの詳細な仕様について見つけることができませんでした。

個人的には、SupabaseのLogin with Notionのページにこの情報が記載されていれば、もっとスムーズに実装できたのではないかと感じています。
https://supabase.com/docs/guides/auth/social-login/auth-notion

今回の経験を通して、OAuth認証の仕組み、特にPKCEフローについて理解が深まったことは大きな収穫でした。

以上、Supabase × Notionの認証実装で得た学びを共有させていただきました。この記事が誰かのお役に立てることを願っています。

MOXT Tech Blog

Discussion