HEX
Server: Apache/2.4.46 (Win64) OpenSSL/1.1.1j PHP/8.4.25
System: Windows NT DESKTOP-4TAV2RJ 10.0 build 19045 (Windows 10) AMD64
User: fred (0)
PHP: 8.4.25
Disabled: NONE
Upload Files
File: C:/Users/fred/.codex/.tmp/plugins/plugins/zoom/skills/zoom-apps-sdk/examples/in-client-oauth.md
# In-Client OAuth with PKCE

Authorize users without leaving the Zoom client. Best UX for returning users.

## Why In-Client OAuth?

| 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 |

## Flow

```
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 }
```

## Frontend Implementation

```javascript
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 });
}
```

## Backend Implementation

```javascript
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;
```

## Re-Authorization with promptAuthorize

For guest mode users who need to upgrade their authorization:

```javascript
// 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
});
```

## Token Refresh Pattern

```javascript
// 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();
}
```

## Resources

- **Auth guide**: https://developers.zoom.us/docs/zoom-apps/authentication/
- **OAuth reference**: [../references/oauth.md](../references/oauth.md)