# Zoom Apps SDK - OAuth Reference OAuth flows for Zoom Apps: web-based redirect, In-Client, and third-party. ## Three OAuth Flows | Flow | UX | When to Use | |------|----|-------------| | **Web-based redirect** | Opens browser, redirect back | Initial install from Marketplace | | **In-Client OAuth** | Popup inside Zoom, no redirect | Subsequent authorizations (best UX) | | **Third-party OAuth** | External provider (Auth0, Google) | When your app needs non-Zoom auth | ## PKCE (Required for All Flows) All Zoom Apps OAuth must use PKCE (Proof Key for Code Exchange): ```javascript const crypto = require('crypto'); // Generate PKCE pair const verifier = crypto.randomBytes(32).toString('hex'); const challenge = crypto.createHash('sha256') .update(verifier) .digest('base64url'); // verifier: stored server-side (never exposed to client) // challenge: sent with authorization request ``` ## Flow 1: Web-Based OAuth (Initial Install) ``` User clicks "Add" in Marketplace | v GET https://zoom.us/oauth/authorize ?client_id=YOUR_CLIENT_ID &response_type=code &redirect_uri=YOUR_REDIRECT_URI &code_challenge=CHALLENGE &code_challenge_method=S256 &state=RANDOM_STATE | v User authorizes -> Zoom redirects to YOUR_REDIRECT_URI?code=AUTH_CODE&state=STATE | v Backend validates state, exchanges code for tokens | v Backend gets deeplink, redirects user to Zoom client ``` ### Server Route Handler ```javascript app.get('/auth', async (req, res) => { const { code, state } = req.query; // Validate state (CSRF protection) if (state !== req.session.state) { return res.status(403).send('Invalid state'); } // Exchange code for tokens 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') } }); const { access_token, refresh_token, expires_in } = tokenResponse.data; // Store tokens securely req.session.tokens = { access_token, refresh_token, expires_at: Date.now() + expires_in * 1000 }; // Get deeplink to open app in Zoom const deeplink = await axios.post('https://api.zoom.us/v2/zoomapp/deeplink', { action: '' }, { headers: { 'Authorization': `Bearer ${access_token}` } } ); res.redirect(deeplink.data.deeplink); }); ``` ## Flow 2: In-Client OAuth (Best UX) No browser redirect - authorization happens inside Zoom: ```javascript // Frontend const { codeChallenge, state } = await fetch('/api/auth/challenge').then(r => r.json()); zoomSdk.addEventListener('onAuthorized', async (event) => { await fetch('/api/auth/token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ code: event.code, state: event.state }) }); }); await zoomSdk.authorize({ codeChallenge, state }); ``` See **[In-Client OAuth example](../examples/in-client-oauth.md)** for complete implementation. ## Token Exchange Endpoint ``` POST https://zoom.us/oauth/token Headers: Authorization: Basic base64(CLIENT_ID:CLIENT_SECRET) Parameters: grant_type=authorization_code code=AUTH_CODE redirect_uri=YOUR_REDIRECT_URI code_verifier=PKCE_VERIFIER ``` Response: ```json { "access_token": "...", "token_type": "bearer", "refresh_token": "...", "expires_in": 3600, "scope": "zoomapp:inmeeting" } ``` ## Token Refresh Access tokens expire in 1 hour. Use refresh token to get new ones: ```javascript async function refreshTokens(refreshToken) { const response = await axios.post('https://zoom.us/oauth/token', null, { params: { grant_type: 'refresh_token', refresh_token: refreshToken }, headers: { 'Authorization': 'Basic ' + Buffer.from( `${process.env.ZOOM_APP_CLIENT_ID}:${process.env.ZOOM_APP_CLIENT_SECRET}` ).toString('base64') } }); return response.data; // { access_token, refresh_token, expires_in } } ``` **Note:** Refresh tokens are single-use. Each refresh returns a new refresh_token. ## Deep Linking After web OAuth, get a deeplink to open your app in Zoom: ```javascript const response = await axios.post('https://api.zoom.us/v2/zoomapp/deeplink', { action: '' }, { headers: { 'Authorization': `Bearer ${accessToken}` } } ); const { deeplink } = response.data; // Redirect user to this URL to open app in Zoom client ``` ## Required Scopes | Scope | Description | |-------|-------------| | `zoomapp:inmeeting` | In-meeting functionality (most common) | | `user:read` | Read user profile | | `meeting:read` | Read meeting details | | `meeting:write` | Create/modify meetings | ## Token Storage Patterns | Pattern | When to Use | |---------|-------------| | **Redis** | Multi-instance production servers | | **Session cookie** | Simple single-server apps | | **Firestore** | Serverless (Firebase) | | **Encrypted database** | Complex apps with user accounts | ## Resources - **Auth docs**: https://developers.zoom.us/docs/zoom-apps/authentication/ - **In-Client OAuth example**: [../examples/in-client-oauth.md](../examples/in-client-oauth.md) - **oauth skill**: [../../oauth/SKILL.md](../../oauth/SKILL.md)