apps/docs/content/guides/api/custom-claims-and-role-based-access-control-rbac.mdx
Custom Claims are special attributes attached to a user that you can use to control access to portions of your application. For example:
{
"user_role": "admin",
"plan": "TRIAL",
"user_level": 100,
"group_name": "Super Guild!",
"joined_on": "2022-05-20T14:28:18.217Z",
"group_manager": false,
"items": ["toothpick", "string", "ring"]
}
To implement Role-Based Access Control (RBAC) with custom claims, use a Custom Access Token Auth Hook. This hook runs before a token is issued. You can use it to add additional claims to the user's JWT.
This guide uses the Slack Clone example to demonstrate how to add a user_role claim and use it in your Row Level Security (RLS) policies.
In this example, you will implement two user roles with specific permissions:
moderator: A moderator can delete all messages but not channels.admin: An admin can delete all messages and channels.-- Custom types
create type public.app_permission as enum ('channels.delete', 'messages.delete');
create type public.app_role as enum ('admin', 'moderator');
-- USER ROLES
create table public.user_roles (
id bigint generated by default as identity primary key,
user_id uuid references auth.users on delete cascade not null,
role app_role not null,
unique (user_id, role)
);
comment on table public.user_roles is 'Application roles for each user.';
-- ROLE PERMISSIONS
create table public.role_permissions (
id bigint generated by default as identity primary key,
role app_role not null,
permission app_permission not null,
unique (role, permission)
);
comment on table public.role_permissions is 'Application permissions for each role.';
For the full schema, see the example application on GitHub.
</Admonition>You can now manage your roles and permissions in SQL. For example, to add the mentioned roles and permissions from above, run:
insert into public.role_permissions (role, permission)
values
('admin', 'channels.delete'),
('admin', 'messages.delete'),
('moderator', 'messages.delete');
The Custom Access Token Auth Hook runs before a token is issued. You can use it to edit the JWT.
<Tabs scrollable size="small" type="underlined" defaultActiveId="plpgsql" queryGroup="language"
<TabPanel id="plpgsql" label="PL/pgSQL (best performance)">
-- Create the auth hook function
create or replace function public.custom_access_token_hook(event jsonb)
returns jsonb
language plpgsql
stable
as $$
declare
claims jsonb;
user_role public.app_role;
begin
-- Fetch the user role in the user_roles table
select role into user_role from public.user_roles where user_id = (event->>'user_id')::uuid;
claims := event->'claims';
if user_role is not null then
-- Set the claim
claims := jsonb_set(claims, '{user_role}', to_jsonb(user_role));
else
claims := jsonb_set(claims, '{user_role}', 'null');
end if;
-- Update the 'claims' object in the original event
event := jsonb_set(event, '{claims}', claims);
-- Return the modified or original event
return event;
end;
$$;
grant usage on schema public to supabase_auth_admin;
grant execute
on function public.custom_access_token_hook
to supabase_auth_admin;
revoke execute
on function public.custom_access_token_hook
from authenticated, anon, public;
grant all
on table public.user_roles
to supabase_auth_admin;
revoke all
on table public.user_roles
from authenticated, anon, public;
create policy "Allow auth admin to read user roles" ON public.user_roles
as permissive for select
to supabase_auth_admin
using (true);
In the dashboard, navigate to Authentication > Hooks (Beta) and select the appropriate Postgres function from the dropdown menu.
When developing locally, follow the local development instructions.
<Admonition type="note">To learn more about Auth Hooks, see the Auth Hooks docs.
</Admonition>To utilize Role-Based Access Control (RBAC) in Row Level Security (RLS) policies, create an authorize method that reads the user's role from their JWT and checks the role's permissions:
create or replace function public.authorize(
requested_permission app_permission
)
returns boolean as $$
declare
bind_permissions int;
user_role public.app_role;
begin
-- Fetch user role once and store it to reduce number of calls
select (auth.jwt() ->> 'user_role')::public.app_role into user_role;
select count(*)
into bind_permissions
from public.role_permissions
where role_permissions.permission = requested_permission
and role_permissions.role = user_role;
return bind_permissions > 0;
end;
$$ language plpgsql stable security definer set search_path = '';
You can read more about using functions in RLS policies in the RLS guide.
</Admonition>You can then use the authorize method within your RLS policies. For example, to enable the desired delete access, you would add the following policies:
create policy "Allow authorized delete access" on public.channels for delete to authenticated using ( (SELECT authorize('channels.delete')) );
create policy "Allow authorized delete access" on public.messages for delete to authenticated using ( (SELECT authorize('messages.delete')) );
The auth hook will only modify the access token JWT but not the auth response. Therefore, to access the custom claims in your application, e.g. your browser client, or server-side middleware, you will need to decode the access_token JWT on the auth session.
In a JavaScript client application you can for example use the jwt-decode package:
import { jwtDecode } from 'jwt-decode'
const { subscription: authListener } = supabase.auth.onAuthStateChange(async (event, session) => {
if (session) {
const jwt = jwtDecode(session.access_token)
const userRole = jwt.user_role
}
})
For server-side logic you can use packages like express-jwt, koa-jwt, PyJWT, dart_jsonwebtoken, Microsoft.AspNetCore.Authentication.JwtBearer, etc.
You now have a robust system in place to manage user roles and permissions within your database that automatically propagates to Supabase Auth.