Merge pull request #16163 from supabase/docs/connect-supabase

docs: Supabase Integrations Marketplace
This commit is contained in:
Thor 雷神 Schaeff authored and GitHub committed 2023-08-07 17:22:14 +08:00
commit bab2c8cd72
9 files changed
+333 -82

No files matched your search

@@ -1139,12 +1139,8 @@ export const platform: NavMenuConstant = {
url: '/guides/platform/marketplace',
},
{
name: 'Publish an OAuth App',
url: '/guides/platform/oauth-apps/publish-an-oauth-app',
},
{
name: 'Sign in with Supabase',
url: '/guides/platform/oauth-apps/authorize-an-oauth-app',
name: 'Build a Supabase Integration',
url: '/guides/platform/oauth-apps/build-a-supabase-integration',
},
],
},
@@ -1,49 +0,0 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'authorize-an-oauth-app',
title: 'Authorize an OAuth App (Beta)',
description: 'Authorize an OAuth App',
}
## Overview
This guide steps through implementing **Sign-in with Supabase**. Once you've added OAuth2.0 support to your main application, you will receive an access and refresh token from your users that choose to **Sign in with Supabase** on your application.
The access token returned will grant your application full access to the [Management API](https://supabase.com/docs/reference/api/introduction) on behalf of the user.
## Implement the OAuth 2.0 flow
Once you've published your OAuth App on Supabase, you can use the OAuth 2.0 protocol to seek consent from Supabase users to access their organization or project.
1. Within your app's UI, redirect the user to [`https://api.supabase.com/v1/oauth/authorize`](<https://api.supabase.com/api/v1#/oauth%20(beta)/authorize>). Make sure to include all required query parameters such as:
- `client_id` A UUID uniquely identifying your OAuth app in Supabase.
- `redirect_uri` The URL where Supabase will redirect the user after providing consent.
- `scope` The only scope supported is `all`. Scoped access is not available at this time.
- `response_type` The value `code`.
- `state` Information about the state of your app. Note that `redirect_uri` and `state` cannot both exceed 4kB in size.
- We strongly recommend using the PKCE flow for increased security. Generate a random value before taking the user to the authorize endpoint. This value is called code verifier. Hash it with SHA256 and include it as the `code_challenge` parameter, while setting `code_challenge_method` to `s256`. In the next step, you would need to provide the code verifier to get the first access and refresh token.
2. Once the user consents to providing API access to your OAuth App, Supabase will redirect the user to the `redirect_uri` endpoint you provided in the previous step. The URL will contain these query parameters:
- `code` An authorization code you should exchange with Supabase to get the access and refresh token.
- `state` The value you provided in the previous step, to help you associate the request with the user. The `state` property returned here should be compared to the `state` you sent previously.
3. Exchange the authorization code for an access and refresh token by calling `POST https://api.supabase.com/v1/oauth/token` and including the following query parameters:
- `grant_type` The value `authorization_code`.
- `code` The `code` returned in the previous step.
- `client_id` The unique client ID identifying your OAuth App.
- `client_secret` The secret that authenticates your OAuth App to Supabase.
- `redirect_uri` This must be exactly the same URL used in the first step.
- If you used the PKCE flow in the first step, include the code verifier as `code_verifier`.
## Refresh an access token
You can use the [`POST /v1/oauth/token`](<https://api.supabase.com/api/v1#/oauth%20(beta)/token>) endpoint to refresh an access token using the refresh token returned at the end of the previous section.
If the user has revoked access to your application, you will not be able to refresh a token. Furthermore, access tokens will stop working. Make sure you handle HTTP Unauthorized errors when calling any Supabase API.
## Access the Management API using the access token
Refer to [this section](/docs/reference/api/introduction#authentication) to learn more about authentication with the Management API.
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -0,0 +1,163 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'build-a-supabase-integration',
title: 'Build a Supabase Integration (Beta)',
subtitle:
"This guide steps through building a Supabase Integration using OAuth2 and the management API, allowing you to manage users' organizations and projects on their behalf.",
description: 'Build a Supabase Integration using OAuth2 and the Management API.',
}
Using OAuth2.0 you can retrieve an access and refresh token that grant your application full access to the [Management API](https://supabase.com/docs/reference/api/introduction) on behalf of the user.
## Create an OAuth App
1. In your organization's settings, navigate to the [**OAuth Apps**](/dashboard/org/_/apps) tab.
2. In the upper-right section of the page, click **Add application**.
3. Fill in the required details and click **Confirm**.
## Show a "Connect Supabase" button
In your user interface, add a "Connect Supabase" button to kick off the OAuth flow. Follow the design guidelines outlined in our [brand assets](/brand-assets).
## Implementing the OAuth 2.0 flow
Once you've published your OAuth App on Supabase, you can use the OAuth 2.0 protocol get authorization from Supabase users to manage their organizations and projects.
You can use your preferred OAuth2 client or follow the steps below. You can see an example implementation in TypeScript using Supabase Edge Functions [on our GitHub](https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/connect-supabase).
### Redirecting to the authorize URL
Within your app's UI, redirect the user to [`https://api.supabase.com/v1/oauth/authorize`](<https://api.supabase.com/api/v1#/oauth%20(beta)/authorize>). Make sure to include all required query parameters such as:
- `client_id`: Your client id from the app creation above.
- `redirect_uri`: The URL where Supabase will redirect the user to after providing consent.
- `scope`: Currently only `all` is supported. More fine-grained access control coming soon.
- `response_type`: Set this to `code`.
- `state`: Information about the state of your app. Note that `redirect_uri` and `state` together cannot exceed 4kB in size.
- (Recommended) PKCE: We strongly recommend using the PKCE flow for increased security. Generate a random value before taking the user to the authorize endpoint. This value is called code verifier. Hash it with SHA256 and include it as the `code_challenge` parameter, while setting `code_challenge_method` to `S256`. In the next step, you would need to provide the code verifier to get the first access and refresh token.
```ts
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())
// console.log: https://api.supabase.com/v1/oauth/authorize?response_type=code&client_id=7673bde9-be72-4d75-bd5e-b0dba2c49b38&redirect_uri=http%3A%2F%2Flocalhost%3A54321%2Ffunctions%2Fv1%2Fconnect-supabase%2Foauth2%2Fcallback&scope=all&code_challenge=jk06R69S1bH9dD4td8mS5kAEFmEbMP5P0YrmGNAUVE0&code_challenge_method=S256
// Store the codeVerifier in the user session (cookie).
ctx.state.session.flash('codeVerifier', codeVerifier)
// Redirect the user to the authorization endpoint.
ctx.response.redirect(uri)
})
```
Find the full example on [GitHub](https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/connect-supabase).
### Handling the callback
Once the user consents to providing API access to your OAuth App, Supabase will redirect the user to the `redirect_uri` provided in the previous step. The URL will contain these query parameters:
- `code`: An authorization code you should exchange with Supabase to get the access and refresh token.
- `state`: The value you provided in the previous step, to help you associate the request with the user. The `state` property returned here should be compared to the `state` you sent previously.
Exchange the authorization code for an access and refresh token by calling [`POST https://api.supabase.com/v1/oauth/token`](<https://api.supabase.com/api/v1#/oauth%20(beta)/token>) with the following query parameters as content-type `application/x-www-form-urlencoded`:
- `grant_type`: The value `authorization_code`.
- `code`: The `code` returned in the previous step.
- `redirect_uri`: This must be exactly the same URL used in the first step.
- (Recommended) `code_verifier`: If you used the PKCE flow in the first step, include the code verifier as `code_verifier`.
As per OAuth2 spec, provide the client id and client secret as basic auth header:
- `client_id`: The unique client ID identifying your OAuth App.
- `client_secret`: The secret that authenticates your OAuth App to Supabase.
```ts
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
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)
// Store the tokens in your DB for future use.
ctx.response.body = 'Success'
})
```
Find the full example on [GitHub](https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/connect-supabase).
## Refreshing an access token
You can use the [`POST /v1/oauth/token`](<https://api.supabase.com/api/v1#/oauth%20(beta)/token>) endpoint to refresh an access token using the refresh token returned at the end of the previous section.
If the user has revoked access to your application, you will not be able to refresh a token. Furthermore, access tokens will stop working. Make sure you handle HTTP Unauthorized errors when calling any Supabase API.
## Calling the Management API
Refer to [the Management API reference](/docs/reference/api/introduction#authentication) to learn more about authentication with the Management API.
### Use the JavaScript (TypeScript) SDK
For convenience, when working with JavaScript/TypeScript, you can use the [supabase-management-js](https://github.com/supabase-community/supabase-management-js#supabase-management-js) library.
```ts
import { SupabaseManagementAPI } from 'supabase-management-js'
const client = new SupabaseManagementAPI({ accessToken: '<access token>' })
```
## Integration Recommendations
There are a couple common patterns you can consider adding to your integration that can facilitate a great user experience.
### Store API keys in env variables
Some integraions, e.g. like [Cloudflare Workers](/partners/integrations/cloudflare-workers) provide convenient access to the API URL and API keys to allow user to speed up development.
Using the management API, you can retrieve a project's API credentials using the [`/projects/{ref}/api-keys` endpoint](https://api.supabase.com/api/v1#/projects/getProjectApiKeys).
### Pre-fill database connection details
If your integration directly connects to the project's database, you can pref-fill the Postgres connection details for the user, it follows this schema:
```
postgresql://postgres:[DB-PASSWORD]@db.[REF].supabase.co:5432/postgres
```
Note that you cannot retrieve the database password via the management API, so for the user's existing projects you will need to collect their database password in your UI.
### Create new organizations and projects
If you don't need access to the user's existing project, we recommend creating a new project under a new organization. Use the [`/v1/organizations` endpoint](https://api.supabase.com/api/v1#/organizations/createOrganization) to create a new organization named after your integration, afterward use the [`/v1/projects` endpoint](https://api.supabase.com/api/v1#/projects/createProject) to create a new project.
When creating a new project, you can either ask the user to provide a database password, or you can generate a secure password for them. In any case, make sure to securely store the database password on your end which will allow you to construct the Postgres URI.
### Configure custom Auth SMTP
You can configure the user's [custom SMTP settings](https://supabase.com/docs/guides/auth/auth-smtp) using the [`/config/auth` endpoint](https://api.supabase.com/api/v1#/projects%20config/updateV1AuthConfig).
## Current limitations
Only some features are available until we roll out fine-grained access control. If you need full database access, you will need to prompt the user for their database password.
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -1,25 +0,0 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'publish-an-oauth-app',
title: 'Publish an OAuth App (Beta)',
description: 'Publish an OAuth App',
}
## Overview
This guide steps through publishing an OAuth app under your organization.
## Publishing an OAuth App
1. In your organization's settings, navigate to the [**OAuth Apps**](/dashboard/org/_/apps) tab.
2. In the upper-right section of the page, click **Add application**.
3. In "Application name", fill in the name of your OAuth app.
4. In "Website URL", fill in the URL of the app that's using the OAuth app.
5. Upload a logo for your OAuth app.
6. In "Authorization callback URLs", add the callback URL of your app.
7. At the bottom of the page, click **Confirm**.
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
+9 -2
View File
@@ -2248,7 +2248,14 @@ module.exports = [
},
{
permanent: true,
source: '/docs/reference/cli/config',
destination: '/docs/guides/cli/config',
source: '/docs/guides/platform/oauth-apps/publish-an-oauth-app',
destination:
'/docs/guides/platform/oauth-apps/build-a-supabase-integration#create-an-oauth-app',
},
{
permanent: true,
source: '/docs/guides/platform/oauth-apps/authorize-an-oauth-app',
destination: '/docs/guides/platform/oauth-apps/build-a-supabase-integration',
},
{ permanent: true, source: '/docs/reference/cli/config', destination: '/docs/guides/cli/config' },
]
+40
View File
@@ -77,6 +77,46 @@ const Index = () => {
</div>
</div>
</SectionContainer>
<SectionContainer className="sm:pt-0 md:pt-0 lg:pt-0 xl:pt-0">
<div className="shadow-small grid grid-cols-12 rounded-lg border dark:border-gray-600">
<div className="relative col-span-12 h-60 w-full overflow-auto rounded-lg lg:col-span-5 flex items-center justify-center">
<Image
src="https://obuldanrptloktxcffvn.supabase.co/storage/v1/object/public/supabase-brand-assets/connect-supabase/connect-supabase-dark.svg"
alt="Connect Supabase Button"
width={154}
height={31}
/>
</div>
<div className="col-span-12 flex items-center lg:col-span-7">
<div className="p-16">
<div className="space-y-2">
<h1 className="text-scale-1200 text-4xl">Supabase Integrations</h1>
<p className="text-scale-1100 text-sm">
<p>
When building a{' '}
<a
className="underline"
href="/docs/guides/platform/oauth-apps/build-a-supabase-integration"
>
Supabase Integration
</a>
, use this "Connect Supabase" button to initiate the OAuth redirect.
</p>
<p>Do not use any other color for the wordmark.</p>
</p>
<form
method="get"
action={`https://obuldanrptloktxcffvn.supabase.co/storage/v1/object/public/supabase-brand-assets/connect-supabase/connect-supabase.zip`}
>
<Button htmlType="submit" type="default" iconRight={<IconDownload />}>
Download button kit
</Button>
</form>
</div>
</div>
</div>
</div>
</SectionContainer>
<CTABanner />
</Container>
</Layout>
@@ -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=
@@ -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
```
@@ -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<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 })