Custom Claims & Role-based Access Control (RBAC)
Custom Claims are special attributes attached to a user that you can use to control access to portions of your application. For example:
1{2 "user_role": "admin",3 "plan": "TRIAL",4 "user_level": 100,5 "group_name": "Super Guild!",6 "joined_on": "2022-05-20T14:28:18.217Z",7 "group_manager": false,8 "items": ["toothpick", "string", "ring"]9}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.
Create a table to track user roles and permissions
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.
1-- Custom types2create type public.app_permission as enum ('channels.delete', 'messages.delete');3create type public.app_role as enum ('admin', 'moderator');45-- USER ROLES6create table public.user_roles (7 id bigint generated by default as identity primary key,8 user_id uuid references auth.users on delete cascade not null,9 role app_role not null,10 unique (user_id, role)11);12comment on table public.user_roles is 'Application roles for each user.';1314-- ROLE PERMISSIONS15create table public.role_permissions (16 id bigint generated by default as identity primary key,17 role app_role not null,18 permission app_permission not null,19 unique (role, permission)20);21comment on table public.role_permissions is 'Application permissions for each role.';For the full schema, see the example application on GitHub.
You can now manage your roles and permissions in SQL. For example, to add the mentioned roles and permissions from above, run:
1insert into public.role_permissions (role, permission)2values3 ('admin', 'channels.delete'),4 ('admin', 'messages.delete'),5 ('moderator', 'messages.delete');Create Auth Hook to apply user role
The Custom Access Token Auth Hook runs before a token is issued. You can use it to edit the JWT.
1-- Create the auth hook function2create or replace function public.custom_access_token_hook(event jsonb)3returns jsonb4language plpgsql5stable6as $$7 declare8 claims jsonb;9 user_role public.app_role;10 begin11 -- Fetch the user role in the user_roles table12 select role into user_role from public.user_roles where user_id = (event->>'user_id')::uuid;1314 claims := event->'claims';1516 if user_role is not null then17 -- Set the claim18 claims := jsonb_set(claims, '{user_role}', to_jsonb(user_role));19 else20 claims := jsonb_set(claims, '{user_role}', 'null');21 end if;2223 -- Update the 'claims' object in the original event24 event := jsonb_set(event, '{claims}', claims);2526 -- Return the modified or original event27 return event;28 end;29$$;3031grant usage on schema public to supabase_auth_admin;3233grant execute34 on function public.custom_access_token_hook35 to supabase_auth_admin;3637revoke execute38 on function public.custom_access_token_hook39 from authenticated, anon, public;4041grant all42 on table public.user_roles43to supabase_auth_admin;4445revoke all46 on table public.user_roles47 from authenticated, anon, public;4849create policy "Allow auth admin to read user roles" ON public.user_roles50as permissive for select51to supabase_auth_admin52using (true)Enable the hook
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.
To learn more about Auth Hooks, see the Auth Hooks docs.
Accessing custom claims in RLS policies
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:
1create or replace function public.authorize(2 requested_permission app_permission3)4returns boolean as $$5declare6 bind_permissions int;7 user_role public.app_role;8begin9 -- Fetch user role once and store it to reduce number of calls10 select (auth.jwt() ->> 'user_role')::public.app_role into user_role;1112 select count(*)13 into bind_permissions14 from public.role_permissions15 where role_permissions.permission = requested_permission16 and role_permissions.role = user_role;1718 return bind_permissions > 0;19end;20$$ language plpgsql stable security definer set search_path = '';You can read more about using functions in RLS policies in the RLS guide.
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:
1create policy "Allow authorized delete access" on public.channels for delete to authenticated using ( (SELECT authorize('channels.delete')) );2create policy "Allow authorized delete access" on public.messages for delete to authenticated using ( (SELECT authorize('messages.delete')) );Accessing custom claims in your application
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:
1import { jwtDecode } from 'jwt-decode'23const { subscription: authListener } = supabase.auth.onAuthStateChange(async (event, session) => {4 if (session) {5 const jwt = jwtDecode(session.access_token)6 const userRole = jwt.user_role7 }8})For server-side logic you can use packages like express-jwt, koa-jwt, PyJWT, dart_jsonwebtoken, Microsoft.AspNetCore.Authentication.JwtBearer, etc.
Conclusion
You now have a robust system in place to manage user roles and permissions within your database that automatically propagates to Supabase Auth.