# How to Authenticate Users with Your Plugin: A Complete Guide to the OpenAI Plugins Repository

> Authenticate users with your OpenAI plugin using OAuth 2.0, JWT, OIDC, or bearer tokens. Learn to implement secure user authentication for your plugin.

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

---

**You can authenticate users with your plugin using OAuth 2.0 with PKCE, JWT signatures, OIDC platform tokens, or CLI-generated bearer tokens depending on the target service, as implemented in the `openai/plugins` repository.**

The `openai/plugins` repository collects production-ready skills for connecting ChatGPT to external platforms, and understanding how to authenticate users with your plugin is critical for secure API access. Each service-specific skill documents its own identity flow—ranging from Zoom's multi-modal OAuth to Wix's command-line tokens—so you can delegate authorization to the provider rather than building custom identity systems from scratch.

## OAuth 2.0 and PKCE in the Zoom Plugin

The Zoom plugin provides the most comprehensive authentication examples in the repository, supporting web-based OAuth, In-Client PKCE, and Server-to-Server flows. The core references are [`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) and [`plugins/zoom/skills/zoom-apps-sdk/examples/in-client-oauth.md`](https://github.com/openai/plugins/blob/main/plugins/zoom/skills/zoom-apps-sdk/examples/in-client-oauth.md).

### Web-Based Authorization Code Flow

In the standard Zoom OAuth flow, your plugin redirects the user to Zoom's authorization endpoint. After consent, Zoom sends a temporary `code` to your whitelisted `redirect_uri`, which your backend exchanges for an access token. The request/response handling is demonstrated in [`plugins/zoom/skills/zoom-apps-sdk/examples/quick-start.md`](https://github.com/openai/plugins/blob/main/plugins/zoom/skills/zoom-apps-sdk/examples/quick-start.md).

The required environment variables are defined in [`plugins/zoom/skills/zoom-apps-sdk/references/environment-variables.md`](https://github.com/openai/plugins/blob/main/plugins/zoom/skills/zoom-apps-sdk/references/environment-variables.md) and must never be hard-coded:

- `ZOOM_APP_CLIENT_ID`
- `ZOOM_APP_CLIENT_SECRET`
- `ZOOM_APP_REDIRECT_URI`

### In-Client OAuth with PKCE

For embedded Zoom apps, the repository implements **PKCE (Proof Key for Code Exchange)** to prevent interception attacks. The backend generates a `code_challenge` and `code_verifier` pair, then exposes them via an endpoint that the Zoom client consumes.

According to [`plugins/zoom/skills/zoom-apps-sdk/examples/in-client-oauth.md`](https://github.com/openai/plugins/blob/main/plugins/zoom/skills/zoom-apps-sdk/examples/in-client-oauth.md), the client calls `zoomSdk.authorize()` with the challenge instead of performing a full browser redirect:

```javascript
// client.js — runs inside the Zoom embedded browser
async function doAuth() {
  const challenge = await fetch('/api/auth/challenge').then(r => r.json());
  await zoomSdk.authorize({
    codeChallenge: challenge.codeChallenge,
    state: challenge.state,
  });
}

```

The backend then exchanges the returned authorization `code` for tokens using the verifier stored server-side.

### Server-to-Server and JWT Authentication

For background or admin-level operations that do not require a user context, the Zoom plugin supports **Server-to-Server OAuth**. This flow directly obtains an access token using client credentials, skipping the authorization code step entirely.

Zoom's Meeting SDK additionally uses short-lived **JWTs** signed with your SDK key and secret. The JWT proves application identity rather than end-user identity, and is generated server-side before being passed to the client's `InitSDK` call. Details on both patterns are documented in [`plugins/zoom/skills/meeting-sdk/references/authorization.md`](https://github.com/openai/plugins/blob/main/plugins/zoom/skills/meeting-sdk/references/authorization.md).

For common token error scenarios, consult [`plugins/zoom/skills/oauth/references/oauth-errors.md`](https://github.com/openai/plugins/blob/main/plugins/zoom/skills/oauth/references/oauth-errors.md).

## Vercel OIDC and API Token Authentication

The Vercel plugin demonstrates platform-native authentication through **OIDC tokens** that Vercel automatically injects into serverless functions at runtime.

As documented in [`plugins/vercel/skills/vercel-api/SKILL.md`](https://github.com/openai/plugins/blob/main/plugins/vercel/skills/vercel-api/SKILL.md), you can access the pre-authenticated token directly from `process.env`:

```javascript
// Vercel serverless function
export default async (req, res) => {
  const token = process.env.VERCEL_TOKEN;
  const resp = await fetch('https://api.vercel.com/v9/projects', {
    headers: { Authorization: `Bearer ${token}` },
  });
  const data = await resp.json();
  res.json(data);
};

```

For local development, the same file recommends setting a static `VERCEL_QUEUE_API_TOKEN` or similar personal token in your local environment.

## Supabase OAuth 2.1 Authentication

The Supabase plugin follows the standard **OAuth 2.1** flow for end-user authentication. The plugin redirects the user to Supabase's `/authorize` endpoint, receives an `access_token` and optional `refresh_token`, and stores the token for subsequent Data API calls.

Supabase also supports **JWT-based service roles** for server-side operations that do not require a user session.

The full flow is outlined in [`plugins/supabase/skills/supabase/SKILL.md`](https://github.com/openai/plugins/blob/main/plugins/supabase/skills/supabase/SKILL.md).

## Wix CLI Token Authentication

Wix headless plugins use a **CLI-generated, account-scoped token** rather than a browser-based OAuth flow. After running `npx @wix/cli@latest login`, the CLI caches credentials in `~/.wixrc`.

As described in [`plugins/wix/skills/wix-headless/references/shared/AUTHENTICATION.md`](https://github.com/openai/plugins/blob/main/plugins/wix/skills/wix-headless/references/shared/AUTHENTICATION.md), you retrieve a site-scoped bearer token programmatically and attach it to API requests:

```bash
TOKEN=$(npx @wix/cli@latest token --site "$SITE_ID")
curl -H "Authorization: Bearer $TOKEN" https://www.wixapis.com/...

```

This pattern eliminates the need to manage client secrets or redirect URIs for Wix integrations.

## Reusable Node.js Boilerplate to Authenticate Users with Your Plugin

Below is a minimal Express backend adapted from the Zoom In-Client OAuth implementation in [`plugins/zoom/skills/zoom-apps-sdk/examples/in-client-oauth.md`](https://github.com/openai/plugins/blob/main/plugins/zoom/skills/zoom-apps-sdk/examples/in-client-oauth.md). You can repurpose this pattern for any OAuth-based service by updating the token endpoint and client credentials.

```javascript
// server.js — Express backend for OAuth with PKCE
import express from 'express';
import axios from 'axios';
import crypto from 'crypto';
import dotenv from 'dotenv';
dotenv.config();

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

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

/* 2. Endpoint: return challenge to client */
app.get('/api/auth/challenge', (req, res) => {
  const { verifier, challenge } = generatePKCE();
  // Store verifier server-side (e.g., Redis) keyed by session
  res.json({ codeChallenge: challenge, state: crypto.randomBytes(16).toString('hex') });
});

/* 3. Endpoint: exchange authorization code for tokens */
app.post('/api/auth/token', async (req, res) => {
  const { code, codeVerifier } = req.body;
  try {
    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,
        },
        auth: {
          username: process.env.ZOOM_APP_CLIENT_ID,
          password: process.env.ZOOM_APP_CLIENT_SECRET,
        },
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      }
    );
    res.json(tokenResponse.data); // access_token, refresh_token, expires_in
  } catch (e) {
    console.error('Token exchange error:', e.response?.data || e.message);
    res.status(500).json({ error: 'Token exchange failed' });
  }
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Auth server listening on ${PORT}`));

```

