# How to Authenticate an OpenAI Plugin: OAuth PKCE and Declarative Policies Explained

> Learn how to authenticate an OpenAI plugin using OAuth PKCE and declarative policies. Secure plugin tokens with this comprehensive guide from the openai plugins repository.

- Repository: [OpenAI/plugins](https://github.com/openai/plugins)
- Tags: how-to-guide
- Published: 2026-06-26

---

**OpenAI plugins use a declarative authentication model defined in the marketplace policy and implemented via OAuth 2.0 PKCE flows, with tokens stored securely by the plugin backend.**

Authenticating an OpenAI plugin requires understanding the two-tier declarative model used in the openai/plugins repository. The system separates authentication timing policies from implementation details, allowing developers to specify when authorization occurs while implementing standard OAuth 2.0 PKCE flows for credential exchange.

## Understanding the Declarative Authentication Model

The authentication architecture relies on two distinct configuration layers that work together to control when and how users authorize third-party services.

### Marketplace Authentication Policy in [`marketplace.json`](https://github.com/openai/plugins/blob/main/marketplace.json)

The [`.agents/plugins/marketplace.json`](https://github.com/openai/plugins/blob/main/.agents/plugins/marketplace.json) file controls when authentication triggers through the `policy.authentication` field. This field accepts two values:

- `ON_INSTALL` – Authentication occurs **once** immediately after installation, with tokens persisted for subsequent use.
- `ON_USE` – Authentication occurs **each time** a user invokes a skill requiring credentials, suitable for short-lived authorization contexts.

If omitted, the default value is `ON_INSTALL` according to the plugin specification in [`.agents/skills/plugin-creator/references/plugin-json-spec.md`](https://github.com/openai/plugins/blob/main/.agents/skills/plugin-creator/references/plugin-json-spec.md) (lines 54-56).

Example from the Zoom plugin entry:

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

```

### Plugin Manifest Structure

While the [`plugin.json`](https://github.com/openai/plugins/blob/main/plugin.json) manifest located at `plugins/<name>/.codex-plugin/plugin.json` does not contain authentication settings directly, it defines the scopes and metadata required for the OAuth flow. The manifest specifies capabilities, website URLs, and privacy policies that the authorization interface displays.

Example from [`plugins/zoom/.codex-plugin/plugin.json`](https://github.com/openai/plugins/blob/main/plugins/zoom/.codex-plugin/plugin.json):

```json
{
  "name": "zoom",
  "version": "1.0.2",
  "keywords": [ "zoom", "oauth", "meeting-sdk" ],
  "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 Flows

OpenAI plugins implement OAuth 2.0 with Proof Key for Code Exchange (PKCE) to securely obtain access tokens without exposing client secrets in frontend code.

### Generating PKCE Challenges

The PKCE flow begins with generating a cryptographically random verifier and challenge. In [`plugins/zoom/skills/zoom-apps-sdk/references/oauth.md`](https://github.com/openai/plugins/blob/main/plugins/zoom/skills/zoom-apps-sdk/references/oauth.md) (lines 17-28), the implementation uses Node.js crypto:

```javascript
const crypto = require('crypto');

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

```

### Web-Based Authorization Handler

For server-side implementations, the plugin handles the redirect from the OAuth provider and exchanges the authorization code for tokens. The reference implementation in [`plugins/zoom/skills/zoom-apps-sdk/references/oauth.md`](https://github.com/openai/plugins/blob/main/plugins/zoom/skills/zoom-apps-sdk/references/oauth.md) (lines 55-92) demonstrates this pattern:

```javascript
app.get('/auth', async (req, res) => {
  const { code, state } = req.query;
  // Validate CSRF state …
  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, obtain deeplink, redirect user …
});

```

### In-Client OAuth Flow

For enhanced user experience, plugins can use in-client OAuth where the authorization happens within the host application. As shown in [`plugins/zoom/skills/zoom-apps-sdk/references/oauth.md`](https://github.com/openai/plugins/blob/main/plugins/zoom/skills/zoom-apps-sdk/references/oauth.md) (lines 99-112):

```javascript
// Front‑end
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

After obtaining tokens, plugins must implement secure storage and handle token expiration.

### Secure Token Storage

The reference implementation outlines multiple storage strategies depending on deployment architecture. For production multi-instance deployments, Redis provides shared state across servers, while single-server applications can use encrypted session cookies. The Zoom plugin reference (lines 91-95) suggests:

```javascript
// Store in Redis for distributed systems
await redis.set(`zoom:tokens:${sessionId}`, JSON.stringify({
  access_token: tokenResponse.data.access_token,
  refresh_token: tokenResponse.data.refresh_token,
  expires_at: Date.now() + tokenResponse.data.expires_in * 1000
}));

```

### Refreshing Single-Use Tokens

According to [`plugins/zoom/skills/zoom-apps-sdk/references/oauth.md`](https://github.com/openai/plugins/blob/main/plugins/zoom/skills/zoom-apps-sdk/references/oauth.md) (lines 64-66), refresh tokens are **single-use** only. After each refresh request, the provider issues a new refresh token alongside the access token. Implementations must update their storage to replace the old refresh token with the new one to prevent authentication failures.

## Complete Implementation Example

Putting together the PKCE generation, challenge endpoint, and token exchange creates a complete authentication flow:

```javascript
const crypto = require('crypto');
const axios = require('axios');

// PKCE generation utility
function generatePKCE() {
  const verifier = crypto.randomBytes(32).toString('hex');
  const challenge = crypto.createHash('sha256')
    .update(verifier)
    .digest('base64url');
  return { verifier, challenge };
}

// Challenge endpoint
app.get('/api/auth/challenge', (req, res) => {
  const { challenge, verifier } = generatePKCE();
  req.session.codeVerifier = verifier;
  req.session.state = crypto.randomBytes(16).toString('hex');
  
  res.json({
    codeChallenge: challenge,
    state: req.session.state,
    authUrl: `https://zoom.us/oauth/authorize?client_id=${process.env.ZOOM_APP_CLIENT_ID}` +
             `&response_type=code&redirect_uri=${process.env.ZOOM_APP_REDIRECT_URI}` +
             `&code_challenge=${challenge}&code_challenge_method=S256&state=${req.session.state}`
  });
});

// Token exchange
app.post('/api/auth/token', async (req, res) => {
  const { code, state } = req.body;
  if (state !== req.session.state) return res.status(403).send('Invalid state');
  
  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')
    }
  });
  
  // Secure storage implementation here
  res.json({ ok: true });
});

```

## Summary

Authenticating an OpenAI plugin requires coordination between declarative configuration and OAuth implementation:

- The `policy.authentication` field in [`.agents/plugins/marketplace.json`](https://github.com/openai/plugins/blob/main/.agents/plugins/marketplace.json) determines whether authentication occurs `ON_INSTALL` or `ON_USE`, defaulting to `ON_INSTALL` if unspecified.
- The [`plugin.json`](https://github.com/openai/plugins/blob/main/plugin.json) manifest defines metadata and capabilities but does not contain authentication credentials.
- OAuth 2.0 PKCE flows require generating a cryptographically secure verifier and challenge, exchanging the authorization code for tokens, and storing refresh tokens securely.
- Refresh tokens are single-use and must be replaced in storage after each refresh operation.
- Production deployments should use distributed storage like Redis for token persistence across multiple server instances.

## Frequently Asked Questions

### What is the difference between `ON_INSTALL` and `ON_USE` authentication policies?

The `ON_INSTALL` policy triggers authentication immediately after the user installs the plugin, storing tokens for persistent use across all subsequent skill invocations. The `ON_USE` policy defers authentication until the first time a user executes a skill requiring credentials, which is useful for short-lived authorizations or when credentials should not persist between sessions.

### Where should I store OAuth tokens in an OpenAI plugin?

Store tokens in a secure, encrypted backend storage system appropriate for your deployment architecture. For multi-instance production environments, use Redis or a database with encryption at rest. For single-server deployments, encrypted session cookies suffice. Never store tokens in frontend code or local browser storage.

### Does the [`plugin.json`](https://github.com/openai/plugins/blob/main/plugin.json) file contain OAuth client secrets?

No, the [`plugin.json`](https://github.com/openai/plugins/blob/main/plugin.json) manifest located at `plugins/<name>/.codex-plugin/plugin.json` contains only metadata such as capabilities, website URLs, and privacy policies. It does not contain authentication settings or secrets. Client secrets and access tokens belong exclusively in your backend implementation and secure storage.

### How do I handle token expiration in OpenAI plugins?

Implement token refresh logic that detects expired access tokens and uses the stored refresh token to obtain new credentials. According to the Zoom plugin reference in [`plugins/zoom/skills/zoom-apps-sdk/references/oauth.md`](https://github.com/openai/plugins/blob/main/plugins/zoom/skills/zoom-apps-sdk/references/oauth.md) (lines 64-66), remember that refresh tokens are single-use—each refresh request returns a new refresh token that must replace the old one in your storage system.