How to Authenticate an OpenAI Plugin: Declarative Policies and OAuth PKCE Implementation
OpenAI plugins use a declarative authentication model where the marketplace policy defines when authentication occurs (ON_INSTALL or ON_USE), the plugin manifest describes the required OAuth scopes and metadata, and the plugin backend implements the OAuth 2.0 PKCE flow to securely obtain and store access tokens.
The openai/plugins repository demonstrates that authenticating an OpenAI plugin requires coordinating two distinct layers: a declarative policy that tells the Codex runtime when to trigger authentication, and an imperative implementation that handles the actual OAuth exchange. Understanding how to configure the marketplace entry and implement the PKCE flow in your plugin backend is essential for securing third-party API access.
Configuring the Marketplace Authentication Policy
Authentication timing is controlled declaratively in .agents/plugins/marketplace.json. Each plugin entry contains a policy.authentication field that accepts two values:
- ON_INSTALL: Authentication is performed once immediately after the user installs the plugin. The plugin stores tokens for subsequent use.
- ON_USE: Authentication is performed each time the user invokes a skill that requires credentials. This is useful for short-lived or per-action authorization.
According to the specification in .agents/skills/plugin-creator/references/plugin-json-spec.md (lines 54-56), the default value is ON_INSTALL when the field is omitted.
The Zoom plugin provides a concrete example in .agents/plugins/marketplace.json (lines 15-16):
{
"name": "zoom",
"source": {
"source": "local",
"path": "./plugins/zoom"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "Communication"
}
Understanding the Plugin Manifest Role
While the plugin.json manifest does not contain authentication settings directly, it defines the scopes, URLs, and UI metadata required for the OAuth flow. Located at plugins/<name>/.codex-plugin/plugin.json, this file describes the external service interface.
For example, plugins/zoom/.codex-plugin/plugin.json defines:
{
"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 the OAuth 2.0 PKCE Flow
Most OpenAI plugins use the OAuth 2.0 PKCE (Proof Key for Code Exchange) flow to obtain access tokens securely. As implemented in the Zoom reference at plugins/zoom/skills/zoom-apps-sdk/references/oauth.md, the flow involves generating a PKCE pair, redirecting to the provider, exchanging the authorization code, and securely storing the resulting tokens.
Generate PKCE Credentials
First, generate the PKCE verifier and challenge. From plugins/zoom/skills/zoom-apps-sdk/references/oauth.md (lines 17-28):
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 (typically in session or temporary storage) and send the challenge to the authorization endpoint.
Handle Web-Based Authorization
For server-side implementations, handle the redirect from the OAuth provider. From plugins/zoom/skills/zoom-apps-sdk/references/oauth.md (lines 55-92):
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 …
});
Implement In-Client OAuth (Best UX)
For the optimal user experience, handle OAuth within the client application itself. From plugins/zoom/skills/zoom-apps-sdk/references/oauth.md (lines 99-112):
// 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 Patterns
After obtaining tokens, you must store them securely and handle expiration. The Zoom reference outlines two primary patterns in plugins/zoom/skills/zoom-apps-sdk/references/oauth.md (lines 91-95):
- Redis: Use for multi-instance production deployments where multiple servers need access to the same token state.
- Session Cookie: Suitable for single-server applications with simpler deployment requirements.
Refresh tokens are single-use. As noted in lines 64-66 of the OAuth reference, each time you exchange a refresh token for a new access token, the provider issues a new refresh token that must replace the old one in storage.
Complete Integration Example
When a user installs a plugin with "authentication": "ON_INSTALL":
- The Codex UI prompts the user to authorize the external service.
- Your backend executes the PKCE flow, stores the tokens (e.g., in Redis), and returns success.
- Subsequent skill calls retrieve the stored token to call the third-party API.
For "authentication": "ON_USE", step 1 is deferred until the first skill invocation requiring credentials, allowing per-action authorization.
Summary
- Declarative control: Set
policy.authenticationtoON_INSTALLorON_USEin.agents/plugins/marketplace.jsonto define when authentication occurs. - Manifest metadata: Define OAuth scopes and service URLs in
plugins/<name>/.codex-plugin/plugin.jsonto describe the external service interface. - PKCE implementation: Generate a code verifier and challenge, exchange the authorization code for tokens, and validate CSRF state to secure the flow.
- Token lifecycle: Store tokens securely (Redis for scale, sessions for simplicity) and handle single-use refresh tokens properly.
- UX optimization: Implement in-client OAuth when possible to avoid redirecting users away from the chat interface.
Frequently Asked Questions
What is the difference between ON_INSTALL and ON_USE authentication policies?
ON_INSTALL triggers authentication immediately after the user installs the plugin, storing tokens for all future skill invocations. ON_USE defers authentication until a skill actually requires credentials, which is useful for sensitive operations or when tokens should not persist between sessions. According to the plugin specification, ON_INSTALL is the default behavior when no policy is specified.
Does the plugin.json manifest contain OAuth client secrets?
No. The plugin.json manifest located at plugins/<name>/.codex-plugin/plugin.json contains only public metadata such as the privacy policy URL, terms of service URL, and capabilities. Client secrets and OAuth implementation details remain in your backend environment variables and server code, never in the manifest that ships with the plugin.
How do I handle token refresh in an OpenAI plugin?
Store the refresh token securely alongside the access token. When the access token expires, send the refresh token to the provider's token endpoint. According to the Zoom OAuth reference in plugins/zoom/skills/zoom-apps-sdk/references/oauth.md, refresh tokens are single-use, meaning each refresh request returns both a new access token and a new refresh token that must replace the previous one in your storage system.
Can I use authentication methods other than OAuth 2.0 PKCE?
While the OpenAI plugins repository demonstrates OAuth 2.0 PKCE as the standard pattern (used by the Zoom plugin), the architecture supports any authentication flow you implement in your backend. The marketplace policy (ON_INSTALL or ON_USE) is agnostic to the specific protocol, allowing you to implement API keys, JWT tokens, or custom authentication schemes as long as your backend handles the credential exchange and storage securely.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →