Supabase Auth 高度な設定:OAuth、SSO、権限制御の完全ガイド
ある日の午後、営業担当の同僚が駆け込んできました。「クライアントが Okta でのログインを要求していて、2週間以内に SSO をリリースしないといけない」。一瞬呆然としました。私のアプリはメール登録と Google OAuth しか対応していません。SAML? 触れたこともありません。
正直なところ、このシナリオは B2B SaaS では珍しくありません。企業クライアントは従業員に個別のアカウント登録をさせません。彼らは統一されたアイデンティティ管理システム(Okta、Azure AD、Google Workspace)を持っており、ワンクリックでログインでき、従業員が退職すれば自動的にアカウントが無効になります。
この記事は、まさにそのようなシナリオのために書かれました。OAuth ソーシャルログインから始め、SAML SSO 企業統合へと進み、最後に Row Level Security(RLS)でマルチテナント権限分離を実現します。コンシューマーレベルから企業レベルまで対応する、完全な認証・認可ソリューションです。
一、OAuth マルチプロバイダー設定の実践
OAuth ソーシャルログインは、ほとんどのアプリの出発点です。ユーザーはパスワードを覚えたくありませんし、開発者もパスワードの保存と検証を扱いたくありません。Google や GitHub に任せれば、双方にとって楽になります。
Supabase がサポートする OAuth プロバイダーは多数あります:Google、GitHub、Apple、Facebook、Discord、Twitter… しかし実際の本番環境では、Google と GitHub が最も広く使われており、Apple は iOS アプリの必須要件です(App Store 審査で必要)。まずは Google から始めましょう。
Google OAuth 設定
Google Cloud Console を開くと、メニューが複雑で少し戸惑うかもしれません。慌てずに、「OAuth Client ID」で検索すれば入り口が見つかります。
作成時に Web application タイプを選択し、重要なステップは Authorized redirect URI を入力することです:
https://<あなたのプロジェクトref>.supabase.co/auth/v1/callback
この URL は、Supabase Auth サービスが OAuth コールバックを受け取る場所です。プロジェクト ref は Supabase Dashboard の左上で確認でき、abcdefghijklmnop のような形式です。
Client ID と Client Secret を取得したら、Supabase Dashboard に戻り、Authentication > Providers で Google を有効にし、これら2つの値を入力します。
コードからの呼び出しは簡単です:
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
'https://abcdefghijklmnop.supabase.co',
'your-anon-key'
)
// OAuth ログインを開始
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: 'https://your-app.com/auth/callback',
scopes: 'email profile'
}
})
if (error) {
console.error('ログイン失敗:', error.message)
return
}
// data.url がリダイレクト先、window.location.href = data.url で遷移
redirectTo は、アプリがログイン結果を受け取る URL です。Supabase は code と state パラメータを送信します。そのページで supabase.auth.exchangeCodeForSession() を呼び出してログインを完了させます:
// /auth/callback ページで
const { error } = await supabase.auth.exchangeCodeForSession()
if (!error) {
// ログイン成功、ホームにリダイレクト
window.location.href = '/'
}
GitHub OAuth 設定
GitHub の設定も同様です。Settings > Developer settings > OAuth Apps が入り口です。作成時に入力する Authorization callback URL は、同じく https://<プロジェクトref>.supabase.co/auth/v1/callback です。
1つ違いがあります:GitHub OAuth App にはスコープ設定画面がありません。スコープはコード内で指定します:
await supabase.auth.signInWithOAuth({
provider: 'github',
options: {
redirectTo: 'https://your-app.com/auth/callback',
scopes: 'repo user' // repo でプライベートリポジトリにアクセス可能
}
})
ユーザーログインだけなら、デフォルトのスコープで十分です。ユーザーの GitHub データにアクセスする必要がある場合(リポジトリ一覧の同期など)は、repo を追加します。
Apple OAuth 設定
Apple の設定が最も複雑です。Apple Developer Portal で Services ID を作成し、秘密鍵(.p8 ファイル)も生成する必要があります。秘密鍵は1回しかダウンロードできず、紛失した場合は再生成が必要です。
主要なパラメータ:
- Services ID:Client ID に相当
- Team ID:Membership ページで確認
- Key ID:秘密鍵の ID
- Private Key:ダウンロードした .p8 ファイルの内容
Supabase Dashboard でこれらの値を入力する際、Private Key はファイルの内容全体(BEGIN/END の行を含む)をコピーします。
呼び出しコードは Google/GitHub と同じです:
await supabase.auth.signInWithOAuth({
provider: 'apple',
options: {
redirectTo: 'https://your-app.com/auth/callback'
}
})
iOS アプリには別の方法があります:ネイティブの Sign in with Apple API を直接呼び出し、identity token を取得して Supabase に渡します:
// フロントエンドで Apple から返された identity_token を受け取る
const { data, error } = await supabase.auth.signInWithIdTokenCredentials({
provider: 'apple',
token: identityToken
})
この方法は、すでにネイティブの Apple ログインを統合している iOS アプリに適しており、既存のロジックを再利用できます。
二、SAML SSO 企業統合
冒頭のシナリオに戻りましょう:クライアントが Okta でのログインを要求しています。ここで OAuth では不十分で、SAML 2.0 SSO が必要になります。
SAML の動作方式は OAuth と全く異なります。OAuth はユーザーがサードパーティアプリに自分のデータへのアクセスを許可するものですが、SAML は企業のアイデンティティプロバイダー(IdP)がアプリケーション(ServiceProvider, SP)にユーザー識別情報を送信するものです。企業にとって、SAML の方が管理しやすいです。ユーザーが退職すれば IdP でアカウントを無効化するだけで、接続されているすべてのアプリケーションで自動的に無効になります。
設定前の準備
クライアントから IdP の Metadata を入手する必要があります。このファイルには IdP の証明書、エンドポイントアドレスなどの情報が含まれています。Okta、Azure AD、Google Workspace にはすべて Metadata をエクスポートする機能があります。
Supabase が提供する重要な URL:
EntityID(SP 識別子):
https://<プロジェクトref>.supabase.co/auth/v1/sso/saml/metadata
ACS URL(SAML Response を受け取るアドレス):
https://<プロジェクトref>.supabase.co/auth/v1/sso/saml/acs
Metadata URL(ダウンロード可能):
https://<プロジェクトref>.supabase.co/auth/v1/sso/saml/metadata?download=true
これらの URL をクライアントの IT 管理者に伝え、Okta/Azure AD で SAML アプリケーションを作成してもらいます。
Supabase CLI で SSO を設定
Supabase Dashboard も SAML 設定をサポートしていますが、私は CLI を使う方が好みです。企業クライアントの設定は何度もデバッグが必要になることがあり、コマンドラインの方が制御しやすいからです。
# SAML 接続を追加
supabase sso add --type saml --project-ref <プロジェクトref> \
--metadata-url 'https://company.okta.com/app/exk123/saml/samlmetadata' \
--domains company.com
--domains パラメータが重要です。これは Supabase に、「すべての company.com ドメインのユーザーがログインする場合、この SAML 接続を使用する」ことを伝えます。
--metadata-file を使って --metadata-url の代わりに XML ファイルを直接アップロードすることもできます:
supabase sso add --type saml --project-ref <プロジェクトref> \
--metadata-file ./okta-metadata.xml \
--domains company.com
コマンドを実行すると、abc123def456 のような sso_provider_id が返されます。この ID は RLS 権限分離の鍵となります。
Attribute Mapping の設定
SAML Response にはユーザー情報(メール、氏名、部署など)が含まれていますが、フィールド名は統一されていない可能性があります。Supabase にこれらのフィールドをどうマッピングするかを伝える必要があります。
JSON ファイルを作成します:
{
"keys": {
"email": {
"name": "email",
"names": ["EmailAddress", "email", "mail"],
"required": true
},
"first_name": {
"name": "first_name",
"names": ["FirstName", "givenName", "first_name"]
},
"last_name": {
"name": "last_name",
"names": ["LastName", "surname", "last_name"]
}
}
}
そして supabase sso update でこの設定を適用します:
supabase sso update <sso_provider_id> \
--project-ref <プロジェクトref> \
--attribute-mapping-file ./mapping.json
マルチテナント SSO 設定
1つのプロジェクトに複数の SAML 接続を追加できます。例えば、2つの企業クライアントがいるとします。1つは Acme Corp(Okta 使用)、もう1つは Globex Inc(Azure AD 使用):
# Acme Corp の SSO を追加
supabase sso add --type saml --project-ref <プロジェクトref> \
--metadata-url 'https://acme.okta.com/.../metadata' \
--domains acme.com
# sso_provider_id: provider_abc が返される
# Globex Inc の SSO を追加
supabase sso add --type saml --project-ref <プロジェクトref> \
--metadata-url 'https://globex.azure.com/.../metadata' \
--domains globex.com
# sso_provider_id: provider_def が返される
これで、Acme Corp の従業員がログインすると provider_abc、Globex Inc の従業員は provider_def を経由します。各 SSO 接続のユーザーデータは分離されています。
ユーザーログイン体験
設定完了後、ユーザーのログインフローは次のようになります:
- ユーザーがメールアドレスを入力(例:
[email protected]) - Supabase が
acme.comドメインに SSO 設定があることを検出 - Acme の Okta ログインページに自動的にリダイレクト
- ユーザーが Okta でアカウントとパスワードを入力(すでにログイン済みの場合はそのまま通過)
- Okta が SAML Response を Supabase に送信
- Supabase が検証後セッションを作成し、アプリケーションにリダイレクト
コード側で SSO ログインを開始:
// ユーザーがメールを入力した後、このメソッドを呼び出す
const { data, error } = await supabase.auth.signInWithSSO({
domain: 'acme.com'
})
if (data?.url) {
// SSO ログインページにリダイレクト
window.location.href = data.url
}
sso_provider_id を直接使うこともできます:
const { data, error } = await supabase.auth.signInWithSSO({
providerId: 'provider_abc'
})
SSO ユーザーの特徴
SSO ログインで作成されるユーザーは通常ユーザーとは異なります:
- メールは IdP が管理:ユーザーはアプリ内でメールを変更できません
- パスワードなし:パスワードは IdP に保存され、Supabase は検証に関与しません
- メールは未検証:IdP がすでに検証済みだからです
これは、ユーザーがメールを変更する必要がある場合、クライアントの IT 管理者に Okta/Azure AD で操作を依頼する必要があることを意味します。
三、Row Level Security の高度な使い方
認証は「ユーザーが誰か」の問題を解決し、認可は「ユーザーが何ができるか」の問題を解決します。
従来のアプローチは、ビジネスコードで権限をチェックすることです。各 API で現在のユーザーがそのデータにアクセスできるかを判断します。コードが重複し、見落としが発生しやすく、パフォーマンスも良くありません。各リクエストでデータベースをクエリして権限を判断する必要があるからです。
PostgreSQL の Row Level Security(RLS)は、権限チェックをデータベース層に移します。各クエリに自動的に権限フィルタが適用され、ビジネスコードが気にする必要がありません。
RLS 有効化後のデフォルト動作
多くの人がここでハマります:RLS を有効にすると、デフォルトですべてのアクセスが拒否されます。
-- RLS を有効化
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- この時点で、すべてのクエリが空を返す(管理者も含む)
SELECT * FROM posts; -- 0 行を返す
アクセスを許可するには Policy を作成する必要があります。
Policy の2つの重要な部分
Policy には2つの句があります:USING と WITH CHECK。
USING 句:ユーザーがそのデータを「見る」または「特定」できるかを判断します。SELECT、UPDATE、DELETE 操作に使用されます。ユーザーはまずデータを見ることができてから、変更または削除できます。
WITH CHECK 句:ユーザーがそのデータを「書き込める」かを判断します。INSERT、UPDATE 操作に使用されます。書き込まれた後のデータが条件を満たす必要があります。
-- ユーザーは自分の投稿のみ操作可能
CREATE POLICY "Users manage own posts" ON posts
FOR ALL TO authenticated
USING (auth.uid() = author_id)
WITH CHECK (auth.uid() = author_id);
-- 閲覧のみ許可(変更不可)
CREATE POLICY "Users view own posts" ON posts
FOR SELECT TO authenticated
USING (auth.uid() = author_id);
-- 挿入のみ許可(他人のものは閲覧不可)
CREATE POLICY "Users insert posts" ON posts
FOR INSERT TO authenticated
WITH CHECK (auth.uid() = author_id);
auth.uid() は現在ログインしているユーザーの UUID を返します。未ログインの場合は null を返すため、TO authenticated は Policy がログインユーザーにのみ適用されることを保証します。
RESTRICTIVE vs PERMISSIVE
1つのテーブルに複数の Policy を作成できます。デフォルトは PERMISSIVE モードで、いずれかの Policy を満たせばアクセスできます。
-- Policy 1:ユーザーは自分のものを見れる
CREATE POLICY "Own data" ON posts
FOR SELECT USING (auth.uid() = author_id);
-- Policy 2:公開投稿は誰でも見れる
CREATE POLICY "Public posts" ON posts
FOR SELECT USING (is_public = true);
-- 2つの PERMISSIVE Policy:いずれかを満たせばOK
RESTRICTIVE Policy は「必ず満たす必要がある」ものです。PERMISSIVE Policy と組み合わせて、より厳格な制限を形成します。
-- すべてのアクセスはこの条件を満たす必要がある(「グローバルフィルタ」として)
CREATE POLICY "Tenant isolation" ON posts
AS RESTRICTIVE TO authenticated
USING (tenant_id = (
SELECT tenant_id FROM users WHERE id = auth.uid()
));
-- その上で PERMISSIVE Policy で具体的な操作を制御
CREATE POLICY "Authors edit own posts" ON posts
FOR UPDATE USING (auth.uid() = author_id);
この組み合わせの効果:ユーザーは正しいテナントに属している必要があり(RESTRICTIVE)、かつ作者でないと編集できない(PERMISSIVE)。
マルチテナント分離の完全パターン
SaaS アプリケーションがあり、各企業クライアントが1つのテナントと仮定します。ユーザーテーブルに tenant_id があり、すべてのビジネステーブルをテナントで分離する必要があります。
-- ユーザーテーブル
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
email TEXT,
sso_provider_id TEXT -- SSO ユーザーのみこの値を持つ
);
-- ビジネステーブル
CREATE TABLE projects (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL,
name TEXT,
created_by UUID REFERENCES users(id)
);
-- RLS を有効化
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
-- グローバルテナント分離(RESTRICTIVE)
CREATE POLICY "Tenant isolation" ON projects
AS RESTRICTIVE TO authenticated
USING (tenant_id = (
SELECT tenant_id FROM users WHERE id = auth.uid()
));
-- 具体的な操作権限(PERMISSIVE)
CREATE POLICY "Tenant users can view" ON projects
FOR SELECT TO authenticated
USING (true); -- すでに RESTRICTIVE でフィルタリング済み、ここでは通す
CREATE POLICY "Project creators can edit" ON projects
FOR UPDATE TO authenticated
USING (created_by = auth.uid());
このように設定すると、各クエリに自動的にテナントフィルタが適用されます。ビジネスコードで SELECT * FROM projects を書いても、データベースは現在のテナントのデータのみを返します。
SSO ユーザーのマルチテナント分離
SSO ログインのユーザーは sso_provider_id で分離できます。JWT にはログイン方法を示すフィールドがあります:
-- SSO ユーザー用の RLS Policy
CREATE POLICY "SSO tenant isolation" ON organization_settings
AS RESTRICTIVE TO authenticated
USING (sso_provider_id = (
SELECT auth.jwt()#>>'{amr,0,provider}'
));
auth.jwt()#>>'{amr,0,provider}' は JWT からログイン方法の provider ID を抽出します。SSO ログインの場合、この値が sso_provider_id になります。
パフォーマンス最適化のポイント
RLS Policy は暗黙的な WHERE 句です。各クエリに自動的に追加され、サブクエリが含まれる可能性があります。これはパフォーマンスに影響します。
いくつかの最適化のアドバイス:
1. Policy 内のフィールドにインデックスを作成
CREATE INDEX idx_posts_author ON posts(author_id);
CREATE INDEX idx_projects_tenant ON projects(tenant_id);
2. Policy 内のサブクエリを避ける
サブクエリは各行で実行されるため、オーバーヘッドが大きいです。より良い方法は、JWT に tenant_id を保存し、直接 auth.jwt() で抽出することです:
-- 非推奨:サブクエリ
USING (tenant_id = (SELECT tenant_id FROM users WHERE id = auth.uid()))
-- 推奨:JWT に tenant_id を保存
USING (tenant_id = (auth.jwt()->>'tenant_id')::uuid)
後述の Custom Access Token Hook で tenant_id を JWT に入れる方法を説明します。
3. SECURITY DEFINER 関数で複雑なロジックをカプセル化
Policy 内の複雑なロジックは関数にカプセル化できます。関数を SECURITY DEFINER に設定することで、毎回重複実行を避けます:
CREATE FUNCTION current_tenant_id() RETURNS UUID
LANGUAGE SQL STABLE SECURITY DEFINER AS $$
SELECT tenant_id FROM users WHERE id = auth.uid();
$$;
-- Policy 内で関数を呼び出す
CREATE POLICY "Tenant isolation" ON projects
AS RESTRICTIVE USING (tenant_id = current_tenant_id());
STABLE は同じトランザクション内で戻り値が変わらないことを示し、PostgreSQL は1回のみ実行するように最適化します。
四、Custom Claims と RBAC の実装
JWT にはデフォルトで Supabase の組み込みフィールドしかありません:ユーザー ID、メール、ロール(authenticated/anon)など。しかし、多くの場合、より多くの情報が必要です。ユーザーロール(admin/moderator)、テナント ID、権限リストなど。
Supabase は Custom Access Token Hook を提供しており、JWT が発行される前にその内容を変更できます。
なぜ Custom Claims が必要か
典型的なシナリオ:
- RBAC 権限制御:JWT にユーザーロールを保存し、RLS Policy でロールに基づいて権限を判断
- マルチテナント分離:JWT に tenant_id を保存し、Policy 内のサブクエリを回避
- JWT サイズの削減:デフォルトの JWT には多くのフィールドが含まれ、SSR シナリオでの転送オーバーヘッドが大きいため、不要なものを削除
Supabase の JWT にはデフォルトで多くのフィールドがあります(session_id、aal、amr など)。各リクエストでこれを送信する必要があります。大量の SSR ページがあるアプリケーションでは、JWT が cookie で転送され、サイズがパフォーマンスに影響します。
Custom Access Token Hook の実装
Hook は PL/pgSQL 関数で、JWT 生成前に実行されます。claims を追加、変更、削除できます。
-- Hook 関数を作成
CREATE OR REPLACE FUNCTION public.custom_access_token_hook(event jsonb)
RETURNS jsonb
LANGUAGE plpgsql
AS $$
DECLARE
claims jsonb;
user_role text;
tenant_id uuid;
BEGIN
-- event から claims を取得
claims := event->'claims';
-- user_roles テーブルからユーザーロールを取得
SELECT role INTO user_role
FROM public.user_roles
WHERE user_id = (event->>'user_id')::uuid;
-- ユーザーにロールがあれば、claims に追加
IF user_role IS NOT NULL THEN
claims := jsonb_set(claims, '{user_role}', to_jsonb(user_role));
END IF;
-- users テーブルから tenant_id を取得
SELECT tenant_id INTO tenant_id
FROM public.users
WHERE id = (event->>'user_id')::uuid;
IF tenant_id IS NOT NULL THEN
claims := jsonb_set(claims, '{tenant_id}', to_jsonb(tenant_id));
END IF;
-- 修正した claims を返す
RETURN jsonb_build_object('claims', claims);
END;
$$;
-- supabase_auth_admin にこの関数の実行権限を付与
GRANT EXECUTE ON FUNCTION public.custom_access_token_hook(jsonb)
TO supabase_auth_admin;
-- Supabase Dashboard でこの Hook を有効化
-- Authentication > Hooks > Custom Access Token > 上の関数を選択
Hook の入力パラメータ event には以下が含まれます:
user_id:現在のユーザー UUIDclaims:現在の JWT claimsauthentication_method:ログイン方法(password、oauth、sso/saml など)
返された claims は最終的な JWT にマージされます。
RBAC テーブル構造設計
完全なロール権限システムには数枚のテーブルが必要です:
-- ロールタイプを定義
CREATE TYPE app_role AS ENUM ('admin', 'moderator', 'user');
-- 権限タイプを定義
CREATE TYPE app_permission AS ENUM (
'posts.delete', -- 任意の投稿を削除
'posts.pin', -- 投稿をピン留め
'users.manage', -- ユーザー管理
'settings.edit' -- 設定の編集
);
-- ユーザー-ロールテーブル
CREATE TABLE public.user_roles (
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
role app_role NOT NULL,
PRIMARY KEY (user_id, role)
);
-- ロール-権限テーブル
CREATE TABLE public.role_permissions (
role app_role NOT NULL,
permission app_permission NOT NULL,
PRIMARY KEY (role, permission)
);
-- デフォルト権限を挿入
INSERT INTO role_permissions (role, permission) VALUES
('admin', 'posts.delete'),
('admin', 'posts.pin'),
('admin', 'users.manage'),
('admin', 'settings.edit'),
('moderator', 'posts.delete'),
('moderator', 'posts.pin');
authorize() 権限チェック関数
ロールと権限テーブルができたら、ユーザーがある権限を持っているかをチェックする関数が必要です:
CREATE OR REPLACE FUNCTION public.authorize(
requested_permission app_permission
)
RETURNS boolean
LANGUAGE plpgsql
STABLE SECURITY DEFINER
AS $$
DECLARE
user_role app_role;
BEGIN
-- JWT からユーザーロールを取得
SELECT (auth.jwt()->>'user_role')::app_role
INTO user_role;
-- JWT にロールがなければ false を返す
IF user_role IS NULL THEN
RETURN false;
END IF;
-- ロールがその権限を持っているかチェック
RETURN EXISTS (
SELECT 1 FROM public.role_permissions
WHERE role = user_role
AND permission = requested_permission
);
END;
$$;
-- authenticated ロールに実行権限を付与
GRANT EXECUTE ON FUNCTION public.authorize(app_permission)
TO authenticated;
RLS Policy で authorize() を呼び出す
これで Policy 内で authorize() を使って権限をチェックできます:
-- admin/moderator のみ投稿を削除可能
CREATE POLICY "Role-based delete" ON posts
FOR DELETE TO authenticated
USING (
authorize('posts.delete') OR auth.uid() = author_id
);
-- admin のみ投稿をピン留め可能
CREATE POLICY "Admin pin posts" ON posts
FOR UPDATE TO authenticated
USING (
NOT is_pinned OR authorize('posts.pin')
);
最初の Policy のロジック:ユーザーは投稿を削除できます。posts.delete 権限があるか(admin/moderator)、または投稿の作者であるか。
2番目の Policy のロジック:投稿を編集する際、is_pinned を true に設定するには posts.pin 権限が必要です。
フロントエンドで Custom Claims を読み取る
Custom Claims は JWT 内にあるため、フロントエンドで access_token をデコードして読み取る必要があります:
import { jwtDecode } from 'jwt-decode'
// セッションを取得
const { data: { session } } = await supabase.auth.getSession()
if (session) {
const decoded = jwtDecode(session.access_token)
console.log('ユーザーロール:', decoded.user_role)
console.log('テナントID:', decoded.tenant_id)
}
jwtDecode は JWT をデコードするだけで、署名を検証しません。フロントエンドでは検証の必要がありません。検証は Supabase サーバー側で行われます。
五、企業 SaaS 完全シナリオ実践
これまで学んだことを組み合わせて、完全な企業 SaaS 認証ソリューションを構築します。
シナリオ:あなたの SaaS 製品には2種類のユーザーがいます:
- 企業ユーザー:クライアント企業の SSO でログイン(Okta/Azure AD)、アカウントはあるテナントに属する
- 個人ユーザー:Google/GitHub OAuth でログイン、テナントには属さず、公開リソースのみアクセス可能
データ分離要件:企業ユーザーは自分のテナントのデータのみ閲覧可能、個人ユーザーは企業データを閲覧不可。
ユーザーテーブル設計
CREATE TABLE public.users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email TEXT NOT NULL,
-- 認証ソース
auth_type TEXT NOT NULL DEFAULT 'oauth', -- 'oauth' または 'sso'
-- SSO ユーザー専用フィールド
sso_provider_id TEXT, -- SSO 接続の ID
tenant_id UUID, -- 所属テナント
-- 基本情報
full_name TEXT,
avatar_url TEXT,
-- タイムスタンプ
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
-- テナントテーブル
CREATE TABLE public.tenants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
sso_provider_id TEXT NOT NULL UNIQUE, -- SSO 接続と関連付け
plan_type TEXT NOT NULL DEFAULT 'team', -- 'team' または 'enterprise'
created_at TIMESTAMPTZ DEFAULT now()
);
Custom Access Token Hook で統一処理
Hook は2種類のユーザーを区別する必要があります:
CREATE OR REPLACE FUNCTION public.custom_access_token_hook(event jsonb)
RETURNS jsonb
LANGUAGE plpgsql
AS $$
DECLARE
claims jsonb;
user_record RECORD;
BEGIN
claims := event->'claims';
-- ユーザー情報を取得
SELECT auth_type, sso_provider_id, tenant_id, role
INTO user_record
FROM public.users
WHERE id = (event->>'user_id')::uuid;
-- ユーザーが存在しない(新規登録)、スキップ
IF user_record IS NULL THEN
RETURN jsonb_build_object('claims', claims);
END IF;
-- 認証タイプを追加
claims := jsonb_set(claims, '{auth_type}', to_jsonb(user_record.auth_type));
-- SSO ユーザーに tenant_id と sso_provider_id を追加
IF user_record.auth_type = 'sso' THEN
IF user_record.tenant_id IS NOT NULL THEN
claims := jsonb_set(claims, '{tenant_id}', to_jsonb(user_record.tenant_id));
END IF;
IF user_record.sso_provider_id IS NOT NULL THEN
claims := jsonb_set(claims, '{sso_provider_id}', to_jsonb(user_record.sso_provider_id));
END IF;
END IF;
-- ユーザーロールを追加(あれば)
IF user_record.role IS NOT NULL THEN
claims := jsonb_set(claims, '{user_role}', to_jsonb(user_record.role));
END IF;
RETURN jsonb_build_object('claims', claims);
END;
$$;
ログイン後のユーザー作成フロー
OAuth と SSO ログインはどちらも auth.users テーブルにレコードを作成します。ログイン成功後、ユーザー情報を public.users テーブルに同期する必要があります。
Auth Hooks の mfa_verification_hook またはビジネスコードで処理できます:
// auth callback ページで、ログイン成功後
const { data: { user } } = await supabase.auth.getUser()
if (user) {
// ユーザーが public.users に存在するかチェック
const { data: existingUser } = await supabase
.from('users')
.select('id')
.eq('id', user.id)
.single()
if (!existingUser) {
// ログイン方法を判定
const authType = user.app_metadata?.provider || 'oauth'
// ユーザーレコードを作成
await supabase.from('users').insert({
id: user.id,
email: user.email,
auth_type: authType.startsWith('sso') ? 'sso' : 'oauth',
sso_provider_id: user.app_metadata?.sso_provider_id,
tenant_id: null, // 後で管理者が割り当て
full_name: user.user_metadata?.full_name,
avatar_url: user.user_metadata?.avatar_url
})
}
}
統一された RLS Policy
ビジネステーブルには統一された RLS Policy が必要で、OAuth ユーザーと SSO ユーザーの両方を処理します:
-- projects テーブルがあると仮定
CREATE TABLE public.projects (
id UUID PRIMARY KEY,
tenant_id UUID REFERENCES tenants(id),
name TEXT NOT NULL,
is_public BOOLEAN DEFAULT false,
created_by UUID REFERENCES users(id)
);
ALTER TABLE public.projects ENABLE ROW LEVEL SECURITY;
-- グローバル分離 Policy(RESTRICTIVE)
CREATE POLICY "Tenant or public access" ON public.projects
AS RESTRICTIVE TO authenticated
USING (
-- SSO ユーザー:tenant_id が一致する必要がある
(auth.jwt()->>'auth_type' = 'sso'
AND tenant_id = (auth.jwt()->>'tenant_id')::uuid)
OR
-- OAuth ユーザー:公開プロジェクトのみ閲覧可能
(auth.jwt()->>'auth_type' = 'oauth' AND is_public = true)
);
-- 閲覧を許可(PERMISSIVE)
CREATE POLICY "Users can view" ON public.projects
FOR SELECT TO authenticated
USING (true);
-- 作成を許可(テナントユーザーのみ作成可能)
CREATE POLICY "Tenant users can create" ON public.projects
FOR INSERT TO authenticated
WITH CHECK (
auth.jwt()->>'auth_type' = 'sso'
AND tenant_id = (auth.jwt()->>'tenant_id')::uuid
);
-- 編集を許可(作成者または管理者)
CREATE POLICY "Creators or admins can edit" ON public.projects
FOR UPDATE TO authenticated
USING (
created_by = auth.uid()
OR authorize('projects.edit')
);
この Policy セットのロジック:
- SSO ユーザーは自分のテナントのプロジェクトのみ閲覧可能
- OAuth ユーザーは公開プロジェクトのみ閲覧可能
- SSO ユーザーのみプロジェクトを作成可能(テナントに属する必要がある)
- 編集権限:作成者または
projects.edit権限を持つ管理者
企業クライアントの管理者がテナントを割り当て
SSO ユーザーはログイン直後、tenant_id が null です。企業管理者が手動で割り当てる必要があります:
-- ユーザーをテナントに追加
UPDATE public.users
SET tenant_id = '<テナントUUID>'
WHERE id = '<ユーザーUUID>';
-- ユーザーロールを設定
INSERT INTO public.user_roles (user_id, role)
VALUES ('<ユーザーUUID>', 'admin');
このプロセスは管理画面を作成するか、Auth Hooks で自動処理(SSO provider に基づいてテナントに匿名マッピング)できます。
完全なフロー図
ユーザーログイン
│
├─ OAuth ユーザー
│ │
│ ├─ Google/GitHub コールバック
│ ├─ Supabase が auth.users を作成
│ ├─ ビジネスコードが public.users を作成 (auth_type='oauth')
│ └─ JWT: { auth_type: 'oauth' }
│ │
│ └─ RLS: is_public=true のデータのみ閲覧可能
│
└─ SSO ユーザー
│
├─ Okta/Azure AD SAML コールバック
├─ Supabase が auth.users を作成 (sso_provider_id 付き)
├─ ビジネスコードが public.users を作成 (auth_type='sso')
├─ 管理者が tenant_id を割り当て
├─ Custom Hook が tenant_id を JWT に追加
└─ JWT: { auth_type: 'sso', tenant_id: '...' }
│
└─ RLS: tenant_id が一致するデータのみ閲覧可能
Supabase Auth エンタープライズ級設定の完全フロー
OAuth ソーシャルログインから SAML SSO 企業統合、RLS マルチテナント権限分離までの完全な設定手順
⏱️ 目安時間: 2 時間
- 1
ステップ1: OAuth プロバイダーを設定(Google/GitHub/Apple)
1. プロバイダーコンソールで OAuth App を作成(Google Cloud Console / GitHub Settings)
2. コールバック URL を設定:https://<プロジェクトref>.supabase.co/auth/v1/callback
3. Supabase Dashboard でプロバイダーを有効化し、Client ID と Secret を入力
4. コードから signInWithOAuth() を呼び出し、スコープと redirectTo を指定 - 2
ステップ2: SAML SSO 企業統合を設定
1. 企業 IdP(Okta/Azure AD)から Metadata ファイルまたは URL を取得
2. CLI で SSO 接続を追加:supabase sso add --type saml --domains company.com
3. Attribute Mapping を設定し、email、name などのフィールドをマッピング
4. ログインフローをテスト:ユーザーがメールを入力すると自動的に IdP にリダイレクト - 3
ステップ3: RLS マルチテナント分離を実装
1. ビジネステーブルに tenant_id フィールドを追加
2. RLS を有効化:ALTER TABLE projects ENABLE ROW LEVEL SECURITY
3. RESTRICTIVE ポリシーを作成し、グローバルテナントフィルタリングを実装
4. PERMISSIVE ポリシーを作成し、具体的な操作権限を制御(SELECT/INSERT/UPDATE)
5. tenant_id フィールドにインデックスを追加し、パフォーマンスを最適化 - 4
ステップ4: Custom Access Token Hook を設定
1. public.custom_access_token_hook() 関数を作成
2. 関数内で tenant_id、user_role などのカスタムクレームを追加
3. supabase_auth_admin に関数の実行権限を付与
4. Supabase Dashboard で Hook を有効化(Authentication > Hooks)
5. フロントエンドで jwtDecode() でカスタムクレームを読み取り - 5
ステップ5: RBAC 権限制御を実装
1. user_roles と role_permissions テーブルを作成
2. app_role と app_permission 列挙型を定義
3. authorize() 関数を作成し、ユーザー権限をチェック
4. RLS Policy 内で authorize('permission.name') を呼び出し
5. フロントエンドで JWT 内の user_role に基づいて UI 要素を表示/非表示
FAQ
OAuth と SAML SSO の違いは何ですか?どちらを選ぶべきですか?
B2B SaaS を開発する場合、SSO は必須機能です。個人向け製品であれば、OAuth で十分です。
RLS を有効にした後、クエリが空の結果を返します。どうすればいいですか?
RLS Policy のパフォーマンスが悪い場合、どう最適化すればいいですか?
• Policy 内のフィールドにインデックスを作成:CREATE INDEX idx_tenant ON projects(tenant_id);
• Policy 内のサブクエリを回避:Custom Access Token Hook で tenant_id を JWT に入れ、auth.jwt()->>'tenant_id' で直接抽出
• SECURITY DEFINER 関数で複雑なロジックをカプセル化、PostgreSQL は1回のみ実行するように最適化
JWT にカスタムフィールド(tenant_id、user_role など)を保存するにはどうすればいいですか?
1. PL/pgSQL 関数 custom_access_token_hook(event jsonb) を作成
2. 関数内で jsonb_set() を使ってカスタムクレームを追加
3. Supabase Dashboard で Hook を有効化(Authentication > Hooks > Custom Access Token)
4. フロントエンドで jwtDecode() で JWT 内のカスタムクレームを読み取り
マルチテナント SaaS で企業ユーザーと個人ユーザーのデータ分離を実現するにはどうすればいいですか?
• SSO ユーザー:tenant_id が一致するテナントデータのみ閲覧可能
• OAuth ユーザー:is_public=true の公開データのみ閲覧可能
RESTRICTIVE ポリシーをグローバルフィルターとして使用し、PERMISSIVE ポリシーで具体的な操作を制御します。
Supabase は Apple Sign In に対応していますか?設定は複雑ですか?
1. Apple Developer Portal で Services ID を作成
2. 秘密鍵を生成(.p8 ファイル、1回のみダウンロード可能)
3. Supabase Dashboard で Team ID、Key ID、Services ID、秘密鍵の内容を入力
iOS アプリではネイティブの Sign in with Apple API を使用し、identity_token を取得して Supabase の signInWithIdTokenCredentials() に渡すことができます。
企業クライアントが SSO を要求していますが、彼らの IdP タイプがわかりません。どうすればいいですか?
OAuth ソーシャルログインから始まり、SAML SSO 企業統合へと進み、最後に RLS と Custom Claims で完全な権限体系を構築しました。このソリューションは、個人製品から企業 SaaS まで、様々なシナリオをカバーできます。
いくつかの重要な判断ポイント:
ユーザーが主に個人消費者の場合:OAuth(Google/GitHub)で十分です。設定が簡単で、ユーザーにとって馴染みがあり、メンテナンスコストが低いです。RLS の基本バージョン——ユーザーは自分のデータにのみアクセス可能——で十分です。
B2B SaaS を開発している場合:SSO は必須機能です。企業クライアントが要求し、なければ直接却下される可能性があります。Okta/Azure AD/Google Workspace の統合を準備し、マルチテナントアーキテクチャを予め用意しておきましょう。
複雑な企業アプリケーションを開発している場合:RBAC + RLS の組み合わせが標準ソリューションです。ロール権限テーブル、Custom Claims、authorize() 関数——この組み合わせで「管理者は削除できるがピン留めできない」「テナントメンバーは編集できるが削除できない」などのきめ細かい権限要件に対応できます。
次のステップのおすすめ:まず OAuth から始め、基本フローを確立しましょう。プロジェクトが成熟し、企業クライアントのニーズが出てきたら、SSO と RBAC を追加します。Supabase のアーキテクチャは段階的なアップグレードをサポートしているため、最初からすべての機能を設定する必要はありません。
8 min read · 公開日: 2026年4月21日 · 更新日: 2026年4月25日
Supabase 実践ガイド
検索からこのページに来た場合は、前後の記事もあわせて読むと同じテーマの理解がかなり早く深まります。
前の記事
Supabase Edge Functions 実践ガイド:Deno ランタイムと TypeScript 開発入門
Supabase Edge Functions 開発を深く学ぶ実践ガイド:Deno ランタイムアーキテクチャと V8 isolate の仕組みを理解し、CLI コマンドの流れをマスターし、Hono フレームワークで RESTful API を構築。ローカルデバッグから本番デプロイまで完全解説
第 5 / 8 記事
次の記事
Supabase Storage 実践ガイド:ファイルアップロード、CDN、アクセス制御
Supabase Storage の完全実践ガイド:3つのアクセス制御モードの比較、TUS 分割アップロード、Smart CDN 最適化テクニック、R2/S3 との価格比較分析。React コード例とトラブルシューティングを含みます。
第 7 / 8 記事
関連記事
Supabase 入門ガイド:PostgreSQL + Auth + Storage でオールインワン バックエンド
Supabase 入門ガイド:PostgreSQL + Auth + Storage でオールインワン バックエンド
Supabase データベース設計:テーブル構造、リレーションとRLS完全ガイド
Supabase データベース設計:テーブル構造、リレーションとRLS完全ガイド
Supabase Auth 実践ガイド:メール認証、OAuth、セッション管理

コメント
GitHubアカウントでログインしてコメントできます