Supabase has a solid system for Row Level Security (RLS), and you may even take advantage of it while bringing Outseta Auth into the mix 🥳
The key is to exchange your authenticated user's JWT Access Token signed by Outseta for a JWT Access Token signed by Supabase and then use the latter in subsequent requests to Supabase.
The token exchange must happen server-side, and do the following:
- Verifies the Outseta JWT Access Token using your Outseta JWT Public Key
- Your Outseta JWT Public Key is available at Auth > Sign up and Login > Login settings > JWT Key
- Or use your well-known URL: https://[your-domain]/.well-known/jwks
- Creates a new JWT
- Supabase required
role
to be "authenticated", - we recommend copying over everything else from the Outseta JWT Access Token
- Supabase required
- Signs the new JWT with your Supabase JWT Secret
- Your Supabase JWT Secret is available at Project Settings > API Settings > JWT Settings > JWT Secret
In subsequent requests to Supabase, use the Supabase signed JWT as a bearer token. If you are using the Supabase JS SDK, that looks like this:
const supabaseClient = supabase.createClient(
<your_supabase_url>,
<your_supabase_anon_key>,
{
global: {
headers: {Authorization: `Bearer ${<the_supabase_signed_jwt>}`}
}
}
);
Supabase makes the decoded and verified Supabase signed JWT available in RLS Policies and as possible default values for columns through the built-in helper function auth.jwt()
:
Person
Uid of the signed-in user:auth.jwt() ->> 'sub'::text
Account
Uid of the signed-in user:auth.jwt() ->> 'outseta:accountUid'::text
Sample Supabase Edge Function
A convenient place to host the exchange code is in a Supabase Edge Function if your app does not already have a server-side component.
Note the need to deploy the exchange function with the no verify flag: supabase functions deploy exchange --no-verify-jwt
.
// File: /functions/exchange/index.ts
// Deploy as a Supabase function with --no-verify-jwt // as we are providing an Outseta token, not a Supabase token // command: supabase functions deploy exchange --no-verify-jwt import * as jose from "https://deno.land/x/[email protected]/index.ts"; const corsHeaders = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type", }; Deno.serve(async (req) => { if (req.method === "OPTIONS") { console.log("OPTIONS request"); return new Response("ok", { headers: corsHeaders }); } // Get the Outseta signed JWT from the Authorization header const authHeader = req.headers.get("Authorization"); const outsetaJwtAccessToken = authHeader?.split(" ")[1] || ""; try { const JWKS = jose.createRemoteJWKSet( new URL(`https://${Deno.env.get("OUTSETA_DOMAIN")}/.well-known/jwks`) ); // Use the JSON Web Key (JWK) to verify the token const { payload } = await jose.jwtVerify(outsetaJwtAccessToken, JWKS); console.log("JWT is valid"); // Update the JWT for Supabase and sign with the Supabase secret payload.role = "authenticated"; const supabaseEncodedJwtSecret = new TextEncoder().encode( Deno.env.get("SUPA_JWT_SECRET") ); const jwtAlg = "HS256"; const supabaseJwt = await new jose.SignJWT(payload) .setProtectedHeader({ alg: jwtAlg, typ: "JWT" }) .setIssuer("supabase") .setIssuedAt(payload.iat) .setExpirationTime(payload.exp || "") .sign(supabaseEncodedJwtSecret); // Respond with the new Supabase JWT return new Response(JSON.stringify({ supabaseJwt }), { headers: { ...corsHeaders, "Content-Type": "application/json" }, status: 200, }); } catch (error) { console.error(error.message, { outsetaJwtAccessToken, }); return new Response(JSON.stringify({ error: "Invalid" }), { headers: { ...corsHeaders, "Content-Type": "application/json" }, status: 401, }); } });
Sample Supabase Function with error handling and more logging can be viewed on Code Sandbox.
The Outseta JWT Payload
Since we copy over the full Outset JWT Payload to use in the following sections, it can be useful to familiarize yourself with the Outseta JWT Access Token.
Sample Supabase RLS Policies
Supabase makes the verified and decoded Supabase signed JWT available in RLS Policies through the built-in helper function auth.jwt()
.
It would look something like this if you were to restrict deleting of a todo to the user that created it:
Sample Supabase Default Value
Supabase makes the verified and decoded Supabase signed JWT available as possible default values for columns through the built-in helper function auth.jwt()
.
It would look something like this if you were to default a todo to belong to an account:
Vanilla JS Demo
The very simplistic demo is of a todo app for your family (aka Account
) and completed by any member of the family (aka Person
).