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

439 lines
11 KiB
Markdown

# 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
1. Go to [Zoom App Marketplace](https://marketplace.zoom.us/)
2. Click **Develop****Build App**
3. Select **Server-to-Server OAuth**
4. Note your credentials:
- Account ID
- Client ID
- Client Secret
### Get Access Token
```bash
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
```json
{
"access_token": "eyJhbGciOiJIUzI1NiJ9...",
"token_type": "bearer",
"expires_in": 3600,
"scope": "meeting:read meeting:write user:read"
}
```
### Code Example (Node.js)
```javascript
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](https://marketplace.zoom.us/)
2. Click **Develop****Build 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
```bash
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
```json
{
"access_token": "eyJhbGciOiJIUzI1NiJ9...",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiJ9...",
"expires_in": 3600,
"scope": "meeting:read meeting:write user:read"
}
```
### Step 5: Refresh Token
```bash
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)
```javascript
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
```javascript
// 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
```javascript
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:
```javascript
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
```javascript
// 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/