Setting the file. One moment.
Subchapter 157.15
examples/in-client-oauth.mdMarkdown6 KBView on GitHub
Authorize users without leaving the Zoom client. Best UX for returning users.
| Approach | UX | When to Use |
|---|---|---|
| Web redirect | Opens browser, redirects back | Initial install from Marketplace |
| In-Client | Popup inside Zoom, no redirect | Subsequent authorizations, best UX |
Frontend Backend Zoom
──────── ──────── ────
GET /api/auth/challenge -->
<-- { codeChallenge, state }
(stores code_verifier in session)
zoomSdk.authorize({ --> --> Shows "Authorize" popup
codeChallenge, state to user
})
onAuthorized fires <-- <-- User clicks "Allow"
{ code, state }
POST /api/auth/token -->
{ code, state } Validates state
Exchanges code + verifier
for access_token
<-- { success: true }import zoomSdk from '@zoom/appssdk';
async function init() {
await zoomSdk.config({
capabilities: ['authorize', 'onAuthorized', 'getUserContext'],
version: '0.16'
});
// Check if already authorized
try {
const response = await fetch('/api/auth/status');
const { authorized } = await response.json();
if (authorized) {
showApp();
return;
}
} catch (e) {
// Not authorized yet
}
// Set up authorization listener BEFORE calling authorize
zoomSdk.addEventListener('onAuthorized', async (event) => {
const { code, state } = event;
const response = await fetch('/api/auth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code, state })
});
if (response.ok) {
showApp();
} else {
showError('Authorization failed');
}
});
// Get challenge and start authorization
const challengeResponse = await fetch('/api/auth/challenge');
const { codeChallenge, state } = await challengeResponse.json();
await zoomSdk.authorize({ codeChallenge, state });
}const crypto = require('crypto');
const express = require('express');
const axios = require('axios');
const router = express.Router();
// Generate PKCE challenge
router.get('/api/auth/challenge', (req, res) => {
const verifier = crypto.randomBytes(32).toString('hex');
const challenge = crypto.createHash('sha256')
.update(verifier)
.digest('base64url');
const state = crypto.randomBytes(16).toString('hex');
// Store in session (server-side only)
req.session.codeVerifier = verifier;
req.session.state = state;
res.json({ codeChallenge: challenge, state });
});
// Exchange authorization code for tokens
router.post('/api/auth/token', async (req, res) => {
const { code, state } = req.body;
// Validate state (CSRF protection)
if (state !== req.session.state) {
return res.status(403).json({ error: 'Invalid state' });
}
try {
const tokenResponse = await axios.post('https://zoom.us/oauth/token', null, {
params: {
grant_type: 'authorization_code',
code,
redirect_uri: process.env.ZOOM_APP_REDIRECT_URI,
code_verifier: req.session.codeVerifier
},
headers: {
'Authorization': 'Basic ' + Buffer.from(
`${process.env.ZOOM_APP_CLIENT_ID}:${process.env.ZOOM_APP_CLIENT_SECRET}`
).toString('base64')
}
});
// Store tokens securely (session, Redis, or database)
req.session.tokens = {
access_token: tokenResponse.data.access_token,
refresh_token: tokenResponse.data.refresh_token,
expires_at: Date.now() + (tokenResponse.data.expires_in * 1000)
};
// Clean up PKCE data
delete req.session.codeVerifier;
delete req.session.state;
res.json({ success: true });
} catch (error) {
console.error('Token exchange failed:', error.response?.data || error.message);
res.status(500).json({ error: 'Token exchange failed' });
}
});
// Check authorization status
router.get('/api/auth/status', (req, res) => {
const tokens = req.session.tokens;
const authorized = tokens && tokens.expires_at > Date.now();
res.json({ authorized });
});
// Token refresh helper
async function refreshTokens(req) {
const { refresh_token } = req.session.tokens;
const response = await axios.post('https://zoom.us/oauth/token', null, {
params: {
grant_type: 'refresh_token',
refresh_token
},
headers: {
'Authorization': 'Basic ' + Buffer.from(
`${process.env.ZOOM_APP_CLIENT_ID}:${process.env.ZOOM_APP_CLIENT_SECRET}`
).toString('base64')
}
});
req.session.tokens = {
access_token: response.data.access_token,
refresh_token: response.data.refresh_token,
expires_at: Date.now() + (response.data.expires_in * 1000)
};
return req.session.tokens;
}
module.exports = router;For guest mode users who need to upgrade their authorization:
// Use when user needs to grant additional permissions
await zoomSdk.promptAuthorize();
// Same onAuthorized listener fires
zoomSdk.addEventListener('onAuthorized', async (event) => {
// Handle same as initial authorization
});// Middleware to auto-refresh expired tokens
async function ensureAuthorized(req, res, next) {
if (!req.session.tokens) {
return res.status(401).json({ error: 'Not authorized' });
}
// Refresh if expiring within 5 minutes
if (req.session.tokens.expires_at < Date.now() + 300000) {
try {
await refreshTokens(req);
} catch (error) {
return res.status(401).json({ error: 'Token refresh failed' });
}
}
next();
}