How to Handle Authentication for OpenAI Plugins: Complete OAuth 2.0 PKCE Implementation Guide

OpenAI plugins use a declarative authentication model where the marketplace policy defines when to authenticate and the plugin implements OAuth 2.0 PKCE to securely exchange tokens, supporting both one-time installation auth and per-use authentication flows.

Handling authentication for your OpenAI plugin requires understanding the two-tier declaration system in the openai/plugins repository. The framework separates authentication timing (controlled by marketplace policy) from the actual OAuth implementation (handled by your plugin backend), enabling secure credential management through standardized OAuth 2.0 PKCE flows.

Understanding the Declarative Authentication Model

OpenAI plugins follow a declarative authentication architecture split between marketplace configuration and plugin manifest metadata. This separation allows the Codex runtime to know when credentials are required while letting you control how those credentials are obtained.

Marketplace Policy Configuration

The .agents/plugins/marketplace.json file determines when authentication occurs through the policy.authentication field. Two values control the timing:

  • ON_INSTALL: Authentication happens once immediately after installation, with tokens stored for subsequent skill calls (default behavior if omitted)
  • ON_USE: Authentication occurs each time a user invokes a skill requiring credentials, ideal for short-lived or highly sensitive operations

As implemented in the Zoom plugin entry at line 15, the configuration appears as:

{
  "name": "zoom",
  "source": {
    "source": "local",
    "path": "./plugins/zoom"
  },
  "policy": {
    "installation": "AVAILABLE",
    "authentication": "ON_INSTALL"
  },
  "category": "Communication"
}

According to the plugin specification at .agents/skills/plugin-creator/references/plugin-json-spec.md lines 54-56, ON_INSTALL serves as the default when the authentication field is not explicitly defined.

Plugin Manifest Structure

While the manifest at plugins/<name>/.codex-plugin/plugin.json does not contain authentication settings directly, it defines the scopes, URLs, and UI metadata required by the OAuth flow. The Zoom manifest demonstrates the standard structure:

{
  "name": "zoom",
  "version": "1.0.2",
  "interface": {
    "capabilities": ["Interactive", "Read", "Write"],
    "websiteURL": "https://developers.zoom.us/",
    "privacyPolicyURL": "https://www.zoom.com/en/trust/privacy/",
    "termsOfServiceURL": "https://www.zoom.com/en/trust/terms/"
  }
}

Implementing OAuth 2.0 PKCE for OpenAI Plugins

The reference implementation in plugins/zoom/skills/zoom-apps-sdk/references/oauth.md establishes the standard pattern: generate a PKCE pair, redirect to the provider, exchange the authorization code for tokens, and securely store the results.

Generate PKCE Credentials

At lines 17-28 of the Zoom OAuth reference, the implementation generates the code verifier and challenge using Node.js crypto:

const crypto = require('crypto');

// Generate PKCE pair
const verifier = crypto.randomBytes(32).toString('hex');
const challenge = crypto.createHash('sha256')
  .update(verifier)
  .digest('base64url');

Store the verifier server-side (in session or temporary storage) while sending the challenge to the authorization endpoint.

Handle Web-Based Authorization

For server-rendered flows, your Express handler validates the CSRF state and exchanges the code for tokens, as shown at lines 55-92:

app.get('/auth', async (req, res) => {
  const { code, state } = req.query;
  // Validate CSRF state first
  const tokenResponse = await axios.post('https://zoom.us/oauth/token', null, {
    params: {
      grant_type: 'authorization_code',
      code,
      redirect_uri: process.env.ZOOM_APP_REDIRECT_URI,
      code_verifier: req.session.codeVerifier
    },
    headers: {
      Authorization: 'Basic ' + Buffer.from(
        `${process.env.ZOOM_APP_CLIENT_ID}:${process.env.ZOOM_APP_CLIENT_SECRET}`
      ).toString('base64')
    }
  });
  
  // Store tokens securely before redirecting
  await storeTokens(req.session.id, tokenResponse.data);
  res.redirect('/success');
});

Implement In-Client OAuth Flow

For better UX within chat interfaces, use the in-client pattern at lines 99-112. The frontend requests a challenge, listens for the authorization event, and sends the code to your backend:

