Securing a GenAI Service with API Key Validation and Trial Management

Securing a GenAI Service with API Key Validation and Trial Management

This tutorial will guide you through securing a GenAI service with a free trial and API key validation using Node.js, Express, and PropelAuth, a B2B authentication provider. We'll set up a system where users can sign up for a free 7-day trial, upgrade to a paid plan, and use their API key to generate images. The user's API key is validated on each request to generate a new image, and the trial expiration date is checked before the request is permitted.

Let's dive in and explore the key components of the user signup process, plan upgrades, and image generation workflow.

What We’ll Build

We’re building an image generation API similar to Midjourney. To complete this tutorial, we’ll leverage PropelAuth’s API Key Authentication, custom user properties, and account management features. We’ll also create an Express API with endpoints to simulate Stripe payment processing and image creation. The endpoints are:

  1. Stripe Webhook (/webhook/stripe): Listen for when users upgrade to a paid plan or downgrade to the free plan.
  2. Image Create (/api/image/create): Before generating an image, validate the API key and check that the user has not exceeded the seven-day trial window.

You can find the complete code on GitHub. Let’s get started!

Setting Up the GenAI Company

Begin by signing up for a free PropelAuth account if you don’t have one already. After signing up, create a project. A project includes everything you need to set up authentication, like UIs, a dashboard for managing your users, transactional emails, and more. In the dashboard, open the Signup page by navigating to the Preview button in the top right corner and choosing Signup.

The Signup page is one of several hosted pages that PropelAuth provides. Hosted pages are pre-built, customizable web pages such as signup, login, and account that integrate seamlessly with your application. It will look similar to:

We need a test user account for this application, so go ahead and sign up! Use any email you can access, including the one you signed up for PropelAuth with.

Securing the Image Creation Service

To secure access to our image creation service, we’ll require users to send an API key to an image creation API we’ll define shortly. PropelAuth provides API key management that is out-of-the-box for personal users and organization use cases. Users can create their own API keys from the Personal API Keys page. One less thing for us to build!

Head back into the PropelAuth dashboard and onto the API Key Settings page. Toggle “Personal API Keys” to ON so users can create API keys themselves.

Next, let’s add free and paid plans as an additional mechanism for verifying image creation access. This is another thing that PropelAuth can help us with! In the dashboard, navigate to User Properties. These are fields that you can use to store information about your users, such as how they use your product, their profile picture, or, in our case, the plan they are subscribed to.

Let’s create a custom User Property called plan_name. Click “Add Custom Property.” We want our users to be able to see their plan from the PropelAuth account page (more on that soon), so choose “Managed by your users,” select the type “Enum,” and enter the name “plan_name.” Once created, set the Enum Values to Free and Paid (Free | Paid).

The user should be able to view their plan at any time. To enable this, toggle “Show in Account Page.” Next, select “Not Required” since we don’t need to collect this property from users. Instead, we’ll set it behind the scenes. Finally, answer “Can Users Edit?” with “No (Read-only)” for obvious reasons— we wouldn’t want users to switch to the Paid plan without paying!

We’ve now set up the backend portion of the image creation service. Now, we can create the webhook and API that leverages the API keys and plan name custom user property.

Create the GenAI Image Creation API

To implement the image creation API, we’ll use Express.js for its simplicity and ease of use. If you have a favorite web API framework, no worries! The concepts covered below should be easy to follow.

Setting Up the Express App

Here’s a brief overview of setting up the app from scratch. You can also reference the complete code on GitHub.

$ mkdir myapp
$ cd myapp
$ npm init
$ entry point: (src/app.ts)

Though not required, the reference code repository was configured to use TypeScript, so we’ll configure it here too.

TypeScript Configuration

First, install Express, TypeScript, and Type Definitions.

npm install express typescript ts-node ts-node-dev @types/node @types/express --save-dev

Next, create a TypeScript config file:

npx tsc --init

Open tsconfig.json and configure it like so:

{
  "compilerOptions": {
    "target": "ES6",
    "module": "CommonJS",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "./dist",
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

Finally, update package.json to add a dev command for local debugging:

"scripts": {
  "dev": "ts-node-dev src/app.ts"
}

Now, we can move on to implementation. Open src/app.ts to see the basics of the Express app’s setup. The following creates the Express server running on port 3000.

import express, { Request, Response } from "express";

const app = express();
const port = 3000;

app.listen(port, () => {
  console.log(`Server is running at http://localhost:${port}`);
});

Create a “Stripe” Webhook

The first functionality we need is updating a user’s plan when they’ve upgraded to a Paid plan or downgraded to a Free one. We’d use a payment provider like Stripe to handle payment processing in a real-world app. Here’s how the workflow would work:

Incorporating Stripe or a similar payment provider is outside the scope of this tutorial, but we can simulate it easily. Create a new endpoint called

/webhook/stripe that pulls out the user ID and event name from the request body. If a new “subscription” has been created, update the user’s plan to Paid. Otherwise, if it’s been removed (downgraded), change it to “Free.”

app.post("/webhook/stripe", async (req: Request, res) => {
  const event = req.body;
  const userId = event.data.object.metadata.userId;
  switch (event.type) {
    case "customer.subscription.created":
      await updateUserPlan(userId, "Paid");
      break;
    case "customer.subscription.deleted":
      await updateUserPlan(userId, "Free");
      break;
  }

  // Return a response to acknowledge receipt of the event
  res.json({ received: true });
});

In newer versions of Express, we need to install a separate package, body-parser , to access the request body.

npm install body-parser

Import it:

import bodyParser from "body-parser";

Then, create a middleware that parses JSON:

const app = express();
const port = 3000;
const jsonParser = bodyParser.json();

Next, add the JSON parser to the webhook method signature. Now, req.body will automatically be parsed and available to us.

app.post("/webhook/stripe", jsonParser, async (req: Request, res) => { 
  const event = req.body;
}

We need to implement the functionality to change a user’s plan. As a reminder, that custom user property is defined in our PropelAuth instance. Fortunately, they provide an easy-to-use SDK.

Implementing PropelAuth

Begin by installing PropelAuth’s Express library:

npm install @propelauth/express

Next, import and call initAuth, which performs a one-time initialization of the library and verifies our image service’s API key is correct.

import { initAuth, UserMetadata } from "@propelauth/express";

const auth = initAuth({
  authUrl: process.env.PROPELAUTH_AUTH_URL!,
  apiKey: process.env.PROPELAUTH_API_KEY!,
});

Wait a minute! We don’t have an apiKey or authUrl.

Creating a Backend API Key

To update a user’s plan or validate a user’s API key, we need to connect our app to our PropelAuth instance. When we make Express library requests to PropelAuth, we’ll authenticate using a dedicated API key. To create one, head back into the PropelAuth dashboard.

  • Go to “Integrate your product” → “Backend Integration.”
  • Remain in the pre-selected “Test” environment, then click “Create New API Key.”
  • Enter a name and click Create.
  • Copy the API key immediately since you can only view it once.

You’ll also need the Auth URL, the unique URL pointing to your PropelAuth auth server. You can access that at any time.

Configure PropelAuth Express Library with Environment Files

Let’s use environment files to share the Auth URL and API Key with the PropelAuth Express library. First, install the popular dotenv package that loads environment variables from .env into Node.js projects.

npm install dotenv

Create a .env file at the project's root and copy/paste your Auth URL and the API key.

# similar values as these
PROPELAUTH_AUTH_URL=https://123.propelauthtest.com
PROPELAUTH_API_KEY=d68422536...

With those in place, process.env.PROPELAUTH_AUTH_URL will resolve correctly during the call to initAuth.

const auth = initAuth({
  authUrl: process.env.PROPELAUTH_AUTH_URL!,
  apiKey: process.env.PROPELAUTH_API_KEY!,
});

Update a User’s Plan

The PropelAuth library can authenticate our image creation service now, so the last step is adding the ability to change a user’s plan. Provide the userId and plan name to updateUserMetadata :

const updateUserPlan = async (userId: string, planName: string) => {
  await auth.updateUserMetadata(userId, {
    properties: {
      plan_name: planName,
    },
  });
};

All set! Our “Stripe” webhook is complete. If this was a real webhook, the last step is to secure the endpoint by verifying Stripe's signature. You can read more about that in the official Stripe documentation.

In the last step of this tutorial, we’ll implement the image creation endpoint, which is responsible for validating the user’s API key and allowing requests only if the user is within seven days of their free trial or on the paid plan.

Implementing the Image Creation Endpoint

This endpoint validates the user’s API key and checks if the user is still in the free trial period or has upgraded to a paid plan. Begin by creating a function to validate the user’s API key.

If the key is invalid, the auth library will throw an error. We’ll catch it and return an “Unauthorized” error.

const validateApiKey = async (apiKey: string, res: Response) => {
  try {
    return (await auth.validatePersonalApiKey(apiKey)).user;
  } catch (error) {
    res.status(401).json({ error: "Invalid API key" });
    throw new Error("Invalid API key");
  }
};
Tip: PropelAuth has a Postman collection you can download, making it easy to view and test all of PropelAuth’s backend APIs.

Next, we need to validate whether the GenAI image creation request from the user is valid. User requests are valid if they are on the free plan and it has been seven or fewer days since they signed up for an account or, of course if they are on the paid plan.

First, inspect the createdAt timestamp we get back from PropelAuth’s user object and use it to create a trial expiration date seven days from when the user’s account was created. If the user is on the Free plan and today’s date is past their trial expiration date, throw a Forbidden error.

/* Allow the GenAI request if:
- The user is on Free plan and created date isn't past 7 days OR
- The user is on Paid plan */
const validateUserIsPayingOrOnTrial = async (validatedUser: UserMetadata, res: Response) => {
  const trialExpirationDate = new Date(validatedUser.createdAt * 1000);
  trialExpirationDate.setDate(trialExpirationDate.getDate() + 7);
  const todaysDate = new Date();

  const planName = validatedUser.properties?.planName ?? "Free";
  if (planName === "Free" && todaysDate.getTime() > trialExpirationDate.getTime()) {
    res.status(403).json({ error: "Trial expired. Please upgrade to Paid plan." });
    throw new Error("Trial expired. Please upgrade to Paid plan.");
  }
};

Finally, we combine the API key validation and GenAI request methods into the Image Create endpoint. Extract the API Key from the request body and pass it to validateApiKey. If the key is valid, check that the user is permitted to make an image request. Creating a GenAI image is outside the scope of this tutorial, so instead, return a cute puppy picture.

app.post("/api/image/create", jsonParser, async (req: Request, res: Response) => {
  const apiKey = req.body.apiKey;
  const validatedUser = await validateApiKey(apiKey, res);
  await validateUserIsPayingOrOnTrial(validatedUser, res);

  // User is permitted to create an image
  // Image creation outside the scope of this example
  // Instead, return a cute puppy picture
  res.json({ imageCreated: "https://picsum.photos/id/237/200/300" });
});

Here’s the complete code:

app.post("/api/image/create", jsonParser, async (req: Request, res: Response) => {
  const apiKey = req.body.apiKey;
  const validatedUser = await validateApiKey(apiKey, res);
  await validateUserIsPayingOrOnTrial(validatedUser, res);

  // User is permitted to create an image
  // Image creation outside the scope of this example
  // Instead, return a cute puppy picture
  res.json({ imageCreated: "https://picsum.photos/id/237/200/300" });
});

/* Allow the GenAI request if:
- The user is on Free plan and created date isn't past 7 days OR
- The user is on Paid plan */
const validateUserIsPayingOrOnTrial = async (validatedUser: UserMetadata, res: Response) => {
  const trialExpirationDate = new Date(validatedUser.createdAt * 1000);
  trialExpirationDate.setDate(trialExpirationDate.getDate() + 7);
  const todaysDate = new Date();

  const planName = validatedUser.properties?.planName ?? "Free";
  if (planName === "Free" && todaysDate.getTime() > trialExpirationDate.getTime()) {
    res.status(403).json({ error: "Trial expired. Please upgrade to Paid plan." });
    throw new Error("Trial expired. Please upgrade to Paid plan.");
  }
};

const validateApiKey = async (apiKey: string, res: Response) => {
  try {
    return (await auth.validatePersonalApiKey(apiKey)).user;
  } catch (error) {
    res.status(401).json({ error: "Invalid API key" });
    throw new Error("Invalid API key");
  }
};

Test the Complete Workflow

We are code complete and can test the API we’ve created. We’ll need a personal user API key, so head back into the PropelAuth dashboard and navigate to the Account page. The easiest way is to select Preview in the upper right corner, then “Account.” This will launch your server’s Account page at a URL similar to https://123.propelauthtest.com/en/login. Log in as your test user.

Navigate to the Personal API Keys page and click “New API key.” For key expiration, choose any value you’d like. Like the server API key, copy it immediately, as it’s only shown once.

When calling the webhook, we need to send the user’s ID along with the API key. Stripe would send it automatically in production, so this is only needed for testing. In the PropelAuth dashboard, navigate to the Users page. Find the user in the Users table, click the three vertical dots on the right-hand side, and choose “Copy User ID.”

Now we’re ready to simulate what Stripe would do by testing the webhook endpoint! Start the app with npm run dev, then make a POST request to localhost:3000/webhook/stripe with the “successful payment” event in the body and the userId you copied from the dashboard. This will upgrade the user to the Paid plan.

{
    "type": "customer.subscription.created",
    "data": {
        "object": {
            "metadata": {
                "userId": "4ed07fa0-8366..."
            }
        }
    }
}

If it worked, you should see a 200 OK along with the response body:

{
    "received": true
}

Since we configured the Plan Name custom user property to be visible on the Account page, let’s verify that it was updated correctly. Back in the PropelAuth dashboard, choose the Preview button, then Account in the upper right-hand corner. Sign in to be redirected to the Settings page. Sure enough, under the Account Settings section, we can see that “Plan Name” has been set to “Paid!”

Now, let’s try the image creation endpoint. Make a POST request to localhost:3000/api/image/create and in the request body, include your API key:

{
    "apiKey": "9d14ac0d..."
}

Since our user is now a paying customer, the request should be successful, and the (hardcoded) created image should be returned:

{
    "imageCreated": "https://picsum.photos/id/237/200/300"
}

Our reward for successfully testing our API? An adorable puppy.

To recap what we’ve accomplished:

  • Set up PropelAuth for user and API key management
  • Created an Express.js app with two API endpoints for a GenAI image creation service
  • Created a test user and verified the API was correct

But wait, there’s more!

Bonus: Tracking API Key Signups

PropelAuth goes beyond providing auth tools. It also surfaces useful information about your customers and their users, including audit logs and exploration of company/user data. In our case, we can track how many users have signed up for an API key, which is useful for reviewing adoption over time.

Head into the PropelAuth dashboard and navigate to the Data page. Select User Audit Logs, and in the filter, choose “Created Personal API Key.” A list of all users that created a personal API key is displayed:

image.png

Summary

In this tutorial, we created an image creation service for a GenAI company. We leveraged PropelAuth’s API Key Authentication, custom user properties, and account management features for the backend and API Key management. We also created an Express.js API with endpoints to simulate payment processing and image creation. With minimal configuration and code, we’ve implemented a common API authentication use case.

Feel free to reference the complete reference code while you build your own APIs.