Zoom Oauth
OpenAIOfficialCreated by ZoomDistributed by OpenAI
- Skill
- Tool integration
- Automation and integrations
- MIT
- No issues found
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
Download the skill as a ZIP:
https://autogineer.com/agent-skills/openai/zoom-oauth/downloadUnzip 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
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.
Confirm the target API, SDK, or app surface, because scopes and token audiences differ by surface.
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.
Store refresh tokens as single-use values: persist the replacement refresh token returned by each refresh response before reusing the old one.
Validate requests against redirect URI, account ID, scopes, app publication state, and token expiration before changing application code.
For local development, keep access tokens out of logs and treat refresh tokens as single-use when rotating credentials.
References
Full preserved guide: references/full-guide.md
OAuth flows: concepts/oauth-flows.md
Token lifecycle: concepts/token-lifecycle.md
Scope architecture: concepts/scopes-architecture.md
Granular scopes: references/granular-scopes.md
OAuth errors: references/oauth-errors.md
Redirect URI issues: troubleshooting/redirect-uri-issues.md
Files
- ATTRIBUTION.mdAdded by AutoGineer862 B
- LICENSELicence file added by AutoGineer1.1 KiB
- RUNBOOK.md3.3 KiB
- SKILL.md1.8 KiB
- agents/openai.yaml131 B
- concepts/oauth-flows.md22.2 KiB
- concepts/pkce.md13.8 KiB
- concepts/scopes-architecture.md6.7 KiB
- concepts/state-parameter.md8.1 KiB
- concepts/token-lifecycle.md15.1 KiB
- examples/device-flow.md371 B
- examples/pkce-implementation.md379 B
- examples/s2s-oauth-basic.md375 B
- examples/s2s-oauth-redis.md6.6 KiB
- examples/token-refresh.md373 B
- examples/user-oauth-basic.md376 B
- examples/user-oauth-mysql.md376 B
- references/classic-scopes.md2.5 KiB
- references/environment-variables.md863 B
- references/full-guide.md31.7 KiB
- references/granular-scopes.md1.8 KiB
- references/oauth-errors.md4.8 KiB
- troubleshooting/common-errors.md458 B
- troubleshooting/redirect-uri-issues.md229 B
- troubleshooting/scope-issues.md222 B
- troubleshooting/token-issues.md222 B
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 Type | Can Store Secrets? | PKCE Required? | Examples |
|---|---|---|---|
| Confidential | ✅ Yes (server-side) | ❌ Optional | Backend servers, traditional web apps |
| Public | ❌ No (client-side) | ✅ Required | Mobile 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 secretHow 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: EncryptedSharedPreferencesStep 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:
| Method | Description | Security | Support |
|---|---|---|---|
| S256 | SHA256 hash of verifier | ✅ Recommended | All OAuth 2.0 servers |
| plain | Verifier sent as-is (no hash) | ⚠️ Weaker | Legacy 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 accessWith 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 tokensCommon 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 hashing3. 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)); // true2. 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 true3. 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
Implement PKCE in your app → ../examples/pkce-implementation.md
Add state parameter → state-parameter.md
Understand OAuth flows → oauth-flows.md
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