Files
2026-07-13 12:20:06 +08:00

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

For backend services that don't need user interaction.

Setup

  1. Go to Zoom App Marketplace
  2. Click DevelopBuild App
  3. Select Server-to-Server OAuth
  4. 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

  1. Go to Zoom App Marketplace
  2. Click DevelopBuild App
  3. Select OAuth
  4. 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:

  1. Create new Server-to-Server OAuth app
  2. Request same scopes
  3. Update code to use OAuth token endpoint
  4. Test thoroughly
  5. Delete JWT app
// OLD (JWT - Deprecated)
const token = jwt.sign(payload, apiSecret);

// NEW (Server-to-Server OAuth)
const token = await getServerToServerToken();

Resources