Supaweek Day 2 - User-level auth in your API

In Day 1 of the Supaweek we covered how to create an AI-based API using OpenAI and Supabase DB as its backend, and we added auth and rate-limiting using Zuplo's API Gateway and even programmed a request handler directly in the gateway.

Today, we'll cover how to handle user requests dynamically in your API (i.e. user-level auth) because not all users are created equal.

User-level authentication#

Putting an authentication layer to your API is essential for its security, but it wouldn't have taken you long to start asking, how do I actually program my API to handle different users differently?

If you were using JWT tokens, you could add claims to the token and then program your API to handle these claims differently. But, as we saw in Day 1, Zuplo's default authentication mechanism is API Keys, and API Keys are just short strings opaque strings, so how do we handle different users differently?

We wrote extensively about why we chose API Keys over JWTs, one of them is our Wait, you're not using API keys?, where we covered how the best APIs in the world use API Keys. At Zuplo, we wanted to make the use of API Keys as easy as possible, but we also made them customizable. Let's see how in today's tutorial.

Adding metadata to API Keys and using them in your API#

We continue to build on the API we created in Day 1, so make sure you have it deployed and ready to go.

We will add an orgId to the API Key so that we can use it in our API to know which organization the user belongs to. We will then store the orgId in the DB and use it to filter the requests.

Step 1 - Add orgId column#

In your Supabase DB, add a column orgId to the blogs table, and the resulting schema should look like this:

id (int8)
created_at (timestamptz)
content (text)
title (text)
+ orgId (int8)

Step 2 - Program the request handler#

For both the GET and POST operations, we will modify the request handler to filter the requests based on the orgId of the user.

For the POST operation, go to generate-blog.ts which is used as the request handler and update the function with the following code (your generate-blog.ts to look like this).

// ... stays the same
 
export default async function (request: ZuploRequest, context: ZuploContext) {
+  // When using the `api-key-inbound` policy (or any auth policy)
+  // Zuplo automatically adds the user's metadata to the request object
+  // so we can use it to get the orgId
+  const { orgId } = request.user?.data;
+
+  if (!orgId) {
+    // This will block the further execution of the request
+    // and return a 401 response to the client and it will not hit
+    // any other policies or the handler
+    return new Response("Unauthorized", { status: 401 });
+  }
 
  // ... stays the same
 
-  const savedData = await saveBlogtoDatabase(blogResult, context.log);
+  const savedData = await saveBlogtoDatabase(blogResult, orgId, context.log);
 
  return new Response(JSON.stringify(savedData), {
    headers: {
      "Content-Type": "application/json; charset=utf-8",
    },
  });
}
 
type CreatedBlogSchema = {
  id: number;
+  orgId: number;
  title: string;
  content: string;
  created_at: string;
};
 
const saveBlogtoDatabase = async (
  blog: string,
+  orgId: string,
  logger: Logger
): Promise<CreatedBlogSchema | null> => {
  try {
    const { content, title } = JSON.parse(blog);
 
    const { data, error } = await supabase
      .from("blogs")
-      .insert({ content, title })
+      .insert({ content, title, orgId })
      .select();
 
    if (error || data === null || data.length === 0) {
      logger.error(error || "No data returned from database");
      return null;
    }
 
    return data[0] as CreatedBlogSchema;
  } catch (err) {
    logger.error(err);
    return null;
  }
};

Notice that we're pulling the orgId from the API Key metadata by using request.user.data?.orgId, that's because Zuplo populates the request.user object to include the details of the API Key that was used to make the request and the metadata that was added to it. If the user is not authenticated, the request.user object will be undefined.

For the GET operation, go to the Request Handler and update the URL to be ${env.SUPABASE_URL}/rest/v1/blogs?orgId=eq.${request.user.data?.orgId} as shown below:

Request handler

Step 3 - Create an API Key with orgId in the metadata#

To make an authenticated request to the API, create an API Key in Project Settings > API Key Consumers > Add New Consumer. Add the orgId in the API Key metadata, you can leave the manager field empty for now.

User with OrgId

Copy the generated API Key as we will use it in the next step.

Step 4 - Make a request to the API!#

First, create a blog using the POST operation from the test console.

Test console

  1. Add your API Key header in the format Authorization: Bearer zpka_1234 and make a request
  2. Add the request body. This endpoint accepts a JSON in the format { "topic": "you blog topic" }. Add it to whichever topic you want OpenAI to create the blog about.
  3. Hit Test! 💥

You can now try to make the request with the GET operation, which will now filter the blogs based on the orgId of the user.

[
  {
    "id": 2,
    "orgId": 2 // <--- Notice the orgId
    "created_at": "2023-09-06T18:01:12.774955+00:00",
    "content": "Driving is hard.",
    "title": "Exploring Different Perspectives",
  },
]

If you now try to make a request with a different API Key with a different orgId, you will get an empty array.

This is Day 2 of Supaweek#

You can find a video walkthrough of this tutorial here:

As always if you have any questions, comments or feedback, join us in our Discord server, or drop us a tweet at X. We'd love to hear from you!

Check out Day 3 of the Supaweek here to learn about the API Documentation that's generated with your Zuplo API!

Designed for Developers, Made for the Edge