├── CHANGELOG.md ├── Makefile ├── README.md ├── claims-demo ├── .env.sample ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public │ ├── index.html │ ├── manifest.json │ └── robots.txt └── src │ ├── App.css │ ├── App.js │ ├── Auth.js │ ├── Logout.js │ ├── TestFunctions.js │ ├── Userinfo.js │ ├── index.css │ ├── index.js │ └── supabaseClient.js ├── install.sql ├── supabase_custom_claims--1.0.sql ├── supabase_custom_claims.control └── uninstall.sql /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | ## 1.0 3 | - first release 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | EXTENSION = supabase_custom_claims 2 | DATA = supabase_custom_claims--1.0.sql 3 | 4 | PG_CONFIG = pg_config 5 | PGXS := $(shell $(PG_CONFIG) --pgxs) 6 | include $(PGXS) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Supabase Custom Claims 2 | Want to know more about Custom Claims? See the [FAQ](#faq) below. 3 | 4 | This is just one way to implement `custom claims` for a Supabase project. The goal here is simply to add JSON data to the 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`) 5 | 6 | ## Installing the Functions 7 | The file [install.sql](./install.sql) contains all the PostgreSQL functions you need to implement and manage custom claims in your Supabase project. 8 | 9 | 1. Paste the SQL code from [install.sql](./install.sql) into the [SQL Query Editor](https://app.supabase.io/project/_/sql) of your Supabase project. 10 | 2. Click `RUN` to execute the code. 11 | ## Uninstalling the Functions 12 | 13 | 1. Paste the SQL code from [uninstall.sql](./uninstall.sql) into the [SQL Query Editor](https://app.supabase.io/project/_/sql) of your Supabase project. 14 | 2. Click `RUN` to execute the code. 15 | 16 | ### Security Considerations 17 | 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. 18 | 19 | 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. 20 | 21 | ### Bootstrapping 22 | 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? 23 | 24 | The answer is to **"bootstrap"** a user by running the following command inside your [Supabase Query Editor](https://app.supabase.io/project/_/sql) window: 25 | 26 | `select set_claim('03acaa13-7989-45c1-8dfb-6eeb7cf0b92e', 'claims_admin', 'true');` 27 | 28 | where `03acaa13-7989-45c1-8dfb-6eeb7cf0b92e` is the `id` of your admin user found in `auth.users`. 29 | 30 | ## Usage 31 | ### Inside the Query Editor 32 | You can get, set, and delete claims for any user based on the user's `id` (uuid) with the following functions: 33 | 34 | #### `get_claims(uid uuid)` returns jsonb 35 | ##### example 36 | `select get_claims('03acaa13-7989-45c1-8dfb-6eeb7cf0b92e');` 37 | ##### result 38 | ``` 39 | | get_claims | 40 | | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 41 | | {"provider": "email", "userrole": "MANAGER", "providers": ["email"], "userlevel": 100, "useractive": true, "userjoined": "2022-05-20T14:07:27.742Z", "claims_admin": true} | 42 | ``` 43 | 44 | #### `get_claim(uid uuid, claim text)` returns jsonb 45 | ##### example 46 | `select get_claim('03acaa13-7989-45c1-8dfb-6eeb7cf0b92e', 'userlevel');` 47 | ##### result 48 | ``` 49 | | get_claim | 50 | | --------- | 51 | | 100 | 52 | ``` 53 | 54 | #### `set_claim(uid uuid, claim text, value jsonb) `returns text 55 | ##### example 56 | Set a **number** value. (Note `value` is passed as a `jsonb` value, so to set a number we need to pass it as a simple string.) 57 | `select set_claim('03acaa13-7989-45c1-8dfb-6eeb7cf0b92e', 'userlevel', '200');` 58 | 59 | Set a **text** value. (Note `value` is passed as a `jsonb` value, so to set a number we need to pass it with double-quotes.) 60 | `select set_claim('03acaa13-7989-45c1-8dfb-6eeb7cf0b92e', 'userrole', '"MANAGER"');` 61 | 62 | **Common Mistake**: If you forget the double-quotes for a string, and try to do this: `select set_claim('03acaa13-7989-45c1-8dfb-6eeb7cf0b92e', 'userrole', 'MANAGER');`, the result will be an error: `invalid input syntax for type json` 63 | 64 | Set a **boolean** value. 65 | `select set_claim('03acaa13-7989-45c1-8dfb-6eeb7cf0b92e', 'useractive', 'true');` 66 | 67 | Set an **array** value. 68 | `select set_claim('03acaa13-7989-45c1-8dfb-6eeb7cf0b92e', 'items', '["bread", "cheese", "butter"]');` 69 | 70 | Set a complex, nested **json** / **object** value. 71 | `select set_claim('03acaa13-7989-45c1-8dfb-6eeb7cf0b92e', 'gamestate', '{"level": 5, "items": ["knife", "gun"], "position":{"x": 15, "y": 22}}');` 72 | 73 | ##### result (for any of the above) 74 | ``` 75 | | set_claim | 76 | | --------- | 77 | | OK | 78 | ``` 79 | 80 | #### `delete_claim(uid uuid, claim text)` returns text 81 | ##### example 82 | `select delete_claim('03acaa13-7989-45c1-8dfb-6eeb7cf0b92e', 'gamestate');` 83 | ##### result 84 | ``` 85 | | delete_claim | 86 | | ------------ | 87 | | OK | 88 | ``` 89 | 90 | ### Inside PostgreSQL Functions and Triggers 91 | 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`. 92 | 93 | In addition, you can use the following functions that are specific to the currently logged-in user: 94 | 95 | #### `is_claims_admin()` returns bool 96 | ##### example 97 | `select is_claims_admin();` 98 | ##### result 99 | ``` 100 | | is_claims_admin | 101 | | --------------- | 102 | | true | 103 | ``` 104 | 105 | #### `get_my_claims()` returns jsonb 106 | ##### example 107 | `select get_my_claims();` 108 | ##### result 109 | ``` 110 | | get_my_claims | 111 | | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 112 | | {"provider": "email", "userrole": "MANAGER", "providers": ["email"], "userlevel": 100, "useractive": true, "userjoined": "2022-05-20T14:07:27.742Z", "claims_admin": true} | 113 | ``` 114 | 115 | #### `get_my_claim(claim TEXT)` returns jsonb 116 | ##### example 117 | `select get_my_claim('userlevel');` 118 | ##### result 119 | ``` 120 | | get_my_claim | 121 | | ------------ | 122 | | 100 | 123 | ``` 124 | 125 | ### Inside an RLS (Row Level Security) Policy 126 | 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. 127 | #### examples 128 | ##### only allow users with userrole "MANAGER" 129 | `get_my_claim('userrole') = '"MANAGER"'` 130 | (which the UI will change into the more formal): 131 | `((get_my_claim('userrole'::text)) = '"MANAGER"'::jsonb)` 132 | 133 | ##### only allow users with userlevel over 100 134 | `coalesce(get_my_claim('userlevel')::numeric,0) > 100` 135 | 136 | ##### only allow users with claim_admin = true 137 | `coalesce(get_my_claim('claims_admin')::bool,false)` 138 | 139 | ### Inside your app (using `.rpc()`) 140 | 141 | #### Getting Claims Data from Local Session Data 142 | You can extract claims information from the `session` object you get when the user is logged in. For example: 143 | 144 | ```js 145 | supabase.auth.onAuthStateChange((_event, session) => { 146 | if (session?.user) { 147 | console.log(session?.user?.app_metadata) // show custom claims 148 | } 149 | }) 150 | ``` 151 | 152 | If any claims have changed since your last log in, you may need to log out and back in to see these changes. 153 | 154 | #### Getting Claims Data from the Server 155 | You can also query the server to see what claims are set for the current user. 156 | 157 | Here are some sample functions that can be used by any authenticated (logged-in) user of your application: 158 | 159 | ```js 160 | public get_my_claims = async () => { 161 | const { data, error } = await supabase 162 | .rpc('get_my_claims', {}); 163 | return { data, error }; 164 | } 165 | public get_my_claim = async (claim: string) => { 166 | const { data, error } = await supabase 167 | .rpc('get_my_claim', {claim}); 168 | return { data, error }; 169 | } 170 | public is_claims_admin = async () => { 171 | const { data, error } = await supabase 172 | .rpc('is_claims_admin', {}); 173 | return { data, error }; 174 | } 175 | ``` 176 | 177 | 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`: 178 | 179 | (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.)) 180 | 181 | ```js 182 | public get_claims = async (uid: string) => { 183 | const { data, error } = await supabase 184 | .rpc('get_claims', {uid}); 185 | return { data, error }; 186 | } 187 | public get_claim = async (uid: string, claim: string) => { 188 | const { data, error } = await supabase 189 | .rpc('get_claim', {uid, claim}); 190 | return { data, error }; 191 | } 192 | public set_claim = async (uid: string, claim: string, value: object) => { 193 | const { data, error } = await supabase 194 | .rpc('set_claim', {uid, claim, value}); 195 | return { data, error }; 196 | } 197 | public delete_claim = async (uid: string, claim: string) => { 198 | const { data, error } = await supabase 199 | .rpc('delete_claim', {uid, claim}); 200 | return { data, error }; 201 | } 202 | ``` 203 | 204 | ## Running an older project (Postgres 13 or earlier?) 205 | See [this issue](https://github.com/supabase-community/supabase-custom-claims/issues/3) 206 | 207 | ## FAQ 208 | ### What are custom claims? 209 | Custom Claims are special attributes attached to a user that you can use to control access to portions of your application. 210 | 211 | For example: 212 | ``` 213 | plan: "TRIAL" 214 | user_level: 100 215 | group_name: "Super Guild!" 216 | joined_on: "2022-05-20T14:28:18.217Z" 217 | group_manager: false 218 | items: ["toothpick", "string", "ring"] 219 | ``` 220 | 221 | ### What type of data can I store in a custom claim? 222 | 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. 223 | 224 | ### Where are these custom claims stored? 225 | Custom claims are stored in the `auth.users` table, in the `raw_app_meta_data` column for a user. 226 | 227 | ### Are there any naming restrictions? 228 | 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. 229 | 230 | ### Why use custom claims instead of just creating a table? 231 | 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. 232 | 233 | 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. 234 | 235 | ### What are the drawbacks to using custom claims? 236 | 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. 237 | 238 | You can force a refresh of the current session token by calling `supabase.auth.refreshSession()` on the client, but if a claim is changed by a server process or by a claims adminstrator 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. 239 | 240 | ### How can I write a query to find all the users who have a specific custom claim set? 241 | #### examples 242 | ##### find all users who have `claims_admin` set to `true` 243 | `select * from auth.users where (auth.users.raw_app_meta_data->'claims_admin')::bool = true;` 244 | ##### find all users who have a `userlevel` over 100 245 | `select * from auth.users where (auth.users.raw_app_meta_data->'userlevel')::numeric > 100;` 246 | ##### find all users whose `userrole` is set to `"MANAGER"` 247 | (note for strings you need to add double-quotes becuase data is data is stored as JSONB) 248 | `select * from auth.users where (auth.users.raw_app_meta_data->'userrole')::text = '"MANAGER"';` 249 | 250 | ### What's the difference between `auth.users.raw_app_meta_data` and `auth.users.raw_user_meta_data`? 251 | The `auth.users` table used by Supabase Auth (GoTrue) has both `raw_app_meta_data` and a `raw_user_meta_data` fields. 252 | 253 | `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](https://supabase.com/docs/reference/javascript/auth-signup#sign-up-with-additional-user-meta-data) or this data can be modified by a user with [auth-update](https://supabase.com/docs/reference/javascript/auth-updateuser) 254 | 255 | `raw_app_meta_data` is designed for use by the application layer and is used by GoTrue to handle authentication (For exampple, 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. 256 | 257 | ### NOTES: 258 | ##### updating claims from a server process or edge function 259 | https://supabase.com/docs/reference/javascript/auth-api-updateuserbyid#updates-a-users-app_metadata 260 | 261 | ## Warning 262 | 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. 263 | -------------------------------------------------------------------------------- /claims-demo/.env.sample: -------------------------------------------------------------------------------- 1 | REACT_APP_SUPABASE_URL=https://xxxxxxxxxxxxxxxxxxxx.supabase.co 2 | REACT_APP_SUPABASE_ANON_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 3 | 4 | -------------------------------------------------------------------------------- /claims-demo/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .env 8 | 9 | # testing 10 | /coverage 11 | 12 | # production 13 | /build 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /claims-demo/README.md: -------------------------------------------------------------------------------- 1 | # claims-demo react app 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## setup 6 | 7 | - Make sure you have a Supabase project set up and the site URL is set to `https://localhost:3000` 8 | - Copy the file `.env.sample` to `.env` and insert your own values for `REACT_APP_SUPABASE_URL` and `REACT_APP_SUPABASE_ANON_KEY` - found in the Supabase dashboard here: [Supabase API Settings](https://app.supabase.io/project/_/settings/api) 9 | - Run the contents of [../install.sql](../install.sql) inside your [Supabase Dashboard SQL Editor](https://app.supabase.io/project/_/sql) 10 | - [Bootstrap](https://github.com/supabase-community/supabase-custom-claims#bootstrapping) a user to make them a `claims_admin` user if you want to be able to set or delete claims 11 | - Start the app: `npm start` 12 | 13 | ### run: `npm start` 14 | 15 | Runs the app in the development mode.\ 16 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser. 17 | 18 | -------------------------------------------------------------------------------- /claims-demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "claims-demo", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@supabase/supabase-js": "^2.4.1", 7 | "@testing-library/jest-dom": "^5.16.4", 8 | "@testing-library/react": "^13.2.0", 9 | "@testing-library/user-event": "^13.5.0", 10 | "react": "^18.1.0", 11 | "react-dom": "^18.1.0", 12 | "react-scripts": "5.0.1", 13 | "web-vitals": "^2.1.4" 14 | }, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "test": "react-scripts test", 19 | "eject": "react-scripts eject" 20 | }, 21 | "eslintConfig": { 22 | "extends": [ 23 | "react-app", 24 | "react-app/jest" 25 | ] 26 | }, 27 | "browserslist": { 28 | "production": [ 29 | ">0.2%", 30 | "not dead", 31 | "not op_mini all" 32 | ], 33 | "development": [ 34 | "last 1 chrome version", 35 | "last 1 firefox version", 36 | "last 1 safari version" 37 | ] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /claims-demo/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 |Supabase Custom Claims Demo Application
26 |Sign in via magic link with your email below
28 | {loading ? ( 29 | 'Sending magic link...' 30 | ) : ( 31 | 45 | )} 46 |{output}133 |