anthropics--knowledge-work-plugins
9.1 KiB
9.1 KiB
Authentication Flows
All Zoom REST API requests require OAuth 2.0 authentication. This guide covers all supported OAuth flows and when to use each.
Complete OAuth implementation guide: See the zoom-oauth skill for full code examples, token storage, and production patterns.
Flow Selection
| Flow | Use Case | User Interaction | Token Lifetime |
|---|---|---|---|
| Server-to-Server OAuth | Backend automation, bots, integrations | None | 1 hour |
| Authorization Code | User-facing web apps | User consent flow | 1 hour (refresh: 15 years) |
| Authorization Code + PKCE | SPAs, mobile apps | User consent flow | 1 hour (refresh: 15 years) |
| Device Code | TV/IoT devices, CLI tools | User enters code on separate device | 1 hour (refresh: 15 years) |
| DEPRECATED — migrate to S2S OAuth |
Server-to-Server OAuth (Recommended for Backend)
No user interaction required. Best for automation, scheduled tasks, and backend services.
Setup
- Go to Zoom App Marketplace → Develop → Build App
- Select Server-to-Server OAuth
- Note: Account ID, Client ID, Client Secret
- Add required scopes (e.g.,
meeting:write:admin,user:read:admin)
Get Access Token
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=account_credentials&account_id=ACCOUNT_ID"
Response
{
"access_token": "eyJhbGciOiJIUzI1NiJ9...",
"token_type": "bearer",
"expires_in": 3600,
"scope": "meeting:read meeting:write user:read",
"api_url": "https://api.zoom.us"
}
Node.js — Token Manager with Auto-Refresh
class ZoomS2SAuth {
constructor(accountId, clientId, clientSecret) {
this.accountId = accountId;
this.clientId = clientId;
this.clientSecret = clientSecret;
this.token = null;
this.tokenExpiry = 0;
}
async getAccessToken() {
// Return cached token if valid (with 60s buffer)
if (this.token && Date.now() < this.tokenExpiry - 60000) {
return this.token;
}
const credentials = Buffer.from(
`${this.clientId}:${this.clientSecret}`
).toString('base64');
const response = await fetch('https://zoom.us/oauth/token', {
method: 'POST',
headers: {
'Authorization': `Basic ${credentials}`,
'Content-Type': 'application/x-www-form-urlencoded'
},
body: `grant_type=account_credentials&account_id=${this.accountId}`
});
if (!response.ok) {
const err = await response.json();
throw new Error(`Token error: ${err.error} - ${err.reason}`);
}
const data = await response.json();
this.token = data.access_token;
this.tokenExpiry = Date.now() + (data.expires_in * 1000);
return this.token;
}
async request(method, path, body = null) {
const token = await this.getAccessToken();
const response = await fetch(`https://api.zoom.us/v2${path}`, {
method,
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: body ? JSON.stringify(body) : undefined
});
if (!response.ok) {
const err = await response.json().catch(() => ({}));
throw new Error(`Zoom API ${response.status}: ${JSON.stringify(err)}`);
}
// Some endpoints return 204 No Content
if (response.status === 204) return null;
return response.json();
}
}
// Usage
const zoom = new ZoomS2SAuth(
process.env.ZOOM_ACCOUNT_ID,
process.env.ZOOM_CLIENT_ID,
process.env.ZOOM_CLIENT_SECRET
);
const users = await zoom.request('GET', '/users?page_size=300');
const meeting = await zoom.request('POST', '/users/user@example.com/meetings', {
topic: 'API Meeting', type: 2, duration: 30
});
Python — Token Manager
import requests
import time
from base64 import b64encode
class ZoomS2SAuth:
def __init__(self, account_id, client_id, client_secret):
self.account_id = account_id
self.client_id = client_id
self.client_secret = client_secret
self.token = None
self.token_expiry = 0
def get_access_token(self):
if self.token and time.time() < self.token_expiry - 60:
return self.token
credentials = b64encode(
f'{self.client_id}:{self.client_secret}'.encode()
).decode()
response = requests.post(
'https://zoom.us/oauth/token',
headers={
'Authorization': f'Basic {credentials}',
'Content-Type': 'application/x-www-form-urlencoded'
},
data=f'grant_type=account_credentials&account_id={self.account_id}'
)
response.raise_for_status()
data = response.json()
self.token = data['access_token']
self.token_expiry = time.time() + data['expires_in']
return self.token
def request(self, method, path, json_data=None):
token = self.get_access_token()
response = requests.request(
method,
f'https://api.zoom.us/v2{path}',
headers={'Authorization': f'Bearer {token}'},
json=json_data
)
response.raise_for_status()
return response.json() if response.content else None
User OAuth (Authorization Code)
For apps that act on behalf of individual Zoom users.
Flow
1. User clicks "Connect to Zoom"
2. Redirect to: https://zoom.us/oauth/authorize?response_type=code&client_id=XXX&redirect_uri=YYY&state=ZZZ
3. User grants permission
4. Zoom redirects to callback: https://yourapp.com/callback?code=AUTH_CODE&state=ZZZ
5. Exchange code for tokens
6. Use access_token for API calls
7. Refresh when expired
Exchange Code for Token
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&code=AUTH_CODE&redirect_uri=https://yourapp.com/callback"
Refresh Token
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=refresh_token&refresh_token=REFRESH_TOKEN"
Important: me Keyword
User OAuth apps must use me instead of userId in API paths:
# CORRECT for user OAuth
GET /v2/users/me/meetings
# WRONG for user OAuth — will return "Invalid access token"
GET /v2/users/abc123/meetings
Common Scopes
| Scope | Description |
|---|---|
user:read |
Read user profile |
user:read:admin |
Read all users (admin) |
user:write:admin |
Manage all users (admin) |
meeting:read |
Read meeting data |
meeting:write |
Create/update meetings |
meeting:write:admin |
Create/update any user's meetings |
recording:read |
Access recordings |
recording:write |
Manage recordings |
webinar:read |
Read webinar data |
webinar:write |
Manage webinars |
report:read:admin |
View reports |
Best practice: Request only the scopes you need. Fewer scopes = less user friction and faster app approval.
Token Storage Best Practices
// DO: Encrypt tokens at rest
const encrypted = encrypt(accessToken, process.env.ENCRYPTION_KEY);
await db.tokens.upsert({ userId, encrypted, expiresAt });
// DO: Use httpOnly secure cookies for web apps
res.cookie('zoom_session', sessionId, {
httpOnly: true, secure: true, sameSite: 'strict', maxAge: 3600000
});
// DON'T: Store tokens in localStorage or log them
localStorage.setItem('zoom_token', token); // INSECURE
console.log('Token:', accessToken); // LEAKS CREDENTIALS
Error Handling
| Error | Cause | Solution |
|---|---|---|
invalid_grant |
Expired/used auth code or refresh token | Restart OAuth flow or re-authenticate |
invalid_client |
Wrong client ID or secret | Verify credentials |
invalid_scope |
Scope not approved for your app | Check app scopes in Marketplace |
access_denied |
User denied permission | Handle gracefully in UI |
try {
const token = await refreshAccessToken(refreshToken);
} catch (error) {
if (error.response?.data?.error === 'invalid_grant') {
// Refresh token revoked or expired — re-authenticate
redirectToOAuthFlow();
}
}
Migration from JWT (Deprecated)
The JWT app type on Zoom Marketplace is deprecated. This does not affect JWT token signatures used elsewhere (e.g., Video SDK).
Steps:
- Create a Server-to-Server OAuth app
- Request the same scopes
- Replace JWT token generation with OAuth token endpoint
- Test all endpoints
- Delete the JWT app
Resources
- OAuth Guide: https://developers.zoom.us/docs/integrations/oauth/
- S2S OAuth: https://developers.zoom.us/docs/internal-apps/s2s-oauth/
- Scopes Reference: https://developers.zoom.us/docs/integrations/oauth-scopes/
- Full OAuth Skill: See zoom-oauth