supabase + GraphQL + Next.js で認証とリソースの権限を設定する
※ 書きかけです
supabase と GraphQL と認証
supabase はテーブルの構造に合わせて GraphQL スキーマが自動生成され、それを利用してプログラムを組むことができるようになりました。ただしユーザーの作成や認証機能は GraphQL を通しては実装されておらず、その部分だけは RestAPI を用いる必要があります。また、GraphQL がアクセス可能なデータベーススキーマは public のみのため、auth 配下にあるユーザ情報は GraphQL 上では関連付けることができません。これらの問題にうまく対処していく必要があります。
ローカル開発で用意するもの
https://github.com/supabase/cli
ローカル開発で必要となります。
supabase-cli の初期設定
supabase init
supabase startこれで supabase が起動できます。supabase-cli を使う場合の注意点があります。外部からのアクセス可能な環境では絶対に使用しないでください。jwt シークレットがsuper-secret-jwt-token-with-at-least-32-characters-longという内容で決め打ちになっているので、外に出したら一瞬でクラックされます。
用意を推奨するファイル
supabase initでsupabaseディレクトリが作成されます。その中に用意しておくと良いファイルです。
supabase/seed.sql
drop extension if exists pg_graphql;
create extension if not exists pg_graphql;
select graphql.rebuild_schema();本来初期データを入れるためのファイルですが、マイグレーション後に GraphQL の機能を有効にするために必要です。supabase startやsupabase db resetでデータベースを作成した際に、一見 pg_graphql の拡張機能が有効になっているように見えるのですが、再起動しないと使えません。また、graphql.rebuild_schema()はデーブルの構造を変更するたびに必要になります。
最初に用意しておくユーザ管理用マイグレーションファイル
以下、ユーザ管理用のテーブルを作成するためのマイグレーションファイルです
supabase/migrations/20220418113839_create_user.sql
CREATE OR REPLACE FUNCTION public.handle_users_update()
RETURNS trigger
LANGUAGE 'plpgsql'
COST 100
VOLATILE NOT LEAKPROOF SECURITY DEFINER
AS $BODY$
begin
IF (TG_OP = 'DELETE') THEN
delete from public."User" where id=old.id;
return old;
ELSEIF (TG_OP = 'UPDATE') THEN
update public."User"
set email=NEW.email,raw_user_meta_data=NEW.raw_user_meta_data where id=old.id;
return new;
ELSEIF (TG_OP = 'INSERT') THEN
insert into public."User"(id, email,raw_user_meta_data) values(NEW.id,NEW.email,NEW.raw_user_meta_data);
return new;
END IF;
return NULL;
end;
$BODY$;
ALTER FUNCTION public.handle_users_update()
OWNER TO postgres;
GRANT EXECUTE ON FUNCTION public.handle_users_update() TO authenticated;
GRANT EXECUTE ON FUNCTION public.handle_users_update() TO postgres;
GRANT EXECUTE ON FUNCTION public.handle_users_update() TO PUBLIC;
GRANT EXECUTE ON FUNCTION public.handle_users_update() TO anon;
GRANT EXECUTE ON FUNCTION public.handle_users_update() TO service_role;
CREATE TABLE IF NOT EXISTS public."User"
(
id uuid NOT NULL,
email character varying(255) COLLATE pg_catalog."default",
raw_user_meta_data text,
CONSTRAINT "User_pkey" PRIMARY KEY (id)
)
TABLESPACE pg_default;
ALTER TABLE IF EXISTS public."User"
OWNER to postgres;
ALTER TABLE IF EXISTS public."User"
ENABLE ROW LEVEL SECURITY;
GRANT ALL ON TABLE public."User" TO anon;
GRANT ALL ON TABLE public."User" TO authenticated;
GRANT ALL ON TABLE public."User" TO postgres;
GRANT ALL ON TABLE public."User" TO service_role;
CREATE POLICY "Enable access to all users"
ON public."User"
AS PERMISSIVE
FOR SELECT
TO public
USING (true);
CREATE trigger on_auth_user_update
AFTER INSERT OR UPDATE OR DELETE ON auth.users
for each row execute procedure public.handle_users_update();auth.usersへの操作をフックして、public.Userに必要なデータを書き込みます。こうしておくと、GraphQL のスキーマーがユーザ情報へアクセスできるようになります。
Todo アプリ用のテーブル
タイトル、説明、日時、非公開属性、ユーザ情報を記憶します。ユーザ情報は public.User にリレーションを張ります。auth.users では無いので注意してください。
CREATE TABLE IF NOT EXISTS public."Todo"
(
id bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 9223372036854775807 CACHE 1 ),
created_at timestamp with time zone DEFAULT now(),
user_id uuid NOT NULL DEFAULT auth.uid(),
title text COLLATE pg_catalog."default",
published boolean NOT NULL DEFAULT false,
description text COLLATE pg_catalog."default",
CONSTRAINT "Todo_pkey" PRIMARY KEY (id),
CONSTRAINT "Todo_user_id_fkey" FOREIGN KEY (user_id)
REFERENCES public."User" (id) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE NO ACTION
)
TABLESPACE pg_default;
ALTER TABLE IF EXISTS public."Todo"
OWNER to postgres;
ALTER TABLE IF EXISTS public."Todo"
ENABLE ROW LEVEL SECURITY;
GRANT ALL ON TABLE public."Todo" TO anon;
GRANT ALL ON TABLE public."Todo" TO authenticated;
GRANT ALL ON TABLE public."Todo" TO postgres;
GRANT ALL ON TABLE public."Todo" TO service_role;
CREATE POLICY "Enable access to all users"
ON public."Todo"
AS PERMISSIVE
FOR SELECT
TO public
USING (published or auth.uid() = user_id);
CREATE POLICY "Enable INSERT for authenticated users only"
ON public."Todo"
AS PERMISSIVE
FOR INSERT
TO public
WITH CHECK ((auth.role() = 'authenticated'::text) and auth.uid() = user_id);
CREATE POLICY "Enable DELETE/UPDATE for users based on user_id"
ON public."Todo"
AS PERMISSIVE
FOR ALL
TO public
USING ((auth.uid() = user_id));Todo テーブルはCREATE POLICYで PostgreSQL の RLS(行レベルセキュリティ)を作っています。
Enable access to all users
select の制限で private が設定されている場合は、ユーザーが一致しないとデータを返さないようにしています。この設定を入れておくと非公開データを作ることができますEnable INSERT for authenticated users only
認証ユーザのみ書き込むことができます。また書き込んだユーザが詐称できないように、実際のユーザと書き込まれる id が一致しているか検査していますEnable DELETE/UPDATE for users based on user_id
ユーザが一致した場合のみ、書き換えと削除を許可しています
このあたりの設定は慣れが必要です。
実験用 User の用意
認証に必要なユーザを作成します。cli では用意されていないので自分で作ります。ユーザの作成には service_role の方のキーを使います。テストユーザをさくっと作りたいので招待機能は使いません。
.env.local
NEXT_PUBLIC_SUPABASE_URL=http://localhost:54321
NEXT_PUBLIC_SUPABASE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24ifQ.625_WdcF3KHqz5amU0x2X5WWHP-OEs_4qj0ssLNHzTs
SUPABASE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSJ9.vI9obAHOGyVVKa3pD--kJlyxp-Z2zV9UUMAhKpNLAcUbin/create-user.ts
import { createClient } from "@supabase/supabase-js";
import { config } from "dotenv";
const { parsed } = config({ path: ".env.local" });
const endpoint = parsed?.NEXT_PUBLIC_SUPABASE_URL;
const key = parsed?.SUPABASE_KEY;
const createUser = async ({
email,
password,
}: {
email: string;
password: string;
}) => {
const supabase = createClient(endpoint!, key!);
const result = await supabase.auth.api.createUser({
email,
password,
email_confirm: true,
user_metadata: { name: email },
});
return result;
};
(async () => {
if (!endpoint || !key || process.argv.length < 4) {
console.log("create-user [email] [password]");
} else {
let result;
for (let i = 0; i < 3; i++) {
result = await createUser({
email: process.argv[2],
password: process.argv[3],
});
if (result.error?.status !== 500) break;
}
console.log(result);
}
})();ユーザ作成
yarn ts-node -s bin/create-user a@example.com a
yarn ts-node -s bin/create-user b@example.com a
たまに 500 エラーを返す時があるので 3 回リトライするようにしています。とりあえずテスト用ユーザを二人作っておきます。
作成したユーザの認証と token の受け取りは GraphQL ではできないので RestAPI を直にたたくかsupabase.auth.signInを使うことになります。