SupabaseのNotion認証でNotion APIのアクセストークン取得に苦戦した話
こんにちは。今回は、SvelteKitプロジェクトでSupabase Authを用いてNotion認証を実装した際に、なかなかNotionのAPIアクセストークンが取得できず苦労した経験について共有します。
同じような問題で悩んでいる方の助けになれば幸いです。私の経験が、みなさんの開発の一助となることを願っています。
使用技術
- SvelteKit
- Supabase Auth
ディレクトリ構成
- src/routes/login
- src/routes/auth/callback
やったこと
まず、+page.svelte
でNotionログインのフォームアクションを記述しました。
<script lang="ts">
import { enhance } from "$app/forms";
</script>
<form method="POST" action="?/notion" use:enhance>
<button type="submit">Notionログイン</button>
</form>
次に、+page.server.ts
でsignInWithOAuth
メソッドを呼び出し、リダイレクトURLを指定しました。
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,
};
},
};
signInWithOAuth
のredirectTo
にパスを指定すると、searchParams
にcode
が付与されてauth/callback
にリダイレクトされます。
https://localhost:5173/auth/callback?code=e202e8c9-0990-40af-855f-ff8f872b1ec6
auth/callback
では、リダイレクト時にsearchParams
からcode
を受け取り、exchangeCodeForSession
でセッションに保存します。
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アクセストークンの取得を試みました。
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
で受け取っているcode
はPKCEフローで生成されたcode
であり、Notionから返されるcode
とは別物だということがわかりました。
code
が別物とわかったのでNotion APIのアクセストークン取得方法について探っていたところ、以下の記事に遭遇。
なんと、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_token
はOAuth呼び出し直後にのみ存在しセッションが更新されると消えてしまいます。
もしこのトークンを使用する必要がある場合は、自分の責任で保存しておく必要があります。
感想
Notionのトークンエンドポイントに一時コードを渡さなくてもアクセストークンが取得できるのは便利(?)だと思います。
ただ、この取得方法は少し暗黙的すぎる気がします。
明示的にNotionのAPIアクセストークンを取得するための手順が提供されていれば、ここまで悩まなかったのに、、、
Supabase公式ドキュメントを読んでも、provider_token
の詳細な仕様について見つけることができませんでした。
個人的には、SupabaseのLogin with Notionのページにこの情報が記載されていれば、もっとスムーズに実装できたのではないかと感じています。
今回の経験を通して、OAuth認証の仕組み、特にPKCEフローについて理解が深まったことは大きな収穫でした。
以上、Supabase × Notionの認証実装で得た学びを共有させていただきました。この記事が誰かのお役に立てることを願っています。
Discussion