Subchapter 114.3
references/authorization-patterns.mdMarkdown21 KBView on GitHub
Permission validation middleware and role-based access control for Zoom API integrations.
Note: These are implementation patterns for YOUR application when building Zoom integrations. These are not Zoom’s internal authorization mechanisms - they are examples of how to structure authorization logic in your own backend.
References
App TypesWhen chaining multiple Zoom API calls, each step may require different scopes and permissions. This document provides patterns for validating authorization at each step before proceeding.
┌─────────────────────────────────────────────────────────────────────────┐
│ AUTHORIZATION VALIDATION FLOW │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ 1. Check Token Validity │
│ └── Is token expired? → Refresh or re-authenticate │
│ └── Is token revoked? → Re-authenticate │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ 2. Validate Required Scopes │
│ └── Does token have scopes for this operation? │
│ └── If missing → Return 403 with required scopes │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ 3. Check Resource Permissions │
│ └── Does user have access to this resource? │
│ └── Is user admin/owner/member? │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ 4. Execute Operation │
│ └── Call Zoom API │
│ └── Handle API-level authorization errors │
└─────────────────────────────────────────────────────────────────────────┘const axios = require('axios');
/**
* Middleware to validate OAuth token has required scopes
* @param {string[]} requiredScopes - Scopes required for this route
*/
function requireScopes(requiredScopes) {
return async (req, res, next) => {
const token = req.headers.authorization?.replace('Bearer '
| Operation | User Scope | Admin Scope (S2S) |
|---|---|---|
| Get own user info | user:read | user:read:admin |
| List all users | N/A | user:read:admin |
| Create user | N/A | user:write:admin |
| Get own meetings | meeting:read | meeting:read:admin |
| Get any user’s meetings | N/A | meeting:read:admin |
| Create meeting for self | meeting:write | meeting:write:admin |
| Create meeting for others | N/A | meeting:write:admin |
| List own recordings | recording:read | recording:read:admin |
| List any user’s recordings | N/A | recording:read:admin |
| Delete own recording | recording:write | recording:write:admin |
| Delete any recording | N/A | recording:write:admin |
| Access own phone | phone:read | phone:read:admin |
| Access any user’s phone | N/A | phone:read:admin |
| Manage phone settings | phone:write | phone:write:admin |
Note: “N/A” means this operation requires admin-level scopes and cannot be done with user-level OAuth.
/**
* Role definitions with allowed scopes
*/
const ROLES = {
admin: {
scopes: [
'user:read:admin', 'user:write:admin',
'meeting:read:admin', 'meeting:write:admin',
'recording:read:admin', 'recording:write:admin',
'account:read:admin', 'account:write:admin'
],
description: 'Full administrative access'
},
manager: {
scopes: [
'user:read:admin',
'meeting:read:admin', 'meeting:write:admin',
'recording:read:admin'
],
description: 'Manage meetings and view users'
},
user: {
scopes: [
'user:read',
'meeting:read', 'meeting:write',
'recording:read'
],
description: 'Manage own meetings and recordings'
},
viewer: {
scopes: [
'meeting:read',
'recording:read'
],
description: 'View-only access'
}
};
/**
* Check if user role has required scope
*/
function roleHasScope(role, requiredScope) {
const roleConfig = ROLES[role];
if (!roleConfig) return false;
return roleConfig.scopes.some(scope => {
// Exact match
if (scope === requiredScope) return true;
// Admin scope covers non-admin version
// e.g., meeting:read:admin covers meeting:read
if (scope.endsWith(':admin')) {
const baseScope = scope.replace(':admin', '');
if (baseScope === requiredScope) return true;
}
return false;
});
}
/**
* Middleware to require a specific role
*/
function requireRole(allowedRoles) {
return (req, res, next) => {
const userRole = req.user?.role; // From your auth system
if (!userRole || !allowedRoles.includes(userRole)) {
return res.status(403).json({
error: 'forbidden',
message: 'Insufficient role permissions',
required_roles: allowedRoles,
your_role: userRole || 'none'
});
}
next();
};
}
// Usage
app.delete('/api/users/:id',
requireRole(['admin']),
requireScopes(['user:write:admin']),
async (req, res) => {
// Only admins can delete users
await deleteUser(req.params.id);
res.json({ success: true });
}
);/**
* Validate permissions for a multi-step operation
* before executing any steps
*/
async function validateChainPermissions(operations, tokenScopes) {
const allRequiredScopes = new Set();
for (const op of operations) {
for (const scope of op.requiredScopes) {
allRequiredScopes.add(scope);
}
}
const missingScopes = [...allRequiredScopes].filter(
scope => !tokenScopes.includes(scope)
);
if (missingScopes.length > 0) {
return {
valid: false,
missingScopes,
message: `Cannot complete operation chain. Missing scopes: ${missingScopes.join(', ')}`
};
}
return { valid: true };
}
/**
* Execute a chain of operations with permission validation
*/
async function executeAuthorizedChain(operations, accessToken) {
// Get token scopes
const tokenInfo = await getTokenInfo(accessToken);
const tokenScopes = tokenInfo.scope.split(' ');
// Validate all permissions upfront
const validation = await validateChainPermissions(operations, tokenScopes);
if (!validation.valid) {
throw new Error(validation.message);
}
// Execute operations in sequence
const results = [];
for (const op of operations) {
console.log(`Executing: ${op.name}`);
try {
const result = await op.execute(accessToken, results);
results.push({ name: op.name, success: true, data: result });
} catch (error) {
// Check if it's an authorization error
if (error.response?.status === 403) {
throw new Error(`Authorization failed at step "${op.name}": ${error.response.data.message}`);
}
throw error;
}
}
return results;
}
// Example: User + Meeting creation chain
const userMeetingChain = [
{
name: 'createUser',
requiredScopes: ['user:write:admin'],
execute: async (token, previousResults) => {
return await createUser({
email: 'new@example.com',
firstName: 'New',
lastName: 'User'
}, token);
}
},
{
name: 'createMeeting',
requiredScopes: ['meeting:write:admin'],
execute: async (token, previousResults) => {
const user = previousResults.find(r => r.name === 'createUser').data;
return await createMeeting(user.id, {
topic: 'Onboarding Meeting'
}, token);
}
}
];
// Usage
try {
const results = await executeAuthorizedChain(userMeetingChain, accessToken);
console.log('Chain completed:', results);
} catch (error) {
console.error('Chain failed:', error.message);
}/**
* Execute with graceful degradation when permissions are partial
*/
async function executeWithDegradation(operations, accessToken) {
const tokenInfo = await getTokenInfo(accessToken);
const tokenScopes = tokenInfo.scope.split(' ');
const results = [];
for (const op of operations) {
// Check if we have permission for this operation
const hasPermission = op.requiredScopes.every(
scope => tokenScopes.includes(scope)
);
if (!hasPermission) {
if (op.required) {
// Required operation - fail the chain
throw new Error(`Missing required scopes for ${op.name}: ${op.requiredScopes.join(', ')}`);
} else {
// Optional operation - skip with warning
console.warn(`Skipping ${op.name}: insufficient permissions`);
results.push({
name: op.name,
skipped: true,
reason: 'insufficient_permissions',
required_scopes: op.requiredScopes
});
continue;
}
}
// Execute operation
const result = await op.execute(accessToken, results);
results.push({ name: op.name, success: true, data: result });
}
return results;
}
// Example with optional operations
const meetingWithOptionalRecording = [
{
name: 'getMeeting',
required: true,
requiredScopes: ['meeting:read'],
execute: async (token) => getMeetingDetails(meetingId, token)
},
{
name: 'getRecordings',
required: false, // Optional - won't fail chain
requiredScopes: ['recording:read'],
execute: async (token, prev) => {
const meeting = prev.find(r => r.name === 'getMeeting').data;
return getRecordings(meeting.uuid, token);
}
}
];┌──────────────────────────────────────────────────────────────────────────┐
│ AUTHORIZATION DECISION FLOW │
└──────────────────────────────────────────────────────────────────────────┘
┌─────────────────┐
│ Receive Request │
└────────┬────────┘
│
▼
┌────────────────────────┐
│ Is token present? │
└───────────┬────────────┘
│
┌───────────┴───────────┐
│ NO │ YES
▼ ▼
┌───────────────┐ ┌────────────────────┐
│ Return 401 │ │ Is token valid? │
│ Unauthorized │ └─────────┬──────────┘
└───────────────┘ │
┌───────────┴───────────┐
│ NO │ YES
▼ ▼
┌───────────────┐ ┌────────────────────┐
│ Return 401 │ │ Has required │
│ Invalid Token │ │ scopes? │
└───────────────┘ └─────────┬──────────┘
│
┌───────────┴───────────┐
│ NO │ YES
▼ ▼
┌───────────────┐ ┌────────────────────┐
│ Return 403 │ │ Has resource │
│ Insufficient │ │ access? │
│ Scope │ └─────────┬──────────┘
└───────────────┘ │
┌───────────┴───────────┐
│ NO │ YES
▼ ▼
┌───────────────┐ ┌────────────────┐
│ Return 403 │ │ Execute │
│ Forbidden │ │ Operation │
└───────────────┘ └────────────────┘| Status | Error | Cause | Solution |
|---|---|---|---|
| 401 | invalid_token | Token expired or revoked | Refresh token or re-authenticate |
| 401 | unauthorized | No token provided | Include Authorization header |
| 403 | insufficient_scope | Token missing required scope | Request additional scopes |
| 403 | forbidden | User lacks resource access | Check user permissions |
| 403 | access_denied | Admin-only operation | Use admin account |
See these use-cases for authorization patterns in action: