Generated banner for Zoom Oauth
Generated logo for Zoom Oauth

Zoom Oauth

OpenAIOfficialCreated by ZoomDistributed by OpenAI

  • Skill
  • Tool integration
  • Automation and integrations
  • MIT
  • No issues found

From the author

Use when implementing OAuth.
— the description by Zoom

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

This skill provides a structured approach to implementing Zoom OAuth, covering various app types including user-level, account-level, and server-to-server OAuth. It assists developers in selecting the correct grant flow, managing token lifecycles, and diagnosing common authentication errors. The skill emphasizes secure practices, such as handling refresh tokens as single-use values and maintaining proper scope architecture.

What it does

  • Guides selection of appropriate OAuth grant flows like PKCE or authorization code.
  • Provides workflows for identifying app types and required scopes.
  • Offers best practices for secure token management and refresh token rotation.
  • Assists in troubleshooting common OAuth errors and redirect URI issues.
  • Clarifies the distinction between classic and granular scope usage.
  • Defines standard environment variables required for Zoom API integration.

When to use it

  • When setting up authentication for a new Zoom Marketplace application.
  • When debugging failed authorization requests or token refresh issues.
  • When determining the correct OAuth scopes for specific API endpoints.
  • When transitioning from classic scopes to granular scopes.
  • When implementing server-to-server OAuth for automated account-level tasks.

Advantages

  • Reduces integration errors by providing clear flow selection criteria.
  • Promotes security by enforcing single-use refresh token patterns.
  • Simplifies scope management through structured lookup workflows.
  • Centralizes configuration requirements and environment variable standards.

Usage examples

“How do I implement authorization code flow with PKCE for my public Zoom app?”
The agent will outline the steps to generate a code challenge, initiate the authorization request, and exchange the code for an access token while ensuring PKCE security.
“I am getting an invalid scope error when calling the meeting API. How do I fix this?”
The agent will guide you to verify your app's configured scopes against the endpoint requirements and check if you are using deprecated classic scopes instead of granular ones.
“What is the correct way to handle Zoom refresh tokens?”
The agent will explain the requirement to persist the new refresh token returned by every refresh response and discard the old one to maintain security.

Install

  1. Download the skill as a ZIP:

    https://autogineer.com/agent-skills/openai/zoom-oauth/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip zoom-oauth.zip -d .agents/skills

Documentation by Zoom

Zoom OAuth

Use this skill for concrete Zoom authentication implementation and troubleshooting. Prefer setup-zoom-oauth for the first-pass setup plan, then return here for exact flows, scope behavior, refresh handling, and error diagnosis.

Workflow

  1. Identify the app type and actor: user-level OAuth, account-level OAuth, server-to-server OAuth where officially supported, SDK JWT, or Build-platform credentials.

  2. Confirm the target API, SDK, or app surface, because scopes and token audiences differ by surface.

  3. Choose the grant flow: authorization code with PKCE for public clients, authorization code for confidential web apps, device authorization where appropriate, or account credentials for supported account-level automation.

  4. Store refresh tokens as single-use values: persist the replacement refresh token returned by each refresh response before reusing the old one.

  5. Validate requests against redirect URI, account ID, scopes, app publication state, and token expiration before changing application code.

  6. For local development, keep access tokens out of logs and treat refresh tokens as single-use when rotating credentials.

References

Files

concepts/pkce.md

PKCE (Proof Key for Code Exchange)

PKCE (pronounced "pixy") is a security extension to OAuth 2.0 for public clients that cannot securely store a client secret.

When PKCE is Required

Client TypeCan Store Secrets?PKCE Required?Examples
Confidential✅ Yes (server-side)❌ OptionalBackend servers, traditional web apps
Public❌ No (client-side)✅ RequiredMobile apps, SPAs, desktop apps

###Why Public Clients Can't Keep Secrets

// ❌ INSECURE: Client secret embedded in mobile/SPA code
const CLIENT_SECRET = "abc123"; // Anyone can decompile/inspect and find this!

// Attacker can:
// 1. Extract CLIENT_SECRET from app
// 2. Intercept authorization code
// 3. Exchange code for tokens using stolen secret

How PKCE Works

PKCE prevents authorization code interception attacks without requiring a client secret.

