Supabase Edge Functions と Lovable 環境での LineWorks 認証実装 ― 分割構成への検討
この記事では、Lovable(フロントエンド)と Supabase(バックエンド)を組み合わせ、LineWorks の OAuth 認証を実装する際の各要素とその課題、そして解決策として Edge Function を分割して実装した試行錯誤について解説します。
まだ完成しておらず、とりあえず一旦これで行ってみるという話です。
1. 背景と目的
-
環境構成:
- フロントエンド:https://example-collection.lovable.app/auth
- バックエンド:Supabase Edge Functions(例: https://samplefunction.functions.supabase.co/get-authcode)
- 認証プロバイダー:LineWorks(OAuth 2.0 を利用)
-
目的:
- Lovable と Supabase を組み合わせた環境下で、LineWorks を用いたログイン認証(OAuth)を実現する。
- クライアント ID は公開しても問題ないが、Client Secret は秘匿が必要なため、Edge Functions 内で安全に取り扱う。
- Edge Function を利用することで、サーバーサイドで Secret を隠蔽しつつ、認可コード取得とアクセストークンの発行を行う。
- Implicit Flowは嫌!
先に結論
- EdgeFunctionは機能ごとに小さく分割する
- LineWorksで認証するので、ANON KEYによる認証は切る
2. OAuth 認証フローにおけるセキュリティ上の注意点
-
Client ID と Client Secret:
- Client ID は公開情報として問題ないが、Client Secret はブラウザに露出してはいけない。
→ Edge Functions で認証フローを処理することで、Secret をサーバーサイドに留める。
- Client ID は公開情報として問題ないが、Client Secret はブラウザに露出してはいけない。
-
OAuth フロー:
- Implicit Flow はアクセストークンが直接ブラウザに返るためリスクがあるため、認可コードグラントフロー(可能なら PKCE)を利用する。
→ 認可コードを受け取り、サーバー側でトークン交換を行う。
- Implicit Flow はアクセストークンが直接ブラウザに返るためリスクがあるため、認可コードグラントフロー(可能なら PKCE)を利用する。
3. 初期実装:単一の Edge Function での認可コード取得とトークン発行
最初は 1 つの Edge Function にて以下の処理を実装しました。
以下の2つの大きな機能を1つのEdgeFunctionで実装していました。
-
認可コード取得:
- フロントエンドからボタンを押すと、Edge Function が呼び出され、LineWorks の認可エンドポイント(https://auth.worksmobile.com/oauth2/v2.0/authorize)へリダイレクトします。
- ここで、client_id、redirect_uri、scope、response_type=code、state といったパラメータが付与されます。
-
アクセストークン発行:
- LineWorks 側でユーザー認証が成功すると、認可コードが Edge Function の redirect_uri に付与されて戻ります。
- Edge Function はそのコードを用いてアクセストークンを取得します。
4. 問題点:リダイレクト時の Authorization ヘッダーと CORS の問題
-
ブラウザのリダイレクトとヘッダーつけられない問題:
- LineWorksからのレスポンスはリダイレクトになります。通常のリダイレクト(HTTP 302)はブラウザ側で自動処理され、カスタムヘッダー(例えば Authorization ヘッダー)は付与されません。
→ そのため、Edge Function が「Authorization ヘッダー必須」の状態になっている場合、認可コード取得後のリダイレクト時にエラーが発生しました。
- LineWorksからのレスポンスはリダイレクトになります。通常のリダイレクト(HTTP 302)はブラウザ側で自動処理され、カスタムヘッダー(例えば Authorization ヘッダー)は付与されません。
-
CORS の問題:
-
fetch
を用いてリダイレクト処理を行おうとすると、LineWorks の認可エンドポイントが CORS 対応していないためエラーになるという課題もありました。
-
-
アクセストークンの返却先がフロントエンド問題
EdgeFunctionにリダイレクトすると、最後にアクセストークンを画面に返したいとなった場合に「リクエストに対してレスポンスを返す」ことによる受け渡しができない。(EdgeFunctionに画面を介さずリダイレクトしてしまったため)
Cookieをつけて画面のURLに遷移させたり、Queryにつけて新たに画面にアクセスしないといけない
5. 解決策:Edge Function の分割構成
これらの課題を解決するため、Edge Function を以下の 2 つに分割する構成を検討しました。
ユーザ情報を取得するものも含めると3つです。
(OAuth2.0じゃなくてOIDC使えばよかったな・・・・)
(1) 認可コード取得用 Edge Function
-
役割:
- ユーザーが「LineWorksでログイン」ボタンを押した際に実行され、認可エンドポイントへアクセス、最終的にはLovableのフロントエンドにリダイレクトします。
-
特徴:
- 認可コードを取得し、画面にリダイレクトする
- EdgeFunctionではなくフロントエンドにリダイレクトすることで、リクエストの起点とレスポンスの終点を揃える。
(2) アクセストークン発行用 Edge Function
-
役割:
- フロントエンド側で、URL に含まれる
code
とstate
を検出した場合に自動実行され、認可コードを受け取ってアクセストークンを発行します。
- フロントエンド側で、URL に含まれる
-
特徴:
- この関数では、認可コードからアクセストークンを取得するために、必要なリクエストを LineWorks のトークンエンドポイントに送信します。
- ここで、Edge Function 内でデフォルトの anon key を使用するか、JWT 検証を無効にして(
--no-verify-jwt
オプション)処理を進めることが検討されます。 - 結果として、アクセストークンがフロントエンドに返却され、フロントエンドはそのトークンを用いて LineWorks の API を呼び出し、認証を完了させます。
※ただし、このLineWorksのAPI実行もフロントエンドから直接はできないので、また別のEdgeFunctionを使うことになります。
6. フロントエンドと連携する流れ
-
ログインボタン押下:
- フロントエンド(https://example-collection.lovable.app/auth)で「LineWorksでログイン」ボタンを押すと、ユーザーは認可コード取得用の Edge Function にGETリクエスト。
-
認可コードの受け取り:
- ユーザーが LineWorks で認証・同意すると、LineWorks は認可コードを付与して、事前に設定したリダイレクトURI(画面)に戻ります。
-
アクセストークン発行:
- 画面は URL のクエリから
code
とstate
を検出すると、自動的にアクセストークン発行用の Edge Function を呼び出す仕組みを実装します。- Edge Function は認可コードからアクセストークンを取得し、EdgeFunctionのレスポンスとしてアクセストークンを画面に返します。
- 画面は URL のクエリから
-
認証完了:
- フロントエンドは受け取ったアクセストークンを利用して LineWorks の API を呼び出し、認証が成功した状態を確立します。
7. まとめ
-
目的:
- Lovable + Supabase 環境下で、LineWorks を利用した認証を実装する。
-
技術的なチャレンジ:
- Client Secret を秘匿するため、Supabase Edge Functions を活用。
- 単一の Edge Function で認可コード取得とアクセストークン発行を実装し、EdgeFunctionにリダイレクトすると Authorization ヘッダーが付かずエラーが発生。
-
解決策:
- Edge Function を 2 つに分割し、1 つは認可コード取得専用、もう 1 つはアクセストークン発行専用とする。
- 画面はcodeとstateがあるかどうかで処理を分ける。(URL に
code
とstate
が含まれる場合に自動的にアクセストークン発行用 Edge Function を呼び出す処理を実装)
一旦これで進めてみて、何か不都合等があれば更新します!