Technology Guides and Tutorials

OpenAI ChatGPT OAuth plugin example in nodejs

chatgpt openai plugin

This is a simple guide to setting up a plugin for ChatGPT. First, you create a Node.js project and install necessary dependencies. Then, you host a JSON file for the plugin on your API domain. You also build an OpenAPI specification to document your API. For authentication, OAuth is used. The main code is a Node.js server handling CRUD operations for a to-do list and serving static files. It also includes OAuth routes. The server runs on a specific port, and data is stored in-memory.

Set up a Node.js project:

  1. Start the project: npm init
  2. Install necessary packages: npm install express cors body-parser
  3. Create the needed files: touch chat-server.js, touch manifest.json, touch openapi.yaml
  4. Add a logo image for the plugin: logo.png

Full OAuth ChatGPT Plugin source code on Github: https://github.com/sebbie1o1/openai-nodejs-oauth-chatgpt-plugin

# init nodejs project
npm init
# install deps
npm install express cors body-parser
# create files:
touch chat-server.js
touch manifest.json
touch openapi.yaml
# add plugin logo image: logo.png

ai-plugin.json manifest

Each plugin needs an ai-plugin.json file on the API’s domain. For instance, if your company is example.com, the plugin JSON file should be at https://example.com, where your API is. To install a plugin via the ChatGPT UI, it search for this file at /.well-known/ai-plugin.json. This /.well-known folder must exist on your domain for ChatGPT to work with your plugin. No file, no plugin installation. For local work, HTTP is OK. For a remote server, you need HTTPS.

full source here:

{
  "schema_version": "v1",
  "name_for_human": "TODO OAuth",
  "name_for_model": "todo_oauth",
  "description_for_human": "Plugin for managing a TODO list, you can add, remove and view your TODOs.",
  "description_for_model": "Plugin for managing a TODO list, you can add, remove and view your TODOs.",
  "auth": {
      "type": "oauth",
      "client_url": "PLUGIN_HOSTNAME/oauth",
      "scope": "",
      "authorization_url": "PLUGIN_HOSTNAME/auth/oauth_exchange",
      "authorization_content_type": "application/json",
      "verification_tokens": {
          "openai": "OPENAI_VERIFICATION_TOKEN"
      }
  },
  "api": {
      "type": "openapi",
      "url": "PLUGIN_HOSTNAME/openapi.yaml",
      "is_user_authenticated": false
  },
  "logo_url": "PLUGIN_HOSTNAME/logo.png",
  "contact_email": "contact@example.com",
  "legal_info_url": "http://www.example.com/legal"
}

auth part:

  "auth": {
      "type": "oauth",
      "client_url": "PLUGIN_HOSTNAME/oauth",
      "scope": "",
      "authorization_url": "PLUGIN_HOSTNAME/auth/oauth_exchange",
      "authorization_content_type": "application/json",
      "verification_tokens": {
          "openai": "OPENAI_VERIFICATION_TOKEN"
      }
  },

Chat plugin protocol works with OAuth. Here’s a simple OAuth flow example:

First, choose “Develop your own plugin” in the ChatGPT plugin store. Enter your plugin’s domain (not localhost). In ai-plugin.json, set auth.type to “oauth”. You’ll be asked for the OAuth client ID and client secret. These can be simple text strings, but should follow OAuth rules. OpenAI keeps an encrypted client secret, but the client ID is seen by users.

After you add your client ID and client secret in ChatGPT, you’ll get a verification token. Put this token in your ai-plugin.json file under the auth section.

OAuth requests will have this info:

request={
'grant_type': 'authorization_code', 
'client_id': 'id_set_by_developer', 
'client_secret': 'secret_set_by_developer', 
'code': 'abc123', 
'redirect_uri': 'https://chat.openai.com/aip/plugin-some_plugin_id/oauth/callback'
}

To use a plugin with OAuth, users install the plugin and click a “Sign in with” button in ChatGPT. The authorization_url endpoint should return something like:

{
"access_token": "example_token", 
"token_type": "bearer", 
"refresh_token": "example_token", 
"expires_in": 59
}

