forked from supabase/supabase
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add supabase connect edge function example.
- Loading branch information
1 parent
a6a99eb
commit 8469278
Showing
3 changed files
with
119 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
32 changes: 32 additions & 0 deletions
32
examples/edge-functions/supabase/functions/connect-supabase/README.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
``` |
83 changes: 83 additions & 0 deletions
83
examples/edge-functions/supabase/functions/connect-supabase/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }) |