From 84692781298cdfcddaf42e46eebd9f16e4d90285 Mon Sep 17 00:00:00 2001 From: thorwebdev Date: Tue, 1 Aug 2023 15:37:38 +0800 Subject: [PATCH] feat: add supabase connect edge function example. --- .../supabase/.env.local.example | 4 + .../functions/connect-supabase/README.md | 32 +++++++ .../functions/connect-supabase/index.ts | 83 +++++++++++++++++++ 3 files changed, 119 insertions(+) create mode 100644 examples/edge-functions/supabase/functions/connect-supabase/README.md create mode 100644 examples/edge-functions/supabase/functions/connect-supabase/index.ts diff --git a/examples/edge-functions/supabase/.env.local.example b/examples/edge-functions/supabase/.env.local.example index 28cf14f6838..a15cee971a7 100644 --- a/examples/edge-functions/supabase/.env.local.example +++ b/examples/edge-functions/supabase/.env.local.example @@ -42,3 +42,7 @@ FUNCTION_SECRET="random secret" # upstash-redis-counter UPSTASH_REDIS_REST_URL= UPSTASH_REDIS_REST_TOKEN= + +# connect-supabase - https://supabase.com/docs/guides/platform/oauth-apps/publish-an-oauth-app +SUPA_CONNECT_CLIENT_ID= +SUPA_CONNECT_CLIENT_SECRET= diff --git a/examples/edge-functions/supabase/functions/connect-supabase/README.md b/examples/edge-functions/supabase/functions/connect-supabase/README.md new file mode 100644 index 00000000000..f35038192b3 --- /dev/null +++ b/examples/edge-functions/supabase/functions/connect-supabase/README.md @@ -0,0 +1,32 @@ +# Build a Supabase Marketplace Integration + +Supabase offers an [OAuth2 connection flow](https://supabase.com/docs/guides/platform/oauth-apps/authorize-an-oauth-app) and a [Management API](https://supabase.com/docs/reference/api/introduction) allowing you to build Supabase Marketplace Integrations that connect to our users' hosted Supabase projects, making it more convenient than ever to create scalabale backends programmatically and tap into the extensive pool of Supabase users. + +## Setup + +1. Follow the [steps in the docs](https://supabase.com/docs/guides/platform/oauth-apps/publish-an-oauth-app) to create an OAuth App. +1. Set `SUPA_CONNECT_CLIENT_ID` and `SUPA_CONNECT_CLIENT_SECRET` in your `.env.local` file as shown in the [`.env.local.example` file](../../.env.local.example). + +## Connect to Supabase using OAuth2 + +This example showcases and end-to-end OAuth2 connection flow with [PKCE](https://supabase.com/blog/supabase-auth-sso-pkce#introducing-pkce), with the following steps: + +1. Create authorization URL with PKCE codeVerifier. +1. Redirect user to Supabase to authorize your application to connect to their Supabase account. +1. User gets redirected to the callback route, where we exchange the code in the URL for `access_token` and `refresh_token`. +1. We use the `access_token` to retrieve a list of the user's projects using the [`supabase-management-js` library](https://github.com/supabase-community/supabase-management-js). + +## Run locally + +```bash +supabase functions serve connect-supabase --no-verify-jwt --env-file ./supabase/.env.local +``` + +Navigate to http://localhost:54321/functions/v1/connect-supabase + +## Deploy to Supabase Edge Functions + +```bash +supabase functions deploy connect-supabase --no-verify-jwt +supabase secrets set --env-file ./supabase/.env.local +``` diff --git a/examples/edge-functions/supabase/functions/connect-supabase/index.ts b/examples/edge-functions/supabase/functions/connect-supabase/index.ts new file mode 100644 index 00000000000..b126a5906aa --- /dev/null +++ b/examples/edge-functions/supabase/functions/connect-supabase/index.ts @@ -0,0 +1,83 @@ +import { Application, Router } from 'https://deno.land/x/oak@v11.1.0/mod.ts' +import { Session, CookieStore } from 'https://deno.land/x/oak_sessions@v4.1.9/mod.ts' +import { OAuth2Client } from 'https://deno.land/x/oauth2_client@v1.0.2/mod.ts' +import { SupabaseManagementAPI } from 'https://esm.sh/supabase-management-js@0.1.2' + +const config = { + clientId: Deno.env.get('SUPA_CONNECT_CLIENT_ID')!, + clientSecret: Deno.env.get('SUPA_CONNECT_CLIENT_SECRET')!, + authorizationEndpointUri: 'https://api.supabase.com/v1/oauth/authorize', + tokenUri: 'https://api.supabase.com/v1/oauth/token', + redirectUri: 'http://localhost:54321/functions/v1/connect-supabase/oauth2/callback', + defaults: { + scope: 'all', + }, +} +const oauth2Client = new OAuth2Client(config) + +type AppState = { + session: Session +} + +const router = new Router() +// Note: path should be prefixed with function name. +router.get('/connect-supabase', (ctx) => { + ctx.response.body = + 'This is an example of implementing https://supabase.com/docs/guides/integrations/oauth-apps/authorize-an-oauth-app . Navigate to /login to start the OAuth flow.' +}) +router.get('/connect-supabase/login', async (ctx) => { + // Construct the URL for the authorization redirect and get a PKCE codeVerifier. + const { uri, codeVerifier } = await oauth2Client.code.getAuthorizationUri() + console.log(uri.toString()) + + // Store both the state and codeVerifier in the user session. + ctx.state.session.flash('codeVerifier', codeVerifier) + + // Redirect the user to the authorization endpoint. + ctx.response.redirect(uri) +}) +router.get('/connect-supabase/oauth2/callback', async (ctx) => { + // Make sure the codeVerifier is present for the user's session. + const codeVerifier = ctx.state.session.get('codeVerifier') as string + console.log('codeVerifier', codeVerifier) + if (!codeVerifier) throw new Error('No codeVerifier!') + + // Exchange the authorization code for an access token. + const tokens = await fetch(config.tokenUri, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Accept: 'application/json', + Authorization: `Basic ${btoa(`${config.clientId}:${config.clientSecret}`)}`, + }, + body: new URLSearchParams({ + grant_type: 'authorization_code', + code: ctx.request.url.searchParams.get('code') || '', + redirect_uri: config.redirectUri, + code_verifier: codeVerifier, + }), + }).then((res) => res.json()) + console.log('tokens', tokens) + // TODO: Make sure to store the tokens in your DB for future use. + + // Use the access token to make an authenticated API request. + const supaManagementClient = new SupabaseManagementAPI({ + accessToken: tokens.accessToken ?? tokens.access_token, + }) + const projects = await supaManagementClient.getProjects() + + ctx.response.body = `Hello, these are your projects: \n ${JSON.stringify( + projects?.map((p) => ({ id: p.id, name: p.name })), + null, + 2 + )}!` +}) + +const app = new Application() +// cookie name for the store is configurable, default is: {sessionDataCookieName: 'session_data'} +const store = new CookieStore('very-secret-key') +// @ts-ignore TODO: open issue at https://github.com/jcs224/oak_sessions +app.use(Session.initMiddleware(store)) +app.use(router.routes()) +app.use(router.allowedMethods()) +await app.listen({ port: 8000 })