Subchapter 157.10
concepts/security.mdMarkdown5 KBView on GitHub
Zoom Apps must meet security requirements for Marketplace approval. This covers required headers, CSP configuration, cookie security, and token storage.
Zoom’s security review requires these HTTP headers on all responses:
| Header | Required Value | Purpose |
|---|---|---|
Strict-Transport-Security | max-age=31536000 | Force HTTPS for 1 year |
X-Content-Type-Options | nosniff | Prevent MIME type sniffing |
Content-Security-Policy | frame-ancestors 'self' zoom.us *.zoom.us | Allow Zoom to embed your app |
Referrer-Policy | same-origin | Limit referrer info |
X-Frame-Options | ALLOW-FROM zoom.us | Legacy frame control |
app.use((req, res, next) => {
res.setHeader('Strict-Transport-Security', 'max-age=31536000');
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('Content-Security-Policy',
"frame-ancestors 'self' zoom.us *.zoom.us"
);
res.setHeader('Referrer-Policy', 'same-origin');
next();
});Critical: The frame-ancestors CSP directive is what allows Zoom’s embedded browser to load your app. Without it, the browser blocks the frame.
Zoom’s embedded browser requires specific cookie settings:
// Express cookie-session example
app.use(require('cookie-session')({
name: 'session',
keys: [process.env.SESSION_SECRET],
maxAge: 24 * 60 * 60 * 1000, // 24 hours
sameSite: 'none', // REQUIRED - Zoom embeds your app cross-origin
secure: true // REQUIRED - SameSite=None requires Secure
}));Why SameSite=None? Your app runs inside Zoom’s embedded browser, which is a different origin. Without SameSite=None, the browser won’t send cookies to your server, and sessions break silently.
All Zoom Apps OAuth flows should use PKCE (Proof Key for Code Exchange):
const crypto = require('crypto');
// Generate PKCE pair
const verifier = crypto.randomBytes(32).toString('hex');
const challenge = crypto.createHash('sha256')
.update(verifier)
.digest('base64url');
// Store verifier in session (server-side)
req.session.codeVerifier = verifier;
// Send challenge to frontend (or include in OAuth redirect URL)
res.json({ codeChallenge: challenge, state: req.session.state });PKCE prevents authorization code interception attacks. The code_verifier never leaves your server.
Never store tokens in the frontend (localStorage, sessionStorage, cookies).
| Storage | When to Use | Security |
|---|---|---|
| Redis | Multi-instance servers, production | Fast, TTL support, scalable |
| Encrypted session | Single server, simple apps | Tied to server process |
| Firestore/DynamoDB | Serverless (Firebase/Lambda) | Persistent, managed |
| Database (encrypted) | Complex apps with user accounts | Full control, encrypted at rest |
// Redis token storage example
const Redis = require('ioredis');
const redis = new Redis(process.env.REDIS_URL);
async function storeTokens(zoomUserId, tokens) {
await redis.set(
`zoom:tokens:${zoomUserId}`,
JSON.stringify(tokens),
'EX', tokens.expires_in // Auto-expire with token
);
}
async function getTokens(zoomUserId) {
const data = await redis.get(`zoom:tokens:${zoomUserId}`);
return data ? JSON.parse(data) : null;
}Always validate the OAuth state parameter:
const crypto = require('crypto');
// Generate state before OAuth redirect
const state = crypto.randomBytes(16).toString('hex');
req.session.oauthState = state;
// Validate state on callback
app.get('/auth', (req, res) => {
if (req.query.state !== req.session.oauthState) {
return res.status(403).send('Invalid state - possible CSRF attack');
}
// Proceed with token exchange
});| Layer | What You Access | Authorization | Risk Level |
|---|---|---|---|
| SDK (contextual) | Meeting context, user info, UI controls | config() capabilities | Low - scoped to current context |
| REST API (server-side) | Full Zoom API (users, meetings, recordings) | OAuth access tokens | Medium - broader data access |
| X-Zoom-App-Context | User identity, meeting info | Client secret decryption | Low - read-only identity |
Principle of least privilege: Only request the OAuth scopes and SDK capabilities you actually need.
SameSite=None; Secure on cookiesframe-ancestors zoom.us *.zoom.us