Flow Diagram
┌─────────────┐                 ┌──────────────┐                 ┌──────────────┐
│ Mobile App  │                 │ Zoom OAuth   │                 │   Attacker   │
│  (Public)   │                 │   Server     │                 │ (Intercepting│
└──────┬──────┘                 └──────┬───────┘                 └──────┬───────┘
       │                               │                                │
       │ 1. Generate code_verifier     │                                │
       │    (random 43-128 chars)      │                                │
       │                               │                                │
       │ 2. Create code_challenge       │                                │
       │    SHA256(code_verifier)      │                                │
       │                               │                                │
       │ 3. Authorize with challenge   │                                │
       │https://zoom.us/oauth/authorize?│                                │
       │    code_challenge={HASH}      │                                │
       │    code_challenge_method=S256 │                                │
       │──────────────────────────────>│                                │
       │                               │                                │
       │ 4. User authorizes            │                                │
       │──────────────────────────────>│                                │
       │                               │                                │
       │ 5. Return authorization code  │                                │
       │<──────────────────────────────│                                │
       │                               │                                │
       │                               │   Attacker intercepts code     │
       │                               │<───────────────────────────────│
       │                               │                                │
       │                               │   Attacker tries to exchange   │
       │                               │   (but doesn't have verifier!) │
       │                               │<───────────────────────────────│
       │                               │                                │
       │                               │   REJECTED: Missing verifier   │
       │                               │───────────────────────────────>│
       │                               │                                │
       │ 6. Exchange code with verifier│                                │
       │  POST /oauth/token            │                                │
       │    code={CODE}                │                                │
       │    code_verifier={ORIGINAL}   │                                │
       │──────────────────────────────>│                                │
       │                               │                                │
       │                               │ Verify:                        │
       │                               │ SHA256(code_verifier)          │
       │                               │   == code_challenge?           │
       │                               │                                │
       │ 7. Return tokens              │                                │
       │<──────────────────────────────│                                │
       │                               │                                │
Key Concept
  • code_verifier: Random secret generated by app (kept secret)

  • code_challenge: SHA256 hash of code_verifier (sent to Zoom)

  • Zoom stores challenge: During authorization

  • App proves possession: By providing original verifier during token exchange

  • Attacker fails: Even with intercepted code, can't generate matching verifier


Implementation

Step 1: Generate PKCE Parameters
const crypto = require('crypto');

function generatePKCE() {
  // Generate random code_verifier (43-128 characters)
  const verifier = crypto
    .randomBytes(32)
    .toString('base64url'); // base64url encoding (no padding)

  // Create code_challenge: SHA256(code_verifier)
  const challenge = crypto
    .createHash('sha256')
    .update(verifier)
    .digest('base64url');

  return {
    code_verifier: verifier,
    code_challenge: challenge
  };
}

// Example output:
// {
//   code_verifier: "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
//   code_challenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
// }
Step 2: Store code_verifier Securely
// Store in session (server-side) or secure storage (mobile)
req.session.pkce_verifier = code_verifier;

// For mobile apps, use secure storage:
// - iOS: Keychain
// - Android: EncryptedSharedPreferences
Step 3: Redirect to Authorization with code_challenge
app.get('/auth', (req, res) => {
  const { code_verifier, code_challenge } = generatePKCE();

  // Store verifier for later (Step 5)
  req.session.pkce_verifier = code_verifier;

  const authURL = new URL('https://zoom.us/oauth/authorize');
  authURL.searchParams.set('response_type', 'code');
  authURL.searchParams.set('client_id', process.env.ZOOM_CLIENT_ID);
  authURL.searchParams.set('redirect_uri', process.env.ZOOM_REDIRECT_URL);
  authURL.searchParams.set('code_challenge', code_challenge);
  authURL.searchParams.set('code_challenge_method', 'S256'); // SHA256

  res.redirect(authURL.toString());
});
Step 4: Exchange Code with code_verifier
app.get('/callback', async (req, res) => {
  const { code } = req.query;
  const code_verifier = req.session.pkce_verifier; // Retrieve stored verifier

  try {
    const response = await axios.post(
      'https://zoom.us/oauth/token',
      qs.stringify({
        grant_type: 'authorization_code',
        code: code,
        redirect_uri: process.env.ZOOM_REDIRECT_URL,
        code_verifier: code_verifier // Prove possession of original verifier
      }),
      {
        headers: {
          'Authorization': `Basic ${Buffer.from(
            `${process.env.ZOOM_CLIENT_ID}:${process.env.ZOOM_CLIENT_SECRET}`
          ).toString('base64')}`,
          'Content-Type': 'application/x-www-form-urlencoded'
        }
      }
    );

    const { access_token, refresh_token } = response.data;

    // Success! Store tokens
    await saveTokens({ access_token, refresh_token });

    // Clean up verifier
    delete req.session.pkce_verifier;

    res.send('Authorization successful!');
  } catch (error) {
    res.status(500).send('Token exchange failed');
  }
});

PKCE Methods

Zoom supports two code_challenge_method values:

MethodDescriptionSecuritySupport
S256SHA256 hash of verifier✅ RecommendedAll OAuth 2.0 servers
plainVerifier sent as-is (no hash)⚠️ WeakerLegacy support only

Always use S256:

// ✅ RECOMMENDED
authURL.searchParams.set('code_challenge_method', 'S256');
// ❌ AVOID (less secure)
authURL.searchParams.set('code_challenge_method', 'plain');

Security Benefits

Without PKCE (Vulnerable)
Attacker intercepts authorization code
   ↓
Attacker exchanges code with client_secret
   ↓
Attacker gets access_token and refresh_token
   ↓
Attacker has full account access
With PKCE (Protected)
Attacker intercepts authorization code
   ↓
Attacker tries to exchange code
   ↓
Zoom: "Provide code_verifier"
   ↓
Attacker doesn't have original verifier
   ↓
Token exchange FAILS
   ↓
Legitimate app exchanges with correct verifier
   ↓
Legitimate app gets tokens

Common Mistakes

1. Not Storing code_verifier
// ❌ WRONG: Generating new verifier on callback
app.get('/callback', async (req, res) => {
  const { code_challenge } = generatePKCE(); // New verifier!
  // This won't match the original challenge
});
// ✅ CORRECT: Retrieve stored verifier
app.get('/callback', async (req, res) => {
  const code_verifier = req.session.pkce_verifier; // Original verifier
});
2. Using 'plain' Method
// ❌ AVOID
code_challenge_method: 'plain' // Less secure
// ✅ RECOMMENDED
code_challenge_method: 'S256' // SHA256 hashing
3. Exposing code_verifier
// ❌ WRONG: Including verifier in URL
authURL.searchParams.set('code_verifier', verifier); // Don't send verifier during auth!
// ✅ CORRECT: Only send code_challenge
authURL.searchParams.set('code_challenge', challenge);
authURL.searchParams.set('code_challenge_method', 'S256');

Mobile App Implementation

iOS (Swift)
import CryptoKit

func generatePKCE() -> (verifier: String, challenge: String) {
    // Generate random verifier
    var buffer = [UInt8](repeating: 0, count: 32)
    _ = SecRandomCopyBytes(kSecRandomDefault, buffer.count, &buffer)
    let verifier = Data(buffer).base64EncodedString()
        .replacingOccurrences(of: "+", with: "-")
        .replacingOccurrences(of: "/", with: "_")
        .replacingOccurrences(of: "=", with: "")

    // Create SHA256 challenge
    let data = verifier.data(using: .utf8)!
    let hash = SHA256.hash(data: data)
    let challenge = Data(hash).base64EncodedString()
        .replacingOccurrences(of: "+", with: "-")
        .replacingOccurrences(of: "/", with: "_")
        .replacingOccurrences(of: "=", with: "")

    return (verifier, challenge)
}

// Store verifier in Keychain
KeychainWrapper.standard.set(verifier, forKey: "pkce_verifier")
Android (Kotlin)
import java.security.MessageDigest
import java.security.SecureRandom
import android.util.Base64

fun generatePKCE(): Pair<String, String> {
    // Generate random verifier
    val bytes = ByteArray(32)
    SecureRandom().nextBytes(bytes)
    val verifier = Base64.encodeToString(bytes,
        Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING)

    // Create SHA256 challenge
    val digest = MessageDigest.getInstance("SHA-256")
    val hash = digest.digest(verifier.toByteArray())
    val challenge = Base64.encodeToString(hash,
        Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING)

    return Pair(verifier, challenge)
}

// Store verifier in EncryptedSharedPreferences
val encryptedPrefs = EncryptedSharedPreferences.create(...)
encryptedPrefs.edit().putString("pkce_verifier", verifier).apply()

Testing PKCE Implementation

1. Verify code_challenge Format
const { code_verifier, code_challenge } = generatePKCE();

console.log('Verifier length:', code_verifier.length); // Should be 43-128
console.log('Challenge length:', code_challenge.length); // Should be 43 for S256
console.log('Verifier chars:', /^[A-Za-z0-9_-]+$/.test(code_verifier)); // true
console.log('Challenge chars:', /^[A-Za-z0-9_-]+$/.test(code_challenge)); // true
2. Verify SHA256 Hashing
const crypto = require('crypto');

const verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
const expected_challenge = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM";

const computed_challenge = crypto
  .createHash('sha256')
  .update(verifier)
  .digest('base64url');

console.log(computed_challenge === expected_challenge); // Should be true
3. Test Token Exchange
curl -X POST https://zoom.us/oauth/token \
  -H "Authorization: Basic $(echo -n 'CLIENT_ID:CLIENT_SECRET' | base64)" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code" \
  -d "code=YOUR_AUTH_CODE" \
  -d "redirect_uri=YOUR_REDIRECT_URI" \
  -d "code_verifier=YOUR_CODE_VERIFIER"

# Should return { access_token, refresh_token, ... }

PKCE Specification

PKCE follows RFC 7636:


Next Steps

Requirements and compatibility

Written for: OpenAI Codex

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Security

No issues found by our static scan· No issues found

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Created by
Zoom
Publisher
OpenAI (Official)
Distributed by
OpenAI
Source
openai/plugins · plugins/zoom/skills/oauth · commit d420d13
Licence
MIT License (redistribution allowed)
  • LICENSE (added by AutoGineer from plugins/zoom/LICENSE)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by Zoom or OpenAI.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download zoom-oauth.zip