はじめに
個人開発で Next.js + Supabase を使っていると、その開発速度の速さに感動する一方で、ある巨大な不安にぶつかります。
「セキュリティ、本当に一人で担保できてる…?」
Webサービスを作る上で避けて通れないのが「認証(Authentication)」と「認可(Authorization)」です。
特にSupabaseのようなBaaS(Backend as a Service)では、認証周りの処理を簡単に実装できる反面、どこまでがSupabase任せで、どこからが開発者自身の責任なのか の境界線があいまいになりがちです。
一歩設定を間違えると、誰でも他人のデータを閲覧・操作できてしまうリスクも潜んでいます。
本記事では、Supabaseを題材に 認証・認可の根本的な違いと仕組み を整理しつつ、
開発者が 実務や個人開発で絶対に気をつけるべきセキュリティの注意点 について調べた内容をまとめました。
セキュリティに対する苦手意識をなくし、安全にプロダクトを公開するための参考になれば幸いです。
この記事で得られる観点
- 認証 と 認可 の違い
- Supabase が 自動で担う部分 と、開発者が 自分で設計すべき部分 の境界
-
RLS(Row Level Security) の基本と、
USING/WITH CHECKの使い分け - API キー と ログイン情報(JWT) について
- 個人開発でも避けたい アンチパターン と、確認すべきチェック項目
認証・認可ってそもそも何?
私が混乱しやすかったのは、どちらも「ログイン周り」と一括りに考えてしまう点です。
実際には、「誰か」を確かめる工程 と、「何をしてよいか」を決める工程 は別物です。
身近なたとえで言うと、マンションの入館 に似ています。
| 概念 | たとえ | 役割 |
|---|---|---|
| 認証(Authentication) | エントランスで「本人確認」 | この人は登録済みの住人か |
| 認可(Authorization) | 各部屋の鍵の有無 | この人は この部屋 に入ってよいか |
認証に成功しても、データベース側で誰に何を許すかを決めていなければ、「ログインはできたが、他人のデータも見える」という状態になり得ます。
Supabase ではログイン UI を用意するだけでは不十分で、データベース側の認可設計 が別途必要です。
認証・認可を図で確認
- 認証 … メール・パスワードや OAuth などで「誰であるか」を確かめる
- 認可 … 確かめたユーザーが、どの行・どの操作 をしてよいかを決める
フロントエンドで「ログイン済みならボタンを表示する」だけでは、認可は担保できません。
ブラウザから直接 API を叩かれた場合、UIの制御はすり抜けられるため、DBのRLSを最も重要な防波堤 として私は設計しています。
Supabase ではどこまで担保している?
SupabaseのAuthは、主に認証を担います。
ログイン・サインアップ、OAuth 連携、JWT(アクセス用トークン)の発行、セッション管理などです。
ただし、Auth が関わるのは認証だけではありません。
発行された JWT には、ユーザー ID などの情報が入っています。この情報は後述する RLS のポリシーから auth.uid() として参照でき、認可の判断材料になります。
Supabase が用意してくれないのは、認可のルールそのものです。
たとえば「自分が登録したレシピだけ読める」「同じ家族グループのメンバーだけ編集できる」は、開発者が RLS ポリシーなどで設計します。
| 領域 | Supabase が担う | 開発者が担う |
|---|---|---|
| ユーザー登録・ログイン | ○ | UI や UX の設計 |
| JWT の発行・検証 | ○ | トークンの扱い方の理解 |
auth.uid() などの判断材料の提供 |
○ | ポリシーでの使い方の設計 |
| テーブル単位のアクセス制御 | 仕組み(RLS)を提供 | 認可ルールそのものを書く |
| ビジネスルール (家族共有・グループ共有など) |
× | ポリシー・RPC・アプリ側の整理 |
認証と判断材料は Supabase、認可のルールは自分と私は整理しました。
RLS とは何か
RLS(Row Level Security)は、PostgreSQL の機能です。
行単位 で SELECT / INSERT / UPDATE / DELETE を制限できます。
たとえば、recipes テーブルに「ログインユーザーは自分の行だけ 読める」というルールを DB に置けます。
アプリのコードやフロントのチェックをすり抜けられても、DB側で拒否できる のがポイントです。
ALTER TABLE recipes ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can read own recipes"
ON recipes FOR SELECT
USING (auth.uid() = user_id);
auth.uid() は、リクエストに含まれる JWT から Supabase が取り出すユーザー ID です。
「誰でログインしているか」 は Supabase が教えてくれますが、「その人に何を許すか」 はポリシー側の責務です。
RLS を有効にすると何が起きるか
私が最初に誤解していたのが、「ポリシーをまだ書いていない状態」の扱いです。
| テーブルの設定 | 結果 |
|---|---|
| RLS が無効 | RLSによる行単位のアクセス制御が行われない |
| RLS が有効・ポリシーなし | すべて拒否される(安全側に倒れる) |
| RLS が有効・ポリシーあり | 条件を満たす行だけ許可される |
つまり、RLS を有効にすると まず全部が閉じます。そこから「誰に何を許すか」をポリシーで開けていく、という順番です。
危険なのはポリシーの書き忘れよりも、そもそも RLS を有効にしていないケースです。
USING と WITH CHECK
ポリシーの条件には2種類あります。すでにある行を見てよいかと、これから書き込む行が条件を満たしているかです。
| 条件 | 対象 | 主に使う操作 |
|---|---|---|
USING |
すでにテーブルにある行 | SELECT / UPDATE / DELETE |
WITH CHECK |
これから保存される行 | INSERT / UPDATE |
書き込み側を縛らないと、「他人の user_id を指定してデータを登録する」といった操作を止められません。
また、UPDATEはUSINGとWITH CHECK両方の観点での設計が必要になるため、どちらも設定していることを確認するのが重要です。
CREATE POLICY "Users can insert own recipes"
ON recipes FOR INSERT
WITH CHECK (auth.uid() = user_id);
API キーとログイン情報は別物
ここで混同しやすいのが、API キー と ログインしたユーザーの情報(JWT) の違いです。
| 種類 | 答えるもの | 具体例 |
|---|---|---|
| API キー | 何が アクセスしているか | このWebアプリ、このサーバー |
| JWT | 誰が アクセスしているか | ログイン中のユーザー |
API キーはアプリの身分証、JWT は利用者の身分証、というイメージです。
同じ API キーを使っていても、ログイン前とログイン後では DB 側で別のロールとして扱われ、RLS の判定もそれに従います。
2種類のAPI キー
Supabase プロジェクトには、公開してよいキーとサーバー専用のキーがあります。
| キー | 置き場所 | RLS | 使いどころ |
|---|---|---|---|
| Publishable key | ブラウザ・モバイル | 適用される | 通常のクライアントからの読み書き |
| Secret key | サーバー専用 | バイパスする | 管理処理・バッチ(要・厳重管理) |
以前は anon キー、service_role キーという名前でした。現在は Legacy API keys という扱いで、役割はそれぞれ Publishable key、Secret key に対応します。
Secret key は RLS を通りません。クライアントに埋め込むとRLSをバイパスし、プロジェクトのデータへ強い権限でアクセスできるため、クライアントに埋め込むとRLSで保護していたデータへの意図しないアクセスを許してしまう恐れがあります。
私の個人開発では、通常の読み書きは Publishable key と RLS の経路に寄せ、
Secret key は 本当に必要なサーバー処理だけ に限定する方針にしています。
よくあるアンチパターン
1. RLS を有効にしていない
RLS が無効なテーブルでは、PostgreSQL の通常の権限設定に基づいてアクセスが許可されます。SupabaseのData APIからアクセスするテーブルでは、RLSを有効にして意図したアクセス制御を行います。
2. フロントの表示制御だけで安心している
「未ログインなら画面を出さない」は UX であって、セキュリティの本体ではありません。
API は直接叩けるため、認可は DB またはサーバー側 に置く必要があります。
3. Secret key をクライアントに渡している
環境変数の命名ミスや、Next.js の NEXT_PUBLIC_ 付き変数への誤設定で、本番に鍵が露出した事例は後を絶ちません。
Secret key は サーバー実行環境だけ に閉じ込めます。
4. 意図した公開範囲を超えてUSING (true)を設定している
USING (true) は「全行を許可」という意味です。お知らせ一覧のように 全員に見せたいデータ なら、これが正解のこともあります。
問題になるのは、動かすことを優先して、本来は制限すべきテーブル にまで USING (true) を置いてしまう場合です。
5. RLS だけを見て安心している
RLS はテーブルへのアクセスを守りますが、データに触れる経路はほかにもあります。
ビュー、SECURITY DEFINER を付けた関数、Storage のバケットなどは、それぞれ別に設定を確認する必要があります。
本番公開前の確認事項
個人開発でも、最低限次を確認しています。
- API から触るテーブルで RLS が 有効 になっているか
- 要件上必要な操作に、過不足のないポリシー が定義されているか
- 登録・更新のポリシーで
WITH CHECKを書いているか - Secret key がクライアント側のコードに含まれていないか
- 「他人の
user_idの行」に アクセスできない ことを実際に試したか - ビューや関数、Storage など RLS 以外の経路 も確認したか
最後に
Supabase は認証まわりを大きく肩代わりしてくれますが、「ログインできた=安全」ではありません。
認証で「誰か」を確かめ、その情報をもとに 認可のルールを DB まで落とし込む のが大切だと学べました。
私自身、RLS とアプリケーションコードの境界で何度も迷いましたが、
Supabase が担う部分 / 自分が設計する部分 を分けて整理すると、次に何を調べればよいかがはっきりしました。