空雲 Blog

Eye catchsupabase + GraphQL + Next.js で認証とリソースの権限を設定する

publication: 2022/04/18
update:2024/02/20

※ 書きかけです

supabase と GraphQL と認証

supabase はテーブルの構造に合わせて GraphQL スキーマが自動生成され、それを利用してプログラムを組むことができるようになりました。ただしユーザーの作成や認証機能は GraphQL を通しては実装されておらず、その部分だけは RestAPI を用いる必要があります。また、GraphQL がアクセス可能なデータベーススキーマは public のみのため、auth 配下にあるユーザ情報は GraphQL 上では関連付けることができません。これらの問題にうまく対処していく必要があります。

ローカル開発で用意するもの

https://github.com/supabase/cli

ローカル開発で必要となります。

supabase-cli の初期設定

1supabase init
2supabase start

これで supabase が起動できます。supabase-cli を使う場合の注意点があります。外部からのアクセス可能な環境では絶対に使用しないでください。jwt シークレットがsuper-secret-jwt-token-with-at-least-32-characters-longという内容で決め打ちになっているので、外に出したら一瞬でクラックされます。

用意を推奨するファイル

supabase initsupabaseディレクトリが作成されます。その中に用意しておくと良いファイルです。

  • supabase/seed.sql

1drop extension if exists pg_graphql;
2create extension if not exists pg_graphql;
3select graphql.rebuild_schema();

本来初期データを入れるためのファイルですが、マイグレーション後に GraphQL の機能を有効にするために必要です。supabase startsupabase db resetでデータベースを作成した際に、一見 pg_graphql の拡張機能が有効になっているように見えるのですが、再起動しないと使えません。また、graphql.rebuild_schema()はデーブルの構造を変更するたびに必要になります。

最初に用意しておくユーザ管理用マイグレーションファイル

以下、ユーザ管理用のテーブルを作成するためのマイグレーションファイルです

  • supabase/migrations/20220418113839_create_user.sql

1CREATE OR REPLACE FUNCTION public.handle_users_update()
2 RETURNS trigger
3 LANGUAGE 'plpgsql'
4 COST 100
5 VOLATILE NOT LEAKPROOF SECURITY DEFINER
6AS $BODY$
7 begin
8 IF (TG_OP = 'DELETE') THEN
9 delete from public."User" where id=old.id;
10 return old;
11 ELSEIF (TG_OP = 'UPDATE') THEN
12 update public."User"
13 set email=NEW.email,raw_user_meta_data=NEW.raw_user_meta_data where id=old.id;
14 return new;
15 ELSEIF (TG_OP = 'INSERT') THEN
16 insert into public."User"(id, email,raw_user_meta_data) values(NEW.id,NEW.email,NEW.raw_user_meta_data);
17 return new;
18 END IF;
19 return NULL;
20 end;
21$BODY$;
22
23ALTER FUNCTION public.handle_users_update()
24 OWNER TO postgres;
25
26GRANT EXECUTE ON FUNCTION public.handle_users_update() TO authenticated;
27
28GRANT EXECUTE ON FUNCTION public.handle_users_update() TO postgres;
29
30GRANT EXECUTE ON FUNCTION public.handle_users_update() TO PUBLIC;
31
32GRANT EXECUTE ON FUNCTION public.handle_users_update() TO anon;
33
34GRANT EXECUTE ON FUNCTION public.handle_users_update() TO service_role;
35
36CREATE TABLE IF NOT EXISTS public."User"
37(
38 id uuid NOT NULL,
39 email character varying(255) COLLATE pg_catalog."default",
40 raw_user_meta_data text,
41 CONSTRAINT "User_pkey" PRIMARY KEY (id)
42)
43
44TABLESPACE pg_default;
45
46ALTER TABLE IF EXISTS public."User"
47 OWNER to postgres;
48
49ALTER TABLE IF EXISTS public."User"
50 ENABLE ROW LEVEL SECURITY;
51
52GRANT ALL ON TABLE public."User" TO anon;
53
54GRANT ALL ON TABLE public."User" TO authenticated;
55
56GRANT ALL ON TABLE public."User" TO postgres;
57
58GRANT ALL ON TABLE public."User" TO service_role;
59CREATE POLICY "Enable access to all users"
60 ON public."User"
61 AS PERMISSIVE
62 FOR SELECT
63 TO public
64 USING (true);
65
66CREATE trigger on_auth_user_update
67 AFTER INSERT OR UPDATE OR DELETE ON auth.users
68 for each row execute procedure public.handle_users_update();