## Summary

- The **Zoom** plugin supports three distinct flows: web-based OAuth 2.0, In-Client OAuth with PKCE, and JWT-based Meeting SDK auth, all documented under [`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).
- **Vercel** relies on automatic OIDC tokens available at runtime via `process.env.VERCEL_TOKEN`, as shown in [`plugins/vercel/skills/vercel-api/SKILL.md`](https://github.com/openai/plugins/blob/main/plugins/vercel/skills/vercel-api/SKILL.md).
- **Supabase** uses standard OAuth 2.1 redirects and refreshable access tokens, with service-role JWTs for server contexts.
- **Wix** simplifies authentication via CLI-generated bearer tokens, avoiding manual OAuth redirects entirely.
- Regardless of the provider, store credentials in environment variables—never hard-code secrets in source control—and validate the `state` parameter to prevent CSRF attacks.

## Frequently Asked Questions

### How do I choose the right authentication method for my plugin?

Choose **OAuth 2.0 with PKCE** when your plugin runs inside a client application like Zoom and requires user consent. Choose **Server-to-Server OAuth** or **JWT** when your plugin performs background or admin-level operations without a user present. Use **platform-native tokens** such as Vercel's OIDC or Wix's CLI tokens when the provider manages identity for you.

### Where should I store client secrets and refresh tokens?

Store `ZOOM_APP_CLIENT_SECRET`, API keys, and refresh tokens in environment variables or a dedicated secret manager, never in your repository. The Zoom plugin explicitly references this requirement in [`plugins/zoom/skills/zoom-apps-sdk/references/environment-variables.md`](https://github.com/openai/plugins/blob/main/plugins/zoom/skills/zoom-apps-sdk/references/environment-variables.md), and the same principle applies to every skill in the repository.

### What is PKCE and why does Zoom require it?

**PKCE (Proof Key for Code Exchange)** is an OAuth extension where the client generates a secret `code_verifier` and sends a hashed `code_challenge` to the authorization server. Zoom's In-Client OAuth requires PKCE to prevent authorization code interception in embedded or mobile environments, as detailed in [`plugins/zoom/skills/zoom-apps-sdk/examples/in-client-oauth.md`](https://github.com/openai/plugins/blob/main/plugins/zoom/skills/zoom-apps-sdk/examples/in-client-oauth.md).

### Can I reuse the same authentication server for multiple plugins?

Yes. The Node.js boilerplate pattern in [`plugins/zoom/skills/zoom-apps-sdk/examples/in-client-oauth.md`](https://github.com/openai/plugins/blob/main/plugins/zoom/skills/zoom-apps-sdk/examples/in-client-oauth.md) demonstrates a generic PKCE and token-exchange handler. You can adapt the endpoints, token URLs, and client credentials to support multiple services from a single backend, provided you scope each provider's tokens and secrets separately.