DEV Community

Mark Burggraf for Supabase

Posted on • Updated on

 

Supabase Custom Claims

I've put together a [GitHub Repository (https://github.com/supabase-community/supabase-custom-claims) that makes it easy to implement Custom Claims for your application based on Supabase. (Supabase is an open-source backend service that uses PostgreSQL as its database and GoTrue for authentication. It does plenty of other cool things, too, but those items are out of the scope of this article.)

This is just one way to implement custom claims for a Supabase project. The goal here is simply to add JSON data to the JWT access token that an authenticated user receives when logging into your application. That token (and thus the custom claims contained in that token) can be read and used by both your application and by your PostgreSQL database server. These custom claims are stored in the raw_app_meta_data field of the users table in the auth schema. (auth.users.raw_app_meta_data)

In order to make this technique as portable as possible, I've implemented it using a set of PostgreSQL functions, which can be called from any Supabase Client Language that can call a PostgreSQL function. (That covers a lot of programming languages!)

FAQ

What are custom claims?

Custom Claims are special attributes attached to a user that you can use to control access to portions of your application.

For example:

plan: "TRIAL"
user_level: 100
group_name: "Super Guild!"
joined_on: "2022-05-20T14:28:18.217Z"
group_manager: false
items: ["toothpick", "string", "ring"]
Enter fullscreen mode Exit fullscreen mode

What type of data can I store in a custom claim?

Any valid JSON data can be stored in a claim. You can store a string, number, boolean, date (as a string), array, or even a complex, nested, complete JSON object.

Where are these custom claims stored?

Custom claims are stored in the auth.users table, in the raw_app_meta_data column for a user.

Are there any naming restrictions?

The Supabase Auth System (GoTrue) currently uses the following custom claims: provider and providers, so DO NOT use these. Any other valid string should be ok as the name for your custom claim(s), though.

Why use custom claims instead of just creating a table?

Performance, mostly. Custom claims are stored in the security token a user receives when logging in, and these claims are made available to the PostgreSQL database as a configuration parameter, i.e. current_setting('request.jwt.claims', true). So the database has access to these values immediately without needing to do any disk i/o.

This may sound trivial, but this could have a significant effect on scalability if you use claims in an RLS (Row Level Security) Policy, as it could potentially eliminate thousands (or even millions) of database calls.

What are the drawbacks to using custom claims?

One drawback is that claims don't get updated automatically, so if you assign a user a new custom claim, they may need to log out and log back in to have the new claim available to them. The same goes for deleting or changing a claim. So this is not a good tool for storing data that changes frequently.

You can force a refresh of the current session token by calling supabase.auth.update({}) on the client, but if a claim is changed by a server process or by a claims administrator manually, there's no easy way to notify the user that their claims have changed. You can provide a "refresh" button or a refresh function inside your app to update the claims at any time, though.

How can I write a query to find all the users who have a specific custom claim set?

examples

find all users who have claims_admin set to true

select * from auth.users where (auth.users.raw_app_meta_data->'claims_admin')::bool = true;

find all users who have a userlevel over 100

select * from auth.users where (auth.users.raw_app_meta_data->'userleval')::numeric > 100;

find all users whose userrole is set to "MANAGER"

(note for strings you need to add double-quotes becuase data is data is stored as JSONB)
select * from auth.users where (auth.users.raw_app_meta_data->'userrole')::text = '"MANAGER"';

What's the difference between auth.users.raw_app_meta_data and auth.users.raw_user_meta_data?

The auth.users table used by Supabase Auth (GoTrue) has both raw_app_meta_data and a raw_user_meta_data fields.

raw_user_meta_data is designed for profile data and can be created and modified by a user. For example, this data can be set when a user signs up: sign-up-with-additional-user-meta-data or this data can be modified by a user with auth-update

raw_app_meta_data is designed for use by the application layer and is used by GoTrue to handle authentication (For example, the provider and providers claims are used by GoTrue to track authentication providers.) raw_app_meta_data is not accessible to the user by default.

Security Considerations

If you want to tighten security so that custom claims can only be set or deleted from inside the query editor or inside your PostgreSQL functions or triggers, edit the function is_claims_admin() to disallow usage by app users (no usage through the API / Postgrest). Instructions are included in the function.

By default, usage is allowed through your API, but the ability to set or delete claims is restricted to only users who have the claims_admin custom claim set to true. This allows you to create an "admin" section of your app that allows designated users to modify custom claims for other users of your app.

Bootstrapping

If the only way to set or delete claims requires the claims_admin claim to be set to true and no users have that claim, how can I edit custom claims from within my app?

The answer is to "bootstrap" a user by running the following command inside your Supabase Query Editor window:

select set_claim('03acaa13-7989-45c1-8dfb-6eeb7cf0b92e', 'claims_admin', 'true');

where 03acaa13-7989-45c1-8dfb-6eeb7cf0b92e is the id of your admin user found in auth.users.

Using the functions

Inside the Query Editor

You can get, set, and delete claims for any user based on the user's id (uuid) with the following functions:

get_claims(uid uuid)

This returns a JSON object containing all the custom claims for a user.

get_claim(uid uuid, claim text)