auth.usersへの操作をフックして、public.Userに必要なデータを書き込みます。こうしておくと、GraphQL のスキーマーがユーザ情報へアクセスできるようになります。

Todo アプリ用のテーブル

タイトル、説明、日時、非公開属性、ユーザ情報を記憶します。ユーザ情報は public.User にリレーションを張ります。auth.users では無いので注意してください。

1CREATE TABLE IF NOT EXISTS public."Todo"
2(
3 id bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 9223372036854775807 CACHE 1 ),
4 created_at timestamp with time zone DEFAULT now(),
5 user_id uuid NOT NULL DEFAULT auth.uid(),
6 title text COLLATE pg_catalog."default",
7 published boolean NOT NULL DEFAULT false,
8 description text COLLATE pg_catalog."default",
9 CONSTRAINT "Todo_pkey" PRIMARY KEY (id),
10 CONSTRAINT "Todo_user_id_fkey" FOREIGN KEY (user_id)
11 REFERENCES public."User" (id) MATCH SIMPLE
12 ON UPDATE NO ACTION
13 ON DELETE NO ACTION
14)
15
16TABLESPACE pg_default;
17
18ALTER TABLE IF EXISTS public."Todo"
19 OWNER to postgres;
20
21ALTER TABLE IF EXISTS public."Todo"
22 ENABLE ROW LEVEL SECURITY;
23
24GRANT ALL ON TABLE public."Todo" TO anon;
25
26GRANT ALL ON TABLE public."Todo" TO authenticated;
27
28GRANT ALL ON TABLE public."Todo" TO postgres;
29
30GRANT ALL ON TABLE public."Todo" TO service_role;
31CREATE POLICY "Enable access to all users"
32 ON public."Todo"
33 AS PERMISSIVE
34 FOR SELECT
35 TO public
36 USING (published or auth.uid() = user_id);
37CREATE POLICY "Enable INSERT for authenticated users only"
38 ON public."Todo"
39 AS PERMISSIVE
40 FOR INSERT
41 TO public
42 WITH CHECK ((auth.role() = 'authenticated'::text) and auth.uid() = user_id);
43CREATE POLICY "Enable DELETE/UPDATE for users based on user_id"
44 ON public."Todo"
45 AS PERMISSIVE
46 FOR ALL
47 TO public
48 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

1NEXT_PUBLIC_SUPABASE_URL=http://localhost:54321
2NEXT_PUBLIC_SUPABASE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24ifQ.625_WdcF3KHqz5amU0x2X5WWHP-OEs_4qj0ssLNHzTs
3SUPABASE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSJ9.vI9obAHOGyVVKa3pD--kJlyxp-Z2zV9UUMAhKpNLAcU

  • bin/create-user.ts

1import { createClient } from "@supabase/supabase-js";
2import { config } from "dotenv";
3
4const { parsed } = config({ path: ".env.local" });
5
6const endpoint = parsed?.NEXT_PUBLIC_SUPABASE_URL;
7const key = parsed?.SUPABASE_KEY;
8
9const createUser = async ({
10 email,
11 password,
12}: {
13 email: string;
14 password: string;
15}) => {
16 const supabase = createClient(endpoint!, key!);
17 const result = await supabase.auth.api.createUser({
18 email,
19 password,
20 email_confirm: true,
21 user_metadata: { name: email },
22 });
23 return result;
24};
25
26(async () => {
27 if (!endpoint || !key || process.argv.length < 4) {
28 console.log("create-user [email] [password]");
29 } else {
30 let result;
31 for (let i = 0; i < 3; i++) {
32 result = await createUser({
33 email: process.argv[2],
34 password: process.argv[3],
35 });
36 if (result.error?.status !== 500) break;
37 }
38 console.log(result);
39 }
40})();

  • ユーザ作成
    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を使うことになります。