ログイン・権限・データの境界を重ねて守る考え方を学びます。
重要なコードの抜粋へ ↓このページでわかること
- 認証と認可の違い
- RLSとサーバー側の確認
- 秘密情報と監査ログの扱い
ログインだけでは足りない
認証は「誰か」を確かめます。認可は「その人がその操作をしてよいか」を決めます。ログイン済みの利用者でも、他人の口座を送金元にすることはできません。管理者用の操作には別の権限が必要です。
境界を二重にする
APIは利用者と口座の関係を確認し、SupabaseのRow Level Securityも行単位で見られるデータを制限します。RLSだけに頼らず、サーバー側でも権限を検証します。
帳簿や監査ログには、一般利用者から直接書き込む権限を与えません。重要な変更は決められた処理経由に限定します。
本人確認は状態遷移として守る
顧客の申請は not_started または rejected から pending に進み、管理者の審査で verified または rejected になります。画面で承認ボタンを隠すだけでなく、データベース側でも不正な遷移を拒否します。
本人確認済みの顧客が自分で未確認へ戻したり、申請者が自分で verified にしたりできないことを確認します。学習用の簡略化であり、外部の本人確認事業者との連携はv1の範囲外です。
権限は経路ごとに確認する
通常の利用者、管理者、デモ用のアクターを分けます。APIの呼び出し、データベース関数、RLSの各境界で、誰の権限で動くのかを確かめます。特にサービスロール権限は強いため、ブラウザへ出さず、任意の表を自由に更新する近道にもしません。
Origin確認や秘密情報の扱いも含め、ひとつの防御が抜けても他の境界で止められる構成を考えます。
自分のデータだけ見えるか
ログインした顧客がAPIのURLに別の口座IDを入れたとします。画面にリンクがなくても、そのリクエストは届きます。サーバーで所有関係を確認し、行単位のアクセス制御でも閲覧対象を絞ります。
「一覧では他人が見えない」だけでは不十分です。詳細、履歴、更新など、それぞれの経路で他人のデータへ届かないか調べます。
管理者の強い権限を分割する
管理者には本人確認の承認、口座凍結、監査閲覧など異なる操作があります。管理画面へのログインを一つ許しただけで全操作を許すと、誤操作や権限の使いすぎが起こりやすくなります。
v1には管理者の役割と操作を分ける仕組みがあります。誰が何を実行できるかを操作単位で確かめ、権限の変更そのものも記録します。
セッションが変わる瞬間
ログインした直後、ログアウトした直後、管理者権限を無効にした直後では、画面とサーバーの認識がずれることがあります。重要な操作では、画面を開いた時点の情報だけに頼らず、実行時点で権限を確認します。
管理者が別タブで無効化されたのに、古い画面のボタンがまだ見えている場合も考えます。表示の古さと、実行権限の正しさを分けて扱います。
監査ログの中身を選ぶ
「送金に失敗した」という事実だけでは、調査時に対象が分かりません。時刻、操作の種類、実行者、対象、結果、リクエストIDなどを関連づけます。一方、パスワード、トークン、OTPの平文、秘密鍵は残しません。
必要な情報が不足すると原因を追えず、残しすぎると漏えい時の被害が大きくなります。監査の目的に合わせて記録項目を決めます。
権限のテストは本人以外から試す
自分の口座を読めるテストだけでは、他人の口座を読めないことは証明できません。別の顧客として同じAPIを呼び、詳細、履歴、送金の各経路で拒否されることを確かめます。管理者の役割を変えた直後も、古いセッションで操作できないか確認します。
ブラウザからの意図しない操作を防ぐ
ログイン済みのブラウザは認証情報を持ちます。別のページから意図しない更新リクエストを送らせないために、更新系APIではリクエストの出どころも確認します。v1にはOriginを確認するコードがあります。
ただしOrigin確認だけですべての権限問題は解決しません。対象口座が本人のものか、管理者としてその操作が許されるかは、それぞれ別に確認します。
重要なコードを読む
v1.0.0 の実コードから、この章で押さえたい箇所だけを抜粋しました。前後の定義は省略しています。
自分の口座だけを読めるようにする
SQLcreate policy bank_accounts_select_own
on public.bank_accounts
for select
to authenticated
using (
exists (
select 1
from public.customers c
where c.id = bank_accounts.customer_id
and c.auth_user_id = auth.uid()
)
);まず理解するログイン中の利用者ID auth.uid() と、口座を持つ顧客のIDが対応するときだけ、その口座行を読めるようにします。
一歩深くこれは行単位の防御です。同じマイグレーションでは一般利用者の表への書き込み権限も取り消しています。RLSだけ、画面だけのどちらか一方に頼らず、APIとデータベースの両方で境界を作ります。
出典:supabase/migrations/20260819132836_bank_accounts_rls_and_privileges.sql
本人確認申請の再送を安全にする
SQL / PLpgSQL if v_customer.kyc_status = 'pending' then
return v_customer;
end if;
if v_customer.kyc_status = 'verified' then
raise exception 'invalid state transition' using errcode = 'KY002';
end if;まず理解する申請が既に pending なら現在の行をそのまま返します。verified から申請し直すことは拒否します。
一歩深く応答が失われた後に同じ操作を再試行しても、pending なら二重更新や重複した申請イベントを作らない設計です。状態そのものがこの操作の再送判定になります。
出典:supabase/migrations/20260822090001_create_submit_kyc_application_function.sql