244 lines
8.1 KiB
Plaintext
244 lines
8.1 KiB
Plaintext
---
|
|
date: 2023-10-03
|
|
title: "Secure your Supabase functions with Unkey"
|
|
description: "Learn how to use Unkey to secure your Supabase functions"
|
|
author: james
|
|
tags: ["product"]
|
|
---
|
|
|
|
Supabase offers [edge functions](https://supabase.com/docs/guides/functions) built upon Deno. They have a variety of uses for applications like OpenAI or working with their storage product. In this post, we will show you how to use Unkey to secure your function in just a few lines of code.
|
|
|
|
## What is Unkey?
|
|
|
|
Unkey is an open source API management platform that helps developers secure, manage, and scale their APIs. Unkey has built-in features that can make it easier than ever to provide an API to your end users, including:
|
|
|
|
- Per key rate limiting
|
|
- Limited usage keys
|
|
- Time-based keys
|
|
- Per key analytics
|
|
|
|
## Prerequisites
|
|
|
|
1. Create a [Supabase account](https://supabase.com)
|
|
2. Create a [Unkey account](https://unkey.dev) and follow our [Quickstart guide](https://unkey.dev/docs/quickstart). So you have an API key to verify.
|
|
3. Setup [Supabase CLI](https://supabase.com/docs/guides/cli/local-development) for local development.
|
|
|
|
## Create our project
|
|
|
|
|
|
### Create a project folder
|
|
|
|
First, we need to create a folder. Let's call that `unkey-supabase`. This will be where our supabase functions exist going forward.
|
|
|
|
```bash
|
|
mkdir unkey-supabase && cd unkey-supabase
|
|
```
|
|
### Start Supabase services
|
|
|
|
Now, we have a folder for our project. We can initialize and start Supabase for local development.
|
|
|
|
```bash
|
|
supabase init
|
|
```
|
|
Make sure Docker is running. The `start` command uses Docker to start the Supabase services.
|
|
This command may take a while to run if this is the first time using the CLI.
|
|
|
|
```bash
|
|
supabase start
|
|
```
|
|
|
|
### Create a Supabase function
|
|
|
|
Now that Supabase is setup, we can create a Supabase function. This function will be where we secure it using Unkey.
|
|
|
|
```bash
|
|
supabase functions new hello-world
|
|
```
|
|
|
|
This command creates a function stub in your Supabase folder at `./functions/hello-world/index.ts`. This stub will have a function that returns the name passed in as data for the request.
|
|
|
|
```tsx title="./functions/hello-world/index.ts"
|
|
import { serve } from "https://deno.land/std@0.168.0/http/server.ts"
|
|
|
|
console.log("Hello from Functions!")
|
|
|
|
serve(async (req) => {
|
|
const { name } = await req.json()
|
|
const data = {
|
|
message: `Hello ${name}!`,
|
|
}
|
|
|
|
return new Response(
|
|
JSON.stringify(data),
|
|
{ headers: { "Content-Type": "application/json" } },
|
|
)
|
|
})
|
|
```
|
|
#### Test your Supabase function
|
|
|
|
Before making any changes, let's ensure your Supabase function runs. Inside the function, you should see a cURL command similar to the following:
|
|
|
|
```bash
|
|
curl -i --location --request POST 'http://localhost:54321/functions/v1/' \
|
|
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' \
|
|
--header 'Content-Type: application/json' \
|
|
--data '{"name":"hello-world"}'
|
|
```
|
|
After invoking your Edge Function, you should see the response `{ "message":"Hello Functions!" }`.
|
|
|
|
> If you receive an error Invalid JWT, find the `ANON_KEY` of your project in the Dashboard under Settings > API.
|
|
|
|
## Add Unkey to secure our Supabase function
|
|
|
|
### Add `verifyKey` to our function
|
|
|
|
Now that we have a function, we must add Unkey to secure the endpoint. Supabase uses Deno, so instead of installing our npm package, we will use ESM CDN to provide the `verifyKey` function we need.
|
|
|
|
```jsx {2} title="./functions/hello-world/index.ts"
|
|
import { serve } from "https://deno.land/std@0.168.0/http/server.ts"
|
|
import { verifyKey } from "https://esm.sh/@unkey/api";
|
|
```
|
|
|
|
### What does `verifyKey` do?
|
|
|
|
Unkey's `verifykey` lets you verify a key from your end users. We will return a result and you can decide whether to give the user access to a resource or not based upon that result. For example, a response could be:
|
|
|
|
```json
|
|
{
|
|
"result": {
|
|
"valid": true,
|
|
"ownerId": "james",
|
|
"meta": {
|
|
"hello": "world"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Updating our Supabase function
|
|
|
|
First, let's remove the boilerplate code from the function so we can work on adding Unkey.
|
|
|
|
```jsx title="./functions/hello-world/index.ts"
|
|
import { serve } from "https://deno.land/std@0.168.0/http/server.ts"
|
|
import { verifyKey } from "https://esm.sh/@unkey/api";
|
|
|
|
serve(async (req) => {
|
|
|
|
})
|
|
```
|
|
|
|
Next, we will wrap the `serve` function inside a try-catch. Just in case something goes wrong, we can handle that.
|
|
|
|
```jsx title="./functions/hello-world/index.ts"
|
|
import { serve } from "https://deno.land/std@0.168.0/http/server.ts"
|
|
import { verifyKey } from "https://esm.sh/@unkey/api";
|
|
|
|
serve(async (req) => {
|
|
try {
|
|
// handle our functions here.
|
|
} catch (error) {
|
|
// return a 500 error if there is an error with a message.
|
|
return new Response(JSON.stringify({ error: error.message }), {
|
|
status: 500,
|
|
});
|
|
}
|
|
})
|
|
```
|
|
|
|
#### Check headers for API Key
|
|
|
|
Inside our try, we can look for a header containing the user's API Key. In this example we will use `x-unkey-api-key` but you could call the header whatever you want. If there is no header will immediately return 401.
|
|
|
|
```jsx title="./functions/hello-world/index.ts"
|
|
import { serve } from "https://deno.land/std@0.168.0/http/server.ts"
|
|
import { verifyKey } from "https://esm.sh/@unkey/api";
|
|
|
|
serve(async (req) => {
|
|
try {
|
|
const token = req.headers.get("x-unkey-api-key");
|
|
if (!token) {
|
|
return new Response("Unauthorized", { status: 401 });
|
|
}
|
|
} catch (error) {
|
|
// return a 500 error if there is an error with a message.
|
|
return new Response(JSON.stringify({ error: error.message }), {
|
|
status: 500,
|
|
});
|
|
}
|
|
})
|
|
```
|
|
|
|
### Verifying the key
|
|
|
|
The `verifyKey` function returns a `result` and `error`, making the logic easy to handle. Below is a simplified example of the verification flow.
|
|
|
|
```tsx
|
|
const { result, error } = await verifyKey("key_123");
|
|
if (error) {
|
|
// handle potential network or bad request error
|
|
// a link to our docs will be in the `error.docs` field
|
|
console.error(error.message);
|
|
return;
|
|
}
|
|
if (!result.valid) {
|
|
// do not grant access
|
|
return;
|
|
}
|
|
// process request
|
|
console.log(result);
|
|
```
|
|
|
|
Now you have a basic understanding of verification, let's add this to our Supabase function.
|
|
|
|
```tsx title="./functions/hello-world/index.ts"
|
|
serve(async (req) => {
|
|
try {
|
|
const token = req.headers.get("x-unkey-api-key");
|
|
if (!token) {
|
|
return new Response("No API Key provided", { status: 401 });
|
|
}
|
|
const { result, error } = await verifyKey(token);
|
|
if (error) {
|
|
// handle potential network or bad request error
|
|
// a link to our docs will be in the `error.docs` field
|
|
console.error(error.message);
|
|
return new Response(JSON.stringify({ error: error.message }), {
|
|
status: 400,
|
|
});
|
|
}
|
|
if (!result.valid) {
|
|
// do not grant access
|
|
return new Response(JSON.stringify({ error: "API Key is not valid for this request" }), {
|
|
status: 401,
|
|
});
|
|
}
|
|
return new Response(JSON.stringify({ result }), { status: 200 });
|
|
}
|
|
```
|
|
|
|
### Testing our Supabase function
|
|
|
|
We can send a curl request to our endpoint to test this functionality. Below is an example of the curl to send. Remember, we now need to include our API key.
|
|
|
|
```bash
|
|
curl -XPOST -H 'Authorization: Bearer <SUPBASE_BEARER_TOKEN>' \
|
|
-H 'x-unkey-api-key: <UNKEY_API_KEY>' \
|
|
-H "Content-type: application/json" 'http://localhost:54321/functions/v1/hello-world'
|
|
```
|
|
|
|
## Adding CORS for added security
|
|
|
|
Adding CORS allows us to call our function from the frontend and decide what headers can be passed to our function. Inside your `functions` folder, add a file called `cors.ts`. Inside this cors file, we will tell the Supabase function which headers and origins are allowed.
|
|
|
|
```tsx title="./functions/cors.ts"
|
|
export const corsHeaders = {
|
|
"Access-Control-Allow-Origin": "*",
|
|
"Access-Control-Allow-Headers": "authorization, x-client-info, x-unkey-api-key, content-type",
|
|
};
|
|
```
|
|
|
|
## Conclusion
|
|
|
|
In this post, we have covered how to use Unkey with Supabase functions to secure them. You can check out the code for this project in our [Examples folder](https://github.com/unkeyed/examples/tree/main/supabase-functions)
|