서비스를 개발할때, 필요한 기능 중에 사용자별로 접근가능한 데이터를 제한 하거나, 기능을 제한 하는 것이 필요할 경우가 있습니다.
이럴 경우 보통, 사용자가 로그인 하고 나서 사용자의 권한 정보를 서버에 쿼리하고 사용자의 권한에 따라 기능을 제한 하는 형태로 진행하도록 할 수 있죠.
보통 사용자 정보에 록(역할, role, permission) 을 두고 처리하는 경우가 많습니다.
어떻게 그럼 서비스에서는 사용자의 role을 확인할 까요?
대략적으로 생각해볼 수 있는 방법이 2가지 정도 인것 같네요.
1. 사용자가 db나 api를 이용할때 해당 api, 또는 db query에서 사용자 정보 table을 참조하여 권한이 있는 사용자인지 확인하는 방법
2. 사용자의 access token에 권한 정보를 추가하여, db, api에서 token 정보를 decoding 하여 권한 정보를 얻는 방법
1번은 매번 DB access 마다 확인을 해야 하는 사항이라 서버에 부하와 성능에 영향을 미치게 됩니다.
2번은 성능에 대한 오버해드가 거의 없습니다. 하지만 사용자의 정보가 변경되었을때 token에 바로 반영되는 것이 아니기 때문에 이를 위한 처리가 필요할 것입니다. (아니면 다음 로그인 까지 무시 하거나.)
오늘 정리할 내용은 성능 이슈도 이슈지이지만, access tokend 다루는 것에 중심을 두고 싶어서 2번에 대한 내용을 정리했습니다.
Supabase 에서 Auth Hooking 을 통해서 token 에 추가정보를 입력하기
Supabase 에 Beta로 현재 Auth Hooking을 지원하고 있고, postgres 에서는 RLS (Row Level Security)를 지원합니다.
이를 활용하여 서버쪽에서 auth정보를 이용해서 접근 권한을 설정할 수 있는 방법이 생겼습니다.
이런 처리를 통해 기대한 Access Token의 전체적인 모습은 다음과 같습니다.
Access Token
Token을 디코딩 결과
{
"aal": "aal1",
"access": "user",
"amr": [
{
"method": "password",
"timestamp": 1738558038
}
],
"app_metadata": {
"provider": "email",
"providers": [
"email"
]
},
"aud": "authenticated",
"email": "xxxxx@xxxx.xxx",
"exp": 1738633992,
"iat": 1738630392,
"is_anonymous": false,
"iss": "http://127.0.0.1:54321/auth/v1",
"org_depart_id": null,
"org_id": "1",
"phone": "",
"role": "authenticated",
"session_id": "b9313b3b-bd86-44ea-b3d7-98c7ca6b9447",
"sub": "e0804eda-a117-4c51-9f50-66804c889b41",
"user_metadata": {
"email": "xxxxx@xxxx.xxx",
"email_verified": false,
"phone_verified": false,
"sub": "e0804eda-a117-4c51-9f50-66804c889b41",
"username": "name...."
}
}
supabase의 token에 "access", "org_depart_id", "org_id" 를 추가한 형태입니다.
그럼 절차를 확인 해보도록 하시죠.
DB의 policies 를 살펴보면, 사용자에 따라 접근권한을 설정할때, auth.uid() 같은 함수를 이용해서 현 사용자만 접근 가능하도록 할 수 있죠.
그런데 만약 같은 팀원만 볼수 있게 하고 싶다면, 음.. 일단 RLS에서 team에 대한 table join 해서 팀에 소속되어있는지 확인해야 합니다.
(예제에도 그렇게 되어있죠)
그런데 table join이 들어간 코드와 아닌코드의 성능 차이기 약 99% 차이가 난다고 예제에 나와있어요.
table join이 빠지면 99% 성능 향상 ..
그런데 만약 auth token에 추가정보를 담고, RLS에서 auth token에 있는 정보를 활용해서 필터링 할 수 있다고 한다면, (지금 시도해보고 있음), ㅅa
table join없이 처리가 가능합니다. 즉 향상된 성능으로 처리할 수 있다는 것이죠.
0. custom_jwt_token_hook 추가하기
- supabase 에서 auth hook을 추가합니다.
Authentication -> Hooks 를 선택 -> Add Hook
이런 화면이 나오죠.
- DB는 postgres 선택하고, schema를 선택해야 하는데 이때 public으로 해도 되지만, 저는 따로 private schema를 만들어두어서 그걸 이용했습니다. (public은 expose되어있어서 불안한 부분이 있거든요)
- 그리고 function을 선택해야 합니다. (지금 구현된 것이 없기 때문에 만들어야 겠죠).
- custom_jwt_token_hook 구현
- private schema 를 만들어서 추가 (public schema는 노출 되어있기 때문에 피하는 것이 좋다. )
- javascript 가 plpgsql보다 익숙하여 plv8 import하여 javacript로 작성하였습니다.
//함수 명: custom_jwt_token_hook
var org_id, access, org_depart_id;
// -- Fetch the current user's level from the profiles table
var result = plv8.execute("select id, org_id, access, org_depart_id from public.user_profiles where id = $1", [event.user_id]);
if (result.length > 0) {
org_id = result[0].org_id;
org_depart_id = result[0].org_depart_id;
access = result[0].access;
} else {
org_id = 0;
org_depart_id = 0;
access = '';
}
// -- Check if 'claims' exists in the event object; if not, initialize it
if (!event.claims) {
event.claims = {};
}
// -- Update the level in the claims
event.claims.org_id = org_id;
event.claims.org_depart_id = org_depart_id;
event.claims.access = access;
return event;
- user_profiles table을 만들고 이 테이블에 org_id, org_depart_id를 담고 있다.
SQL Editor에서 작성하는것이 더 편리합니다.
create or replace function custom_jwt_token_hook(event jsonb)
returns jsonb
language plv8
as $$
var org_id, role, org_depart_id;
// -- Fetch the current user's level from the profiles table
var result = plv8.execute("select org_id, role, org_depart_id from public.user_profiles where user_id = $1", [event.user_id]);
if (result.length > 0) {
org_id = result[0].org_id;
org_depart_id = result[0].org_depart_id;
role = result[0].role;
} else {
org_id = 0;
org_depart_id = 0;
role = '';
}
// -- Check if 'claims' exists in the event object; if not, initialize it
if (!event.claims) {
event.claims = {};
}
// -- Update the level in the claims
event.claims.org_id = org_id;
event.claims.org_depart_id = org_depart_id;
event.claims.role = role;
return event;
$$;
grant all
on table public.user_profiles
to supabase_auth_admin;
revoke all
on table public.user_profiles
from authenticated, anon, public;
1. custom jwt를 통해서 token에 추가정보가 들어간것 확인했습니다.
=> 정상적으로 설정이 완료 되면 token에 정보가 들어간 것을 볼 수 있다.
주의 점은 user_profiles에 접근 권한이 설정 되어있어야 한다는 것입니다.
supabase 의 auth 로직은 supabase_auth_admin role로 동작하기 때문에, user_profiles 의 select에 authenticated, supabase_auth_admin role이 필요합니다.
2. 서버에서 RLS 로직에서 추가정보를 꺼내올 수 있는지 확인 필요.
=> 확인 중인 사항 (확인 완료!)
아래처럼 org_id 값을 비교하고 싶을때, org_id가 int8 (숫자)인경우라면, 숫자를 문자로 변경해서 비교 해야함.
ALTER POLICY "Enable read access for same org"
ON "public"."devices"
TO authenticated
USING (
(auth.jwt() ->> 'org_id')::text = org_id::text
);
3.테스트를 위해서 local pc에서 custom jwt 를 테스트 해 볼 수 있는 환경 구축..
가이드에 이렇게 적혀있음
# This hook runs before a token is issued and allows you to add additional claims based on the authentication method used.
# [auth.hook.custom_access_token]
# enabled = true
# uri = "pg-functions://<database>/<schema>/<hook_name>"
config.toml 에 다음 과 같이 내용 추가.
[auth.hook.custom_access_token]
enabled = true
uri = "pg-functions://postgres/private/custom_jwt_token_hook"
=> 정상 동작 하는것 확인
이것을 시도 해보겠습니다. !!
!해피 코딩
[참고사항]
코드를 작성하고 돌려볼때 user_profiles의 권한 관련된 문제가 발생할 수도 있습니다.
저는 migration하다 문제가 생겨서 골치 아팠었는데요.
1. private/custom_jwt_token_hook을 못찾는 문제가 있었습니다.
환경 문제일 수도 있고, 제가 이것 저것 건드려서 생긴 문제일 수도 있습니다.
ALTER FUNCTION private.custom_jwt_token_hook SECURITY DEFINER;
// 원래 invoker로 설정 되어있어도 동작 했던것 같은데,
// SECURITY DEFINER 로 설정을 바꿨습니다.
GRANT EXECUTE ON FUNCTION private.custom_jwt_token_hook TO authenticator;
//권한 설정 auth logic이 authenticator로 동작하는데 함수에 대해서 도 추가로 설정
// authenticator 역할이 실행 권한을 설정
2. user_profiles 접근 권한 문제 발생 했습니다.
RLS 설정을 하여 문제가 없어보이는 상황(제 짤은 생각으로는.. 그렇습니다.^^;;)이었으나 이상하게 접근 권한에 문제가 계속 생겨서 SQL Editor에서 다음과 같이 권한을 줬습니다. 그 이후 기대했던대로 동작 했습니다.
GRANT SELECT ON TABLE public.user_profiles TO authenticated;
3. View 문제
View는 RLS 를 설정 할 수 없습니다. 이걸로 좌절하고 주말을 보냈는데, 찾다보니 방법이 있더군요.
ALTER VIEW my_view SET (security_invoker = on);
이렇게 설정을 하면 my_view 에서 query하는 table에 RLS가 적용 됩니다.
다만 이렇게 했을때 문제는 my_view 가 접근하는 모든 table에 RLS가 잘 적용 되어있어야 동작 한다는 것입니다.
a table은 접근 권한이 있고 b table은 접근 권한이 없으면 퍼미션 에러가 발생합니다.
(이건 시스템을 어떻게 설계하느냐의 문제이기 때문에 이런 부분 고려해서 설계하면 좋겠네요)
[참고] Supabase Custom Access Token Hook
Custom Access Token Hook | Supabase Docs
Customize the access token issued by Supabase Auth
supabase.com
'Supabase' 카테고리의 다른 글
[supabase] postgres plv8 활성화 (0) | 2025.01.24 |
---|---|
[Supabase] Edge function 만들기 (0) | 2024.12.11 |
[Flutter] supabase database 의 json 내부 query하기 (0) | 2024.09.19 |