This returns a JSON object for a single claim for a user.

set_claim(uid uuid, claim text, value jsonb)

This sets a specific claim for a user. You can send any valid JSON data here, including numbers, text, boolean, arrays, or complete complex nested JSON objects.

delete_claim(uid uuid, claim text)

This will delete a custom claim for a user.

Inside PostgreSQL Functions and Triggers

When using custom claims from inside a PostgreSQL function or trigger, you can use any of the functions shown in the section above: Inside the Query Editor.

In addition, you can use the following functions that are specific to the currently logged-in user:

is_claims_admin()

This returns true if the claims_admin claim is set to true. This means the user is allowed to execute the functions above (set_claim, etc.)

get_my_claims()

This returns a JSON object containing all the custom claims for the current user.

get_my_claim(claim TEXT)

This returns a JSON object for a single claim for the current user.

Inside an RLS (Row Level Security) Policy

To use custom claims in an RLS Policy, you'll normally use the get_my_claim to check a specific claim for the currently logged in user. For example, you could check to see if the user is a subscriber to your app before allowing access to a table. Something like this:
get_my_claim('userrole') = '"SUBSCRIBER"'

The syntax is slightly odd here because the get_my_claim function returns a JSON object, so if you're checking for a string you need to enclose it in quotes. You can also check for other types (numeric, boolean, etc.)

Getting Claims Data from Local Session Data

You can extract claims information from the session object you get when the user is logged in. For example:

supabase.auth.onAuthStateChange((_event, session) => {
  if (session?.user) {
    console.log(session?.user?.app_metadata) // show custom claims
  }
})
Enter fullscreen mode Exit fullscreen mode

If any claims have changed since your last log in, you may need to log out and back in to see these changes.

Setting Claims Data From Your Application (using .rpc())

The following functions can only be used by a "claims admin", that is, a user who has the claims_admin custom claim set to true:

(Note: these functions allow you to view, set, and delete claims for any user of your application, so these would be appropriate for an administrative branch of your application to be used only by high-level users with the proper security rights (i.e. claims_admin level users.))

TypeScript Examples:

public get_claims = async (uid: string) => {
    const { data, error } = await supabase
        .rpc('get_claims', { uid });
    return { data, error };
}
public get_claim = async (uid: string, claim: string) => {
    const { data, error } = await supabase
        .rpc('get_claim', { uid, claim });
    return { data, error };
}
public set_claim = async (uid: string, claim: string, value: object) => {
    const { data, error } = await supabase
        .rpc('set_claim', { uid, claim, value });
    return { data, error };
}
public delete_claim = async (uid: string, claim: string) => {
    const { data, error } = await supabase
        .rpc('delete_claim', { uid, claim });
    return { data, error };
}
Enter fullscreen mode Exit fullscreen mode

Warning

Be sure to watch for reserved claims in your particular development environment. For example, the claims exp and role are reserved by the Supabase Realtime system and can cause problems if you try use these names. To avoid these potential problems, it's good practice to use a custom identifier in your custom claims, such as MY_COMPANY_item1, MY_COMPANY_item2, etc.

Conclusion

I hope you've found this article helpful as you develop your application with Supabase. This is just my personal approach to handling claims, so feel free to use this as a starting point for your custom approach. I'm looking forward to seeing what cool things you come up with in your applications!

Top comments (8)

Collapse
 
liamgsmith profile image
Liam Smith

This is so amazingly simple - thank you!

I know it's more of a newbie thing in terms off adding/using triggers and functions, but the bit I'm struggling with now is a 'after user is created, add a claim to them for 'company_role as user' so I've got the ability to manually set any admin's behind the scenes via the SQL editor.
Will keep investigating!

Collapse
 
burggraf profile image
Mark Burggraf

You can definitely call these functions from your trigger functions to set claims automatically when users are created (or updated).

Collapse
 
jerzakm profile image
Martin J • Edited

I just started diving into supabase and pgsql for the first time and I think I'm going to use it.

What about this for notifying a webapp user that a server changed their claim:

  • Make a table 'userClaimHash' where id is userID (and RLS read constraint) and hash column is either uuid or just autoincrementing int
  • Make a trigger to update userClaimHash whener claim is changed
  • Subscribe to userClaimHash and call supabase.auth.update({}) on change
Collapse
 
burggraf profile image
Mark Burggraf

Pretty cool solution!

Collapse
 
mrmartineau profile image
Zander Martineau

This is really useful info, especially for folks coming from Firebase. Thank you 👍

Collapse
 
roneneizen profile image
Ronen Eizen

Hi Mark. Thanks a lot for this. I've been looking all over the internet for an example like yours. How would I extend raw_app_meta_data when new user is created. Ideally I would like to run this function before new record is inserted. Thank you.

Collapse
 
chocovish profile image
Vishal Ghosh

is there any way to add custom claims in jwt from a different table rather than from raw_user_meta_data?
because I have a profiles table. and I stored all the custom profile related data there, now I want to add some info from profiles table to jwt

Collapse
 
burggraf profile image
Mark Burggraf

Unfortunately, the only claims that make it into the JWT are from raw_app_meta_data, so you need to put things you want in the claims there.