zoom: add redirectUri config for OAuth

Add redirectUri prompt in onboarding and update docs.
Fixes OAuth token exchange by ensuring redirect_uri matches Zoom app settings.
This commit is contained in:
Pranav Katariya 2026-01-28 02:39:15 -08:00
parent bc109c4ae3
commit 2310030f5f
4 changed files with 56 additions and 7 deletions

View File

@ -25,6 +25,7 @@ Status: production-ready plugin for Zoom Team Chat direct messages via Team Chat
clientSecret: "YOUR_CLIENT_SECRET",
botJid: "YOUR_BOT_JID@xmppdev.zoom.us",
secretToken: "YOUR_SECRET_TOKEN",
redirectUri: "https://yourdomain.com/api/zoomapp/auth",
dm: { policy: "open" }
}
}
@ -81,6 +82,12 @@ Replace `gateway-host` with:
Subscribe to these event types:
- `bot_notification` - Required for receiving messages
**Important**: The `redirectUri` in your Moltbot config must EXACTLY match the OAuth Redirect URL configured in your Zoom app. OAuth will fail if these don't match.
Examples:
- Local dev: `https://abc123.ngrok.io/api/zoomapp/auth`
- Production: `https://yourdomain.com/api/zoomapp/auth`
### 3. Install App to Your Account
1) In app settings, click "Install"
@ -100,6 +107,7 @@ Add to `~/.clawdbot/moltbot.json`:
clientSecret: "YOUR_CLIENT_SECRET",
botJid: "YOUR_BOT_JID@xmppdev.zoom.us",
secretToken: "YOUR_SECRET_TOKEN",
redirectUri: "https://yourdomain.com/api/zoomapp/auth", // Must match Zoom app OAuth Redirect URL
apiHost: "https://zoomdev.us", // Use https://api.zoom.us for production
oauthHost: "https://zoomdev.us", // Use https://zoom.us for production
dm: {

View File

@ -129,6 +129,43 @@ export const zoomOnboardingAdapter: ChannelOnboardingAdapter = {
}),
).trim();
// Prompt for OAuth Redirect URI
await prompter.note(
[
"OAuth Redirect URL is the public URL where Zoom will redirect after installation.",
"",
"Examples:",
"- Local dev: https://abc123.ngrok.io/api/zoomapp/auth (use ngrok http 3001)",
"- Production: https://yourdomain.com/api/zoomapp/auth",
"",
"This MUST exactly match the OAuth Redirect URL configured in your Zoom app.",
].join("\n"),
"OAuth Redirect URL",
);
const redirectUri = String(
await prompter.text({
message: "OAuth Redirect URL",
placeholder: "https://yourdomain.com/api/zoomapp/auth",
validate: (value) => {
const trimmed = String(value ?? "").trim();
if (!trimmed) return "OAuth Redirect URL is required";
try {
const url = new URL(trimmed);
if (!url.protocol.startsWith("http")) {
return "URL must start with http:// or https://";
}
if (!trimmed.endsWith("/api/zoomapp/auth")) {
return "URL should end with /api/zoomapp/auth";
}
return undefined;
} catch {
return "Invalid URL format";
}
},
}),
).trim();
// Determine environment from Bot JID
const isDev = botJid.includes("@xmppdev");
const apiHost = isDev ? "https://zoomdev.us" : "https://api.zoom.us";
@ -145,6 +182,7 @@ export const zoomOnboardingAdapter: ChannelOnboardingAdapter = {
clientSecret,
botJid,
secretToken,
redirectUri,
apiHost,
oauthHost,
},
@ -152,19 +190,16 @@ export const zoomOnboardingAdapter: ChannelOnboardingAdapter = {
};
// Show URL configuration instructions
const gatewayHost = redirectUri.replace(/\/api\/zoomapp\/auth$/, "");
await prompter.note(
[
"IMPORTANT: Configure these URLs in your Zoom app:",
"",
"Webhook URL (for receiving messages):",
" https://gateway-host/webhooks/zoom",
` ${gatewayHost}/webhooks/zoom`,
"",
"OAuth Redirect URL (for app installation):",
" https://gateway-host/api/zoomapp/auth",
"",
"Replace gateway-host with:",
"- For local dev: Use ngrok (ngrok http 3001)",
"- For production: Your server's public domain",
` ${redirectUri}`,
"",
"Subscribe to event type: bot_notification",
].join("\n"),

View File

@ -7,6 +7,7 @@ export const ZoomConfigSchema = z.object({
clientSecret: z.string().optional(),
botJid: z.string().optional(),
secretToken: z.string().optional(),
redirectUri: z.string().optional(),
apiHost: z.string().optional(),
oauthHost: z.string().optional(),
dm: z
@ -26,6 +27,7 @@ export type ResolvedZoomAccount = {
clientSecret?: string;
botJid?: string;
secretToken?: string;
redirectUri?: string;
apiHost: string;
oauthHost: string;
config: ZoomConfig;
@ -46,6 +48,7 @@ export function resolveZoomAccount(params: {
clientSecret: config.clientSecret,
botJid: config.botJid,
secretToken: config.secretToken,
redirectUri: config.redirectUri,
apiHost: config.apiHost || "https://api.zoom.us",
oauthHost: config.oauthHost || "https://zoom.us",
config,

View File

@ -178,7 +178,10 @@ export async function monitorZoomProvider(opts: MonitorZoomOpts = {}): Promise<v
const tokenUrl = `${account.oauthHost}/oauth/token`;
const tokenParams = new URLSearchParams();
tokenParams.set("grant_type", "authorization_code");
tokenParams.set("redirect_uri", `${req.protocol}://${req.get("host")}/api/zoomapp/auth`);
// Use configured redirectUri or fall back to dynamically built one
const redirectUri =
account.redirectUri || `${req.protocol}://${req.get("host")}/api/zoomapp/auth`;
tokenParams.set("redirect_uri", redirectUri);
tokenParams.set("code", code);
const authHeader = Buffer.from(`${account.clientId}:${account.clientSecret}`).toString(