11 KiB
11 KiB
Authentication Guide
Comprehensive guide to Zoom API authentication methods: OAuth 2.0, Server-to-Server OAuth, and JWT (legacy).
Overview
Zoom APIs support multiple authentication methods depending on your use case:
| Method | Use Case | Token Lifetime |
|---|---|---|
| User OAuth 2.0 | Act on behalf of users | 1 hour (refresh: 15 years) |
| Server-to-Server OAuth | Backend automation | 1 hour |
| JWT (Deprecated) | Legacy integrations | Custom |
Server-to-Server OAuth (Recommended for Backend)
For backend services that don't need user interaction.
Setup
- Go to Zoom App Marketplace
- Click Develop → Build App
- Select Server-to-Server OAuth
- Note your credentials:
- Account ID
- Client ID
- Client Secret
Get Access Token
curl -X POST "https://zoom.us/oauth/token" \
-H "Authorization: Basic $(echo -n '{clientId}:{clientSecret}' | base64)" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=account_credentials&account_id={accountId}"
Response
{
"access_token": "eyJhbGciOiJIUzI1NiJ9...",
"token_type": "bearer",
"expires_in": 3600,
"scope": "meeting:read meeting:write user:read"
}
Code Example (Node.js)
const axios = require('axios');
class ZoomAuth {
constructor(accountId, clientId, clientSecret) {
this.accountId = accountId;
this.clientId = clientId;
this.clientSecret = clientSecret;
this.token = null;
this.tokenExpiry = null;
}
async getAccessToken() {
// Return cached token if still valid
if (this.token && this.tokenExpiry > Date.now() + 60000) {
return this.token;
}
const credentials = Buffer.from(
`${this.clientId}:${this.clientSecret}`
).toString('base64');
const response = await axios.post(
'https://zoom.us/oauth/token',
`grant_type=account_credentials&account_id=${this.accountId}`,
{
headers: {
'Authorization': `Basic ${credentials}`,
'Content-Type': 'application/x-www-form-urlencoded'
}
}
);
this.token = response.data.access_token;
this.tokenExpiry = Date.now() + (response.data.expires_in * 1000);
return this.token;
}
async apiRequest(method, endpoint, data = null) {
const token = await this.getAccessToken();
const config = {
method,
url: `https://api.zoom.us/v2${endpoint}`,
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
};
if (data) {
config.data = data;
}
return axios(config);
}
}
// Usage
const zoom = new ZoomAuth(
process.env.ZOOM_ACCOUNT_ID,
process.env.ZOOM_CLIENT_ID,
process.env.ZOOM_CLIENT_SECRET
);
const users = await zoom.apiRequest('GET', '/users');
User OAuth 2.0 (For User Actions)
For applications that act on behalf of individual users.
OAuth Flow
1. User clicks "Connect to Zoom"
↓
2. Redirect to Zoom authorization URL
↓
3. User grants permission
↓
4. Zoom redirects to your callback with code
↓
5. Exchange code for access token
↓
6. Use access token for API calls
↓
7. Refresh token when expired
Step 1: Create OAuth App
- Go to Zoom App Marketplace
- Click Develop → Build App
- Select OAuth
- Configure:
- Redirect URI(s)
- Required scopes
- App information
Step 2: Authorization URL
Redirect users to:
https://zoom.us/oauth/authorize?response_type=code&client_id={clientId}&redirect_uri={redirectUri}&state={state}
| Parameter | Description |
|---|---|
response_type |
Always code |
client_id |
Your OAuth app client ID |
redirect_uri |
Must match registered URI |
state |
Random string for CSRF protection |
Step 3: Handle Callback
User is redirected to your callback URL:
https://yourapp.com/callback?code=AUTH_CODE&state=STATE
Step 4: Exchange Code for Token
curl -X POST "https://zoom.us/oauth/token" \
-H "Authorization: Basic $(echo -n '{clientId}:{clientSecret}' | base64)" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code&code={authCode}&redirect_uri={redirectUri}"
Response
{
"access_token": "eyJhbGciOiJIUzI1NiJ9...",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiJ9...",
"expires_in": 3600,
"scope": "meeting:read meeting:write user:read"
}
Step 5: Refresh Token
curl -X POST "https://zoom.us/oauth/token" \
-H "Authorization: Basic $(echo -n '{clientId}:{clientSecret}' | base64)" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=refresh_token&refresh_token={refreshToken}"
Complete OAuth Example (Express.js)
const express = require('express');
const axios = require('axios');
const crypto = require('crypto');
const app = express();
const ZOOM_CLIENT_ID = process.env.ZOOM_CLIENT_ID;
const ZOOM_CLIENT_SECRET = process.env.ZOOM_CLIENT_SECRET;
const REDIRECT_URI = 'https://yourapp.com/auth/zoom/callback';
// Step 2: Initiate OAuth
app.get('/auth/zoom', (req, res) => {
const state = crypto.randomBytes(16).toString('hex');
req.session.oauthState = state;
const authUrl = new URL('https://zoom.us/oauth/authorize');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('client_id', ZOOM_CLIENT_ID);
authUrl.searchParams.set('redirect_uri', REDIRECT_URI);
authUrl.searchParams.set('state', state);
res.redirect(authUrl.toString());
});
// Step 3 & 4: Handle callback and exchange code
app.get('/auth/zoom/callback', async (req, res) => {
const { code, state } = req.query;
// Verify state
if (state !== req.session.oauthState) {
return res.status(400).send('Invalid state');
}
try {
// Exchange code for token
const credentials = Buffer.from(
`${ZOOM_CLIENT_ID}:${ZOOM_CLIENT_SECRET}`
).toString('base64');
const tokenResponse = await axios.post(
'https://zoom.us/oauth/token',
`grant_type=authorization_code&code=${code}&redirect_uri=${REDIRECT_URI}`,
{
headers: {
'Authorization': `Basic ${credentials}`,
'Content-Type': 'application/x-www-form-urlencoded'
}
}
);
const { access_token, refresh_token, expires_in } = tokenResponse.data;
// Store tokens securely (database)
await storeTokens(req.user.id, {
accessToken: access_token,
refreshToken: refresh_token,
expiresAt: Date.now() + (expires_in * 1000)
});
res.redirect('/dashboard');
} catch (error) {
console.error('OAuth error:', error.response?.data);
res.status(500).send('Authentication failed');
}
});
// Helper: Get valid access token (refresh if needed)
async function getValidToken(userId) {
const tokens = await getStoredTokens(userId);
// Check if token is expired (with 1 min buffer)
if (tokens.expiresAt < Date.now() + 60000) {
const credentials = Buffer.from(
`${ZOOM_CLIENT_ID}:${ZOOM_CLIENT_SECRET}`
).toString('base64');
const response = await axios.post(
'https://zoom.us/oauth/token',
`grant_type=refresh_token&refresh_token=${tokens.refreshToken}`,
{
headers: {
'Authorization': `Basic ${credentials}`,
'Content-Type': 'application/x-www-form-urlencoded'
}
}
);
await storeTokens(userId, {
accessToken: response.data.access_token,
refreshToken: response.data.refresh_token,
expiresAt: Date.now() + (response.data.expires_in * 1000)
});
return response.data.access_token;
}
return tokens.accessToken;
}
Scopes
Common Scopes
| Scope | Description |
|---|---|
user:read |
Read user profile |
user:write |
Update user profile |
meeting:read |
Read meeting data |
meeting:write |
Create/update meetings |
recording:read |
Access recordings |
recording:write |
Manage recordings |
phone:read |
Read Zoom Phone data |
phone:write |
Manage Zoom Phone |
Admin Scopes
| Scope | Description |
|---|---|
user:read:admin |
Read all users |
user:write:admin |
Manage all users |
meeting:read:admin |
Read all meetings |
account:read:admin |
Read account settings |
Scope Selection
Request only scopes you need:
- More scopes = more user friction
- Less scopes = better approval chance
- Add scopes incrementally as features grow
Token Storage Best Practices
// DO: Encrypt tokens at rest
const encryptedToken = encrypt(accessToken, encryptionKey);
await db.tokens.save({ userId, encryptedToken });
// DO: Use secure, httpOnly cookies for web apps
res.cookie('zoom_token', accessToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 3600000
});
// DON'T: Store tokens in localStorage
localStorage.setItem('zoom_token', token); // BAD
// DON'T: Log tokens
console.log('Token:', accessToken); // BAD
Error Handling
Common OAuth Errors
| Error | Cause | Solution |
|---|---|---|
invalid_grant |
Expired/used code | Restart OAuth flow |
invalid_client |
Wrong credentials | Check client ID/secret |
invalid_scope |
Unauthorized scope | Request only approved scopes |
access_denied |
User denied permission | Handle gracefully |
Token Refresh Errors
try {
const newToken = await refreshToken(refreshToken);
} catch (error) {
if (error.response?.data?.error === 'invalid_grant') {
// Refresh token expired or revoked
// User needs to re-authorize
redirectToOAuth();
}
}
Webhook Verification
For webhook security, verify the request signature:
const crypto = require('crypto');
function verifyWebhook(req, secret) {
const message = `v0:${req.headers['x-zm-request-timestamp']}:${JSON.stringify(req.body)}`;
const signature = crypto
.createHmac('sha256', secret)
.update(message)
.digest('hex');
const expected = `v0=${signature}`;
return req.headers['x-zm-signature'] === expected;
}
app.post('/webhooks/zoom', (req, res) => {
if (!verifyWebhook(req, process.env.ZOOM_WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
// Process webhook
const event = req.body;
// ...
});
Migration from JWT (Deprecated)
JWT apps are deprecated. Migrate to Server-to-Server OAuth:
- Create new Server-to-Server OAuth app
- Request same scopes
- Update code to use OAuth token endpoint
- Test thoroughly
- Delete JWT app
// OLD (JWT - Deprecated)
const token = jwt.sign(payload, apiSecret);
// NEW (Server-to-Server OAuth)
const token = await getServerToServerToken();
Resources
- OAuth Guide: https://developers.zoom.us/docs/integrations/oauth/
- Server-to-Server OAuth: https://developers.zoom.us/docs/internal-apps/s2s-oauth/
- Scopes Reference: https://developers.zoom.us/docs/integrations/oauth-scopes/
- Migration Guide: https://developers.zoom.us/docs/internal-apps/jwt-app-migration/