How to Handle Plugin Authentication and API Keys Securely in OpenAI Plugins

OpenAI plugins enforce secure credential management by declaring authentication requirements in plugin.json manifests and mandating that all secrets be supplied via environment variables rather than hard-coded strings.

The openai/plugins repository defines the reference architecture for ChatGPT plugin security. To handle plugin authentication and API keys securely, developers must implement the three auth patterns found in the source code—API keys, OAuth 2.0, and custom tokens—while ensuring credentials are never exposed to clients or committed to version control.

Authentication Mechanisms Declared in plugin.json

Each plugin declares its security schema in a .codex-plugin/plugin.json manifest. The runtime reads this file to determine which environment variables to load and which headers to attach to outbound requests.

API-Key Authentication

API-key authentication is used for simple services that accept static bearer tokens. In plugins/airtable/.codex-plugin/plugin.json and plugins/stripe/.codex-plugin/plugin.json, the manifest specifies an apiKey type that maps to an environment variable.

The runtime injects the key into the Authorization: Bearer <key> header at request time. The source code never contains the literal token; instead, the plugin crashes with a clear error if the environment variable is undefined.

OAuth 2.0 Flow

OAuth 2.0 handles user-consent scenarios such as Zoom or Google integrations. The plugins/zoom/.codex-plugin/plugin.json manifest declares an oauth block containing the required scopes (e.g., meeting:read, meeting:write).

The plugin initiates the standard authorization-code flow, stores the resulting access and refresh tokens in server-side session storage, and automatically refreshes expired tokens before they reach the API. Refresh tokens are persisted in encrypted storage and are never transmitted to the client.

Custom Token Handling

Custom tokens support proprietary formats such as JWTs signed by third-party services. The plugin backend generates these tokens using secrets stored in environment variables and returns short-lived, per-request tokens to the client. This pattern minimizes the blast radius of a compromised token.

Security Best Practices from the Source Code

The repository enforces four non-negotiable security rules across all authentication implementations.

  • Never commit secrets. All manifests reference environment variables. The .github/workflows/ci.yml pipeline includes a step that scans for high-entropy strings and blocks the pull request if a potential secret is detected.

  • Least-privilege scopes. OAuth declarations in plugin.json request only the minimum scopes required for the plugin’s functionality. The Zoom manifest, for example, explicitly omits administrative scopes even when the API supports them.

  • Token rotation. Refresh tokens are stored server-side and exchanged for new access tokens automatically. When a user revokes access, the refresh token is immediately invalidated in the secret store.

  • Secure defaults. If a required credential is missing, the plugin raises a runtime error and aborts the request before any network call is made.

Implementing Secure Authentication: Code Examples

The following patterns demonstrate how to load credentials safely according to the repository’s conventions.

Loading an API Key from Environment Variables (Node.js)

This example mirrors the implementation found in API-key-based plugins such as Airtable and Stripe.

// The plugin.json declares: "auth": { "type": "apiKey", "envVar": "EXAMPLE_API_KEY" }

const axios = require('axios');

// Read the key from the environment – never from source code
const apiKey = process.env.EXAMPLE_API_KEY;
if (!apiKey) {
  throw new Error('Missing EXAMPLE_API_KEY – set it in your .env file.');
}

// Attach the key to the Authorization header
async function callExampleApi(payload) {
  const response = await axios.post(
    'https://api.example.com/v1/endpoint',
    payload,
    { headers: { Authorization: `Bearer ${apiKey}` } }
  );
  return response.data;
}

OAuth 2.0 Authorization Flow (Python/Flask)

This implementation aligns with the Zoom plugin’s OAuth structure defined in plugins/zoom/.codex-plugin/plugin.json.

import os
from flask import Flask, redirect, request, session
import requests

app = Flask(__name__)
app.secret_key = os.getenv('FLASK_SECRET_KEY')  # Never hard-code

CLIENT_ID = os.getenv('ZOOM_CLIENT_ID')
CLIENT_SECRET = os.getenv('ZOOM_CLIENT_SECRET')
REDIRECT_URI = 'https://myapp.com/oauth/callback'

@app.route('/auth')
def start_auth():
    auth_url = (
        'https://zoom.us/oauth/authorize'
        f'?response_type=code&client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}'
    )
    return redirect(auth_url)

@app.route('/oauth/callback')
def oauth_callback():
    code = request.args.get('code')
    token_resp = requests.post(
        'https://zoom.us/oauth/token',
        params={
            'grant_type': 'authorization_code',
            'code': code,
            'redirect_uri': REDIRECT_URI
        },
        auth=(CLIENT_ID, CLIENT_SECRET),
    )
    data = token_resp.json()
    # Store tokens securely server-side (e.g., encrypted database)

    session['zoom_access'] = data['access_token']
    session['zoom_refresh'] = data['refresh_token']
    return 'Zoom authenticated!'

Refreshing an OAuth Token Automatically (JavaScript)

Use this helper to maintain long-lived sessions without user intervention, as required by the repository’s token-rotation policy.

async function refreshToken(refreshToken, clientId, clientSecret) {
  const resp = await fetch('https://provider.com/oauth/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'refresh_token',
      refresh_token: refreshToken,
      client_id: clientId,
      client_secret: clientSecret,
    }),
  });
  const data = await resp.json();
  // Persist the new access and refresh tokens securely
  return data;
}

Key Source Files for Authentication Logic

The following files in openai/plugins define the contract for secure credential handling:

Summary

  • Declare auth type in plugin.json – Choose between apiKey, oauth, or custom schemes in the manifest file.
  • Use environment variables exclusively – Load all secrets via process.env or equivalent; never hard-code credentials.
  • Implement OAuth token rotation – Store refresh tokens server-side and exchange them before expiry.
  • Enforce least-privilege scopes – Request only the permissions required for the plugin’s functionality.
  • Block secrets in CI – Leverage the repository’s .github/workflows/ci.yml checks to prevent accidental commits of API keys.

Frequently Asked Questions

How should I store API keys when developing a new OpenAI plugin?

Store API keys in a .env file that is listed in .gitignore, or use a production secret manager such as AWS Secrets Manager or HashiCorp Vault. The plugin.json manifest should reference the environment variable name, and your application code should read the variable at runtime. According to the source code in plugins/airtable/.codex-plugin/plugin.json, the runtime will fail fast with a clear error if the variable is unset.

What is the difference between API-key and OAuth authentication in this repository?

API-key authentication is suited for service-to-service calls where a static token is sufficient, such as the Stripe integration. OAuth 2.0 is required when the plugin acts on behalf of a user and requires consent, as seen in the Zoom plugin. OAuth implementations must handle the full authorization-code flow, store tokens server-side, and implement automatic refresh logic.

How does the repository prevent accidental exposure of secrets?

The .github/workflows/ci.yml workflow scans every pull request for high-entropy strings that match common API key patterns. If a potential secret is detected, the build fails and the merge is blocked. Additionally, the plugin.json schema does not allow inline credential strings, enforcing indirection through environment variables.

When should I rotate OAuth refresh tokens?

Refresh tokens should be rotated whenever the authorization server issues a new one, or immediately after a user revokes access. The source code patterns in the Zoom plugin demonstrate storing the new refresh token returned during the refresh grant and discarding the old one, ensuring that compromised tokens have a short lifetime.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →