You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This guide covers the Node.js/Express backend implementation for Zoom Apps, including OAuth 2.0 flows, token management, session handling, and Zoom REST API proxy.
// Route: GET /api/zoomapp/installinstall(req,res){// 1. Generate and save state for CSRF protectionreq.session.state=generateState()// 2. Build OAuth authorization URLconstdomain=process.env.ZOOM_HOSTconstpath='oauth/authorize'constparams={redirect_uri: process.env.ZOOM_APP_REDIRECT_URI,response_type: 'code',client_id: process.env.ZOOM_APP_CLIENT_ID,state: req.session.state,}constauthRequestParams=createRequestParamString(params)constredirectUrl=`${domain}/${path}?${authRequestParams}`// 3. Redirect user to Zoom OAuth pageres.redirect(redirectUrl)}
OAuth Callback Handler
// Route: GET /api/zoomapp/auth?code=XXX&state=YYYasyncauth(req,res,next){constzoomAuthorizationCode=req.query.codeconstzoomAuthorizationState=req.query.stateconstzoomState=req.session.state// Destroy session for securityreq.session.destroy()// Validate authorization codeif(!zoomAuthorizationCode){consterror=newError('No authorization code was provided')error.status=400returnnext(error)}// Validate state (CSRF protection)if(!zoomAuthorizationState||zoomAuthorizationState!==zoomState){consterror=newError('Invalid state parameter')error.status=400returnnext(error)}try{// Exchange code for tokensconsttokenResponse=awaitgetZoomAccessToken(zoomAuthorizationCode)constzoomAccessToken=tokenResponse.data.access_token// Get user infoconstuserResponse=awaitgetZoomUser(zoomAccessToken)constzoomUserId=userResponse.data.id// Save tokens to storeawaitstore.upsertUser(zoomUserId,tokenResponse.data.access_token,tokenResponse.data.refresh_token,Date.now()+tokenResponse.data.expires_in*1000)// Generate deeplink to return to Zoom clientconstdeepLinkResponse=awaitgetDeeplink(zoomAccessToken)res.redirect(deepLinkResponse.data.deeplink)}catch(error){returnnext(error)}}
In-Client OAuth Flow (PKCE)
Generate Code Challenge
// Route: GET /api/zoomapp/authorizeasyncinClientAuthorize(req,res,next){try{// Generate code verifier, challenge and stateconstcodeVerifier=generateCodeVerifier()constcodeChallenge=codeVerifierconstzoomInClientState=generateState()// Save to sessionreq.session.codeVerifier=codeVerifierreq.session.state=zoomInClientState// Return to frontendreturnres.json({
codeChallenge,state: zoomInClientState,})}catch(error){returnnext(error)}}
Exchange Code for Token
// Route: POST /api/zoomapp/onauthorizedasyncinClientOnAuthorized(req,res,next){constzoomAuthorizationCode=req.body.codeconsthref=req.body.hrefconststate=decodeURIComponent(req.body.state)constzoomInClientState=req.session.stateconstcodeVerifier=req.session.codeVerifiertry{// Validate stateif(!zoomAuthorizationCode||state!==zoomInClientState){thrownewError('State mismatch')}// Exchange code for tokens with PKCE verifierconsttokenResponse=awaitgetZoomAccessToken(zoomAuthorizationCode,href,codeVerifier)constzoomAccessToken=tokenResponse.data.access_token// Get user infoconstuserResponse=awaitgetZoomUser(zoomAccessToken)constzoomUserId=userResponse.data.idreq.session.user=zoomUserId// Save tokensawaitstore.upsertUser(zoomUserId,tokenResponse.data.access_token,tokenResponse.data.refresh_token,Date.now()+tokenResponse.data.expires_in*1000)returnres.json({result: 'Success'})}catch(error){returnnext(error)}}
App Home URL Handler
// Route: GET /api/zoomapp/homehome(req,res,next){try{// Check for x-zoom-app-context headerif(!req.headers['x-zoom-app-context']){thrownewError('x-zoom-app-context header is required')}// Decrypt the Zoom App context headerconstdecryptedAppContext=decryptZoomAppContext(req.headers['x-zoom-app-context'],process.env.ZOOM_APP_CLIENT_SECRET)// Verify not expiredif(!decryptedAppContext.exp||decryptedAppContext.exp<Date.now()){thrownewError('x-zoom-app-context header is expired')}// Persist user id and meetingUUID to sessionreq.session.user=decryptedAppContext.uidreq.session.meetingUUID=decryptedAppContext.mid}catch(error){returnnext(error)}res.redirect('/api/zoomapp/proxy')}
constrefreshToken=async(req,res,next)=>{constuser=req.appUserconst{ expired_at =0, refreshToken =null}=userif(!refreshToken){returnnext(newError('No refresh token saved for this user'))}// Check if expired with 5 second bufferif(expired_at&&Date.now()>=expired_at-5000){try{consttokenResponse=awaitrefreshZoomAccessToken(user.refreshToken)awaitstore.updateUser(req.session.user,{accessToken: tokenResponse.data.access_token,refreshToken: tokenResponse.data.refresh_token,expired_at: Date.now()+tokenResponse.data.expires_in*1000,})}catch(error){returnnext(newError('Error refreshing user token'))}}returnnext()}
constsetZoomAuthHeader=async(req,res,next)=>{try{if(!req.session.user){thrownewError('No user in session')}constuser=awaitstore.getUser(req.session.user)if(!user||!user.accessToken){thrownewError('No Zoom REST API access token for this user')}req.headers['Authorization']=`Bearer ${user.accessToken}`returnnext()}catch(error){returnnext(error)}}