Skip to content

Instantly share code, notes, and snippets.

@raae
Last active April 8, 2024 12:40
Show Gist options
  • Save raae/166821dea48368da65027ea1f207a54a to your computer and use it in GitHub Desktop.
Save raae/166821dea48368da65027ea1f207a54a to your computer and use it in GitHub Desktop.
Supabase Edge Function to exchange an Outseta-signed JWT with a Supabase-signed JWT
// 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/jose@v4.14.4/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 });
}
if (req.method === "GET") {
console.log("GET request");
return new Response("Method not allowed", {
headers: corsHeaders,
status: 405,
});
}
const outsetaDomain = Deno.env.get("OUTSETA_DOMAIN");
if (!outsetaDomain) {
throw new Error("No OUTSETA_DOMAIN env variable set");
}
const supabaseJwtSecret = Deno.env.get("SUPA_JWT_SECRET");
if (!supabaseJwtSecret) {
throw new Error("No SUPA_JWT_SECRET env variable set");
}
// Get the Outseta signed JWT from the Authorization header
const authHeader = req.headers.get("Authorization");
const outsetaJwtAccessToken = authHeader?.split(" ")[1] || "";
if (!outsetaJwtAccessToken) {
throw new Error("No Outseta JWT provided");
}
try {
console.log("Verify Outseta JWT");
const JWKS = jose.createRemoteJWKSet(
new URL(`https://${outsetaDomain}/.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");
console.log("Payload:", payload);
// Update the JWT for Supabase and sign with the Supabase secret
payload.role = "authenticated";
const supabaseEncodedJwtSecret = new TextEncoder().encode(
supabaseJwtSecret
);
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);
console.log("Supabase Signed JWT:", supabaseJwt);
// 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,
});
}
});
@raae
Copy link
Author

raae commented Sep 19, 2023

Then, one can do the below in policies:

  • ((auth.jwt() ->> 'outseta:accountUid'::text) = (account_uid)::text) -> to match on account uid
  • ((auth.jwt() ->> 'sub'::text) = (person_uid)::text) -> to match on person uid

@raae
Copy link
Author

raae commented Apr 8, 2024

Remember to use --no-verify-jwt when deploying:

supabase functions deploy exchange --no-verify-jwt

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment