Securing OpenAI Text Completion API Requests with Firebase Cloud Functions as a Proxy
In this tutorial, I’d like to show you how you can deploy a Firebase Cloud function that queries the OpenAI ChatGPT API and returns the payload acting as a proxy API for your client apps. By following the steps outlined in this tutorial, you will learn how to implement a secure and efficient way of handling API requests that involve sensitive information, such as API Keys, by using a cloud function as an intermediary. With this approach, you can protect your API Key from being exposed or compromised, and ensure that your client apps are communicating with the OpenAI ChatGPT API in a reliable and scalable manner.
Backstory
I recently encountered an issue while developing a proxy API for my app, Neon, which utilises ChatGPT to generate colors for user-described scenes. The problem arose from the fact that the API requests were being made on the device, which meant that the API Key was also stored on the device. Upon further investigation, I discovered a significant discrepancy between the number of requests made to OpenAI using my API Key as reported by TelemetryDeck, a privacy-focused analytics software, and OpenAI’s usage charts. It seems that someone intercepted the request while the device was executing it, extracted the API Key from the header, and started using it for their own purposes. To address this issue, I decided to implement a Firebase cloud function that would store the API Key and handle the actual request to OpenAI, while the mobile app would simply call this cloud function.
Setup
To get started with setting up your device for development of cloud functions, Firebase documentation provides an excellent resource that you can use. Here are the steps you can follow:
- Make sure you have node installed on your Mac. Node is a server environment that allows you to run JS code on your local machine without using a browser. You can download node either from their official website or by using a package manager like home-brew.
- Create a new folder in your directory of choice and name it
sample-cf
. - Install the Firebase tools package from node using this command:
npm install -g firebase-tools
. - Next, you will need to authenticate yourself. Assuming you already have a Firebase project created on console.firebase.google.com, you can use firebase login to login and authenticate yourself with the project of your choice.
- Once you have successfully authenticated, run
firebase init functions
to set up Firebase cloud functions. This command will create a new directory inside ofsample-cf
calledfunctions
. - Select “Typescript” as the language for the Cloud Function when prompted.
Extra modules required
In this tutorial, we will be using three external packages:
express
: This is a NodeJS web application framework that simplifies the process of building web applications by providing useful features such as routing, middleware, and templating. You can perform requests with inbuilt libaries such as promises and async-await as well but I am familiar with express and hence the usage of this library for this tutorial.axios
: It is a promise-based HTTP Client for NodeJS and the browser. A promise represents an asynchronous operation that is in a pending state when it’s being executed and moves to either a fulfilled or a failed state upon completion.dotenv
: We will be using this package to manage our secret API Key from our project’s environment.dotenv
enables us to store sensitive information, such as API Keys, in a .env file and load them as environment variables during runtime, ensuring that the information remains secure and is not exposed to the public.
Getting started
First, create a .ENV
file in the root of your functions
directory. Add this file to your .gitignore
so that you don’t accidentally commit this to your repository. Inside the .ENV
file, add your API Key.
API_KEY=YOUR_API_KEY_HERE
In your index.ts
file (inside of the sources directory) delete all the existing code and get started by adding the import statements.
import * as functions from "firebase-functions";
import express from "express";
import axios from "axios";
import dotenv from "dotenv";
Next, configure the express
and dotenv
packages
const app = express();
dotenv.config();
app.use(express.json());
Sidestep: OpenAI completion request payload
In their API documentation, OpenAI has given an example of the information that we can provide to their text completion API. This example includes several parameters that we can adjust to control how the API generates its output. The payload looks like the following
{
"model": "text-davinci-003",
"prompt": "ask a question",
"max_tokens": 20,
"temperature": 0
}
model
: Specifies the ID of the language model to be used. In this case, the model is “text-davinci-003”, which is one of the most powerful and capable models provided by OpenAI.prompt
: Specifies the starting text that will be used to generate the completion. In this case, the prompt is “ask a question”.max_tokens
: Specifies the maximum number of tokens (words or subwords) that the API should generate in the completion. In this case, the maximum number of tokens is 20.temperature
: Specifies the “creativity” or randomness of the generated completion. A value of 0 means that the API will always choose the most likely completion, while higher values will result in more creative and diverse completions. In this case, the temperature is set to 0, meaning that the API will always choose the most likely completion and be more deterministic.
Creating the express Request
Add this to your index.ts
file below the library setup we had done earlier.
app.post("/", (req, res) => {
const body = req.body;
const url = "https://api.openai.com/v1/completions";
const API_KEY = process.env.API_KEY;
const headers = {
"Content-Type": "application/json",
"Authorization": `Bearer ${API_KEY}`,
};
axios.post(url, body, {headers: headers})
.then((response) => {
res.json(response.data);
})
.catch((error) => {
console.log(error);
res.status(500).send(error);
});
});
This code creates a server-side API endpoint that listens for incoming HTTP POST requests. When a POST request is received, the server retrieves the request body containing the input data in JSON format. The server then prepares an HTTP POST request to OpenAI’s text completion API using the axios
library, which is a widely used library for sending HTTP requests. The axios.post()
method sends the request to the specified URL, with the request body and headers. Here, the expectation is that the client, in our case the mobile app, send the correct payload that OpenAI is expecting. The cloud function simply acts as an intermediary.
The URL is https://api.openai.com/v1/completions, which is the endpoint for the OpenAI text completion API.
The headers object contains two headers: “Content-Type” and “Authorization”. The Content-Type
header specifies that the request body is in JSON format, while the Authorization
header includes the API key needed for authentication, which is stored in the environment variable API_KEY inside of the .ENV
file that we had setup earlier.
If the API request is successful, the response data is sent back to the client that made the initial HTTP POST
request in JSON format using the res.json()
method. If there is an error in the API request, such as a server error or invalid input, the server logs the error to the console and sends a 500 error response to the client using the res.status().send()
method.
Export the function
Finally, export the express
app to create a cloud function.
exports.defaultOpenAIRequest = functions.https.onRequest(app);
Deploy to Firebase
Now that we have our code ready we need to deploy it to Firebase. Deploy the Cloud Function to Firebase by running the command firebase deploy --only functions
Once the deployment is successful you should see the function in your Firebase console under the functions tab.
Testing
Open an API testing application such as Postman and follow the following steps.
- In the URL field, paste the endpoint of the cloud function that you want to test.
- In the body of the request, paste the OpenAI text completion payload that you want to send to the cloud function.
- Add a header with the key
Content-Type
and set its value toapplication/json
. This tells the server that the data in the request body is in JSON format. - Set the request type to
POST
since the cloud function is designed to receivePOST
requests. - Finally, click the Send button to execute the request and see the response from the cloud function.
Authentication
I will not go into the authentication part of the cloud function as it is outside the scope of this tutorial but it’s very important that you do implement it. You can check the following links
- https://stackoverflow.com/a/56837672
- https://stackoverflow.com/a/44500591
- https://github.com/firebase/functions-samples/tree/main/authorized-https-endpoint
Conclusion
One benefit of deploying a cloud function to make the OpenAI API call is that it helps protect sensitive information, such as API keys, from users. This approach ensures that only authorised individuals can access the keys and make API calls on behalf of the application. In this article, we explored how to use the Javascript express
module to send HTTP requests to the OpenAI text completion API and receive responses, and we also discussed how to deploy this code as a cloud function on Firebase. With the help of cloud platforms like Firebase, it is easier than ever to build and deploy server-less applications that leverage the power of AI.