// Frontend code
const { codeChallenge, state } = await fetch('/api/auth/challenge').then(r => r.json());

zoomSdk.addEventListener('onAuthorized', async (event) => {
  await fetch('/api/auth/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ code: event.code, state: event.state })
  });
});

await zoomSdk.authorize({ codeChallenge, state });

Token Storage and Refresh Strategies

OpenAI plugins support multiple storage patterns depending on your deployment architecture, as documented at lines 91-95 of the Zoom reference.

Secure Storage Options

  • Redis: Use for multi-instance production deployments requiring shared state
  • Session cookies: Acceptable for single-server applications with encrypted storage
  • Encrypted databases: For persistent long-term token storage across restarts

Refresh Token Handling

Access tokens expire, but refresh tokens allow renewal. As noted at lines 64-66, Zoom's implementation (and standard OAuth 2.0 security practice) treats refresh tokens as single-use—each refresh request returns a new refresh token alongside the new access token.

Implement a wrapper function to check expiration before API calls:

async function getValidToken(sessionId) {
  const tokens = await redis.get(`plugin:tokens:${sessionId}`);
  const tokenData = JSON.parse(tokens);
  
  if (Date.now() > tokenData.expires_at) {
    const refreshResponse = await axios.post('https://zoom.us/oauth/token', null, {
      params: {
        grant_type: 'refresh_token',
        refresh_token: tokenData.refresh_token
      },
      headers: {
        Authorization: 'Basic ' + Buffer.from(
          `${process.env.CLIENT_ID}:${process.env.CLIENT_SECRET}`
        ).toString('base64')
      }
    });
    
    // Store new tokens (includes new refresh_token)
    await redis.set(`plugin:tokens:${sessionId}`, JSON.stringify({
      access_token: refreshResponse.data.access_token,
      refresh_token: refreshResponse.data.refresh_token,
      expires_at: Date.now() + refreshResponse.data.expires_in * 1000
    }));
    
    return refreshResponse.data.access_token;
  }
  
  return tokenData.access_token;
}

Summary

  • OpenAI plugin authentication uses a declarative model split between marketplace policy (ON_INSTALL or ON_USE) and OAuth 2.0 PKCE implementation
  • Configure authentication timing in .agents/plugins/marketplace.json—defaulting to ON_INSTALL when unspecified
  • Implement the standard PKCE flow: generate verifier/challenge, redirect to provider, exchange code for tokens, and store securely
  • Support both web-based redirects and in-client OAuth flows depending on your UI requirements
  • Store tokens in Redis for distributed systems or encrypted sessions for single-server deployments
  • Handle refresh tokens as single-use credentials, rotating storage after each renewal

Frequently Asked Questions

What is the difference between ON_INSTALL and ON_USE authentication policies?

ON_INSTALL authenticates the user once immediately after plugin installation, storing tokens for all future skill invocations, while ON_USE defers authentication until the first time a skill requiring credentials is executed. Use ON_USE for sensitive operations or when tokens expire quickly, and ON_INSTALL for standard API integrations.

Where do I configure the OAuth scopes for my OpenAI plugin?

OAuth scopes are not defined in the authentication section itself. Instead, they are configured in your OAuth provider's developer console and referenced in the authorization URL generated by your backend. The plugin.json manifest contains metadata like websiteURL and capabilities, but the actual scope requests happen during the PKCE flow when redirecting to the provider's authorize endpoint.

How do I handle token storage in a serverless OpenAI plugin environment?

For serverless deployments, avoid in-memory sessions. Instead, store PKCE verifiers in short-lived encrypted cookies or temporary KV stores during the exchange phase, then persist tokens in a database like DynamoDB, Firestore, or Redis. Ensure your token refresh logic handles the single-use nature of refresh tokens atomically to prevent race conditions in concurrent serverless invocations.

Why does my OpenAI plugin need PKCE instead of basic OAuth 2.0?

PKCE (Proof Key for Code Exchange) prevents authorization code interception attacks by requiring a cryptographic secret (the code verifier) that is never transmitted to the client. Since OpenAI plugins often run in browser contexts or mobile clients where the redirect URI might be intercepted, PKCE ensures that only your server backend—possessing the original verifier—can exchange the authorization code for tokens.

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 →