Skip to content

Commit

Permalink
feat: add supabase connect edge function example.
Browse files Browse the repository at this point in the history
  • Loading branch information
thorwebdev committed Aug 1, 2023
1 parent a6a99eb commit 8469278
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 0 deletions.
4 changes: 4 additions & 0 deletions examples/edge-functions/supabase/.env.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Original file line number Diff line number Diff line change
@@ -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
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { Application, Router } from 'https://deno.land/x/[email protected]/mod.ts'
import { Session, CookieStore } from 'https://deno.land/x/[email protected]/mod.ts'
import { OAuth2Client } from 'https://deno.land/x/[email protected]/mod.ts'
import { SupabaseManagementAPI } from 'https://esm.sh/[email protected]'

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<AppState>()
// 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<AppState>()
// 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 })

0 comments on commit 8469278

Please sign in to comment.