During sign in, ChatGPT asks your authorization_url for an access token (and maybe a refresh token). User requests to the plugin include the token in the Authorization header.

Here’s an example of OAuth config in the ai-plugin.json file:

Some explanation about OAuth URL structure:

When setting up your plugin, you’ll provide your OAuth client_id and client_secret.
When a user logs into the plugin, they are directed to

"[client_url]?response_type=code&client_id=[client_id]&scope=[scope]&redirect_uri=https%3A%2F%2Fchat.openai.com%2Faip%2F[plugin_id]%2Foauth%2Fcallback"

The plugin_id is sent with the request to your OAuth endpoint. It’s not visible in the ChatGPT UI now, but might be later. You can see the plugin_id in the request.
After your plugin redirects back to the given redirect_uri, ChatGPT finishes the OAuth flow with a POST request to the authorization_url, using the same authorization_content_type and parameters as before.

Official docs: https://platform.openai.com/docs/plugins/authentication/oauth

OpenAPI Specification

Next, you need to create YAML file OpenAPI spec to document your API. ChatGPT only knows about your API from this spec and the manifest file. If your API is big, you can choose what parts to show to the model. For example, with a social media API, you might let the model see site content but not comment on posts to avoid spam. (OpenApi spec is here: https://swagger.io/specification/)

The OpenAPI spec is like a cover for your API. A simple spec for TODO plugin looks like this:

openapi: 3.0.1
info:
    title: TODO Plugin
    description: A plugin that allows the user to create and manage a TODO list using ChatGPT. If you do not know the user's username, ask them first before making queries to the plugin. Otherwise, use the username "global".
    version: "v1"
servers:
    - url: PLUGIN_HOSTNAME
paths:
    /todos/{username}:
        get:
            operationId: getTodos
            summary: Get the list of todos
            parameters:
                - in: path
                  name: username
                  schema:
                      type: string
                  required: true
                  description: The name of the user.
            responses:
                "200":
                    description: OK
                    content:
                        application/json:
                            schema:
                                $ref: "#/components/schemas/getTodosResponse"
        post:
            operationId: addTodo
            summary: Add a todo to the list
            parameters:
                - in: path
                  name: username
                  schema:
                      type: string
                  required: true
                  description: The name of the user.
            requestBody:
                required: true
                content:
                    application/json:
                        schema:
                            $ref: "#/components/schemas/addTodoRequest"
            responses:
                "200":
                    description: OK
        delete:
            operationId: deleteTodo
            summary: Delete a todo from the list
            parameters:
                - in: path
                  name: username
                  schema:
                      type: string
                  required: true
                  description: The name of the user.
            requestBody:
                required: true
                content:
                    application/json:
                        schema:
                            $ref: "#/components/schemas/deleteTodoRequest"
            responses:
                "200":
                    description: OK

components:
    schemas:
        getTodosResponse:
            type: object
            properties:
                todos:
                    type: array
                    items:
                        type: string
                    description: The list of todos.
        addTodoRequest:
            type: object
            required:
                - todo
            properties:
                todo:
                    type: string
                    description: The todo to add to the list.
                    required: true
        deleteTodoRequest:
            type: object
            required:
                - todo_idx
            properties:
                todo_idx:
                    type: integer
                    description: The index of the todo to delete.
                    required: true

This starts with the spec version, title, description, and version number. When a query runs in ChatGPT, it checks the info section’s description to see if the plugin matches the user’s query.

Keep in mind these limits in your OpenAPI spec (which may change):

  • 200 characters max for each API endpoint description/summary field.
  • 200 characters max for each API param description field.

NodeJS OAuth Server

This is a basic guide on how to create a simple Node.js server for a OpenAI plugin To-do list application with OAuth

  1. Import necessary modules (express, cors, body-parser, fs, path).
  2. Set up the Express application and use middleware (cors, bodyParser).
  3. Define environment variables (OPENAI_VERIFICATION_TOKEN, APP_PORT) and an object to store todos.
  4. Create routes to add (app.post), get (app.get), and delete (app.delete) todos for a specific user.
  5. Create routes to serve logo image, plugin manifest file, and OpenAPI specification (app.get).
  6. Define OAuth routes (app.get, app.post) to handle OAuth authorization and token exchange.
  7. Finally, start the server to listen on a specified port (app.listen).

In this code, todos are stored in-memory, so data is lost when the server restarts. This code also serves static files like logo.png, ai-plugin.json (the manifest), and openapi.yaml (the API specification). Additionally, it has routes for OAuth to handle authorization and token exchange. The server runs on the port specified by APP_PORT environment variable, or port 85 by default.

const express = require('express');
const cors = require('cors');
const bodyParser = require('body-parser');
const fs = require('fs');
const path = require('path');

const app = express();
app.use(cors());
app.use(bodyParser.json());

const OPENAI_VERIFICATION_TOKEN = process.env.OPENAI_VERIFICATION_TOKEN;
const APP_PORT = process.env.APP_PORT || 85;

let todos = {};

app.post("/todos/:username", (req, res) => {
    let username = req.params.username;
    if (!(username in todos)) {
        todos[username] = [];
    }
    todos[username].push(req.body.todo);
    res.status(200).send('OK');
});

app.get("/todos/:username", (req, res) => {
    let username = req.params.username;
    res.status(200).send(JSON.stringify(todos[username] || []));
});

app.delete("/todos/:username", (req, res) => {
    let username = req.params.username;
    let todoIdx = req.body.todo_idx;
    if (0 <= todoIdx && todoIdx < todos[username].length) {
        todos[username].splice(todoIdx, 1);
    }
    res.status(200).send('OK');
});

app.get("/logo.png", (req, res) => {
    res.sendFile(path.join(__dirname, 'logo.png'));
});

app.get("/.well-known/ai-plugin.json", (req, res) => {
    console.log('manifest');
    let host = req.headers.host;
    fs.readFile("manifest.json", 'utf8', function(err, data){
        if (err) throw err;
        data = data.replace(/PLUGIN_HOSTNAME/g, `https://${host}`).replace(/OPENAI_VERIFICATION_TOKEN/g, OPENAI_VERIFICATION_TOKEN)
        res.send(data);
    });
});

app.get("/openapi.yaml", (req, res) => {
    let host = req.headers.host;
    fs.readFile("openapi.yaml", 'utf8', function(err, data){
        if (err) throw err;
        data = data.replace(/PLUGIN_HOSTNAME/g, `https://${host}`);
        res.send(data);
    });
});

const querystring = require('querystring');
const { privateDecrypt } = require('crypto');

const OPENAI_CLIENT_ID = "id";
const OPENAI_CLIENT_SECRET = "secret";
const OPENAI_CODE = "abc123";
const OPENAI_TOKEN = "def456";

app.get('/oauth', (req, res) => {
    const kvps = {};
    const parts = req.originalUrl.split('?')[1].split('&');

    parts.forEach(part => {
        const [k, v] = part.split('=');
        kvps[k] = decodeURIComponent(v);
    });

    console.log("OAuth key value pairs from the ChatGPT Request: ", kvps);
    const url = `${kvps["redirect_uri"]}?code=${OPENAI_CODE}`;
    console.log("URL: ", url);
    res.send(`<a href="${url}">Click to authorize</a>`);
});

app.post("/auth/oauth_exchange", (req, res) => {
    if (req.body.client_id !== OPENAI_CLIENT_ID) {
        throw "bad client ID";
    }
    if (req.body.client_secret !== OPENAI_CLIENT_SECRET) {
        throw "bad client secret";
    }
    if (req.body.code !== OPENAI_CODE) {
        throw "bad code";
    }

    res.send({
        "access_token": OPENAI_TOKEN,
        "token_type": "bearer"
    });
});

app.listen(APP_PORT, '0.0.0.0', () => {
    console.log('Server is running on port ' + APP_PORT);
});

Now that you understand the basics of setting up a OAuth ChatGPT plugin, you can continue exploring and developing your project. Remember, practice is key in software development. Don’t be afraid to experiment and learn from any challenges you encounter along the way. Happy coding!

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *