Database

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:


_10
{
_10
"user_role": "admin",
_10
"plan": "TRIAL",
_10
"user_level": 100,
_10
"group_name": "Super Guild!",
_10
"joined_on": "2022-05-20T14:28:18.217Z",
_10
"group_manager": false,
_10
"items": ["toothpick", "string", "ring"]
_10
}

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.
supabase/migrations/init.sql

_21
-- Custom types
_21
create type public.app_permission as enum ('channels.delete', 'messages.delete');
_21
create type public.app_role as enum ('admin', 'moderator');
_21
_21
-- USER ROLES
_21
create table public.user_roles (
_21
id bigint generated by default as identity primary key,
_21
user_id uuid references public.users on delete cascade not null,
_21
role app_role not null,
_21
unique (user_id, role)
_21
);
_21
comment on table public.user_roles is 'Application roles for each user.';
_21
_21
-- ROLE PERMISSIONS
_21
create table public.role_permissions (
_21
id bigint generated by default as identity primary key,
_21
role app_role not null,
_21
permission app_permission not null,
_21
unique (role, permission)
_21
);
_21
comment on table public.role_permissions is 'Application permissions for each role.';

You can now manage your roles and permissions in SQL. For example, to add the mentioned roles and permissions from above, run:

supabase/seed.sql

_10
insert into public.role_permissions (role, permission)
_10
values
_10
('admin', 'channels.delete'),
_10
('admin', 'messages.delete'),
_10
('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 edit the JWT.

supabase/migrations/auth_hook.sql

_52
-- Create the auth hook function
_52
create or replace function public.custom_access_token_hook(event jsonb)
_52
returns jsonb
_52
language plpgsql
_52
stable
_52
as $$
_52
declare
_52
claims jsonb;
_52
user_role public.app_role;
_52
begin
_52
-- Fetch the user role in the user_roles table
_52
select role into user_role from public.user_roles where user_id = (event->>'user_id')::uuid;
_52
_52
claims := event->'claims';
_52
_52
if user_role is not null then
_52
-- Set the claim
_52
claims := jsonb_set(claims, '{user_role}', to_jsonb(user_role));
_52
else
_52
claims := jsonb_set(claims, '{user_role}', 'null');
_52
end if;
_52
_52
-- Update the 'claims' object in the original event
_52
event := jsonb_set(event, '{claims}', claims);
_52
_52
-- Return the modified or original event
_52
return event;
_52
end;
_52
$$;
_52
_52
grant usage on schema public to supabase_auth_admin;
_52
_52
grant execute
_52
on function public.custom_access_token_hook
_52
to supabase_auth_admin;
_52
_52
revoke execute
_52
on function public.custom_access_token_hook
_52
from authenticated, anon, public;
_52
_52
grant all
_52
on table public.user_roles
_52
to supabase_auth_admin;
_52
_52
revoke all
_52
on table public.user_roles
_52
from authenticated, anon, public;
_52
_52
create policy "Allow auth admin to read user roles" ON public.user_roles
_52
as permissive for select
_52
to supabase_auth_admin
_52
using (true)

Enable the hook

In the dashboard, navigate to Authentication > Hooks (Beta) and select the appropriate PostgreSQL function from the dropdown menu.

When developing locally, follow the local development instructions.

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:

supabase/migrations/init.sql

_20
create or replace function public.authorize(
_20
requested_permission app_permission
_20
)
_20
returns boolean as $$
_20
declare
_20
bind_permissions int;
_20
user_role public.app_role;
_20
begin
_20
-- Fetch user role once and store it to reduce number of calls
_20
select (auth.jwt() ->> 'user_role')::public.app_role into user_role;
_20
_20
select count(*)
_20
into bind_permissions
_20
from public.role_permissions
_20
where role_permissions.permission = requested_permission
_20
and role_permissions.role = user_role;
_20
_20
return bind_permissions > 0;
_20
end;
_20
$$ language plpgsql stable security definer set search_path = '';

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:


_10
create policy "Allow authorized delete access" on public.channels for delete using ( (SELECT authorize('channels.delete')) );
_10
create policy "Allow authorized delete access" on public.messages for delete 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:


_10
import { jwtDecode } from 'jwt-decode'
_10
_10
const { subscription: authListener } = supabase.auth.onAuthStateChange(async (event, session) => {
_10
if (session) {
_10
const jwt = jwtDecode(session.access_token)
_10
const userRole = jwt.user_role
_10
}
_10
})

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.

More resources