Hi everyone — I’m running into a very specific issue with in-client OAuth (PKCE) that only occurs when I (the admin) add the app for a user in my account.
In the scenario when the user adds the app for themselves (or I as the admin add it for myself), Zoom redirects them through my auth/callback URL in the browser, I exchange the code normally, and everything works.
But when I (the admin) add the app for a user in my account
The user opens the app inside the Zoom client → auth/callback is never hit → I must authorize them via in-client OAuth using:
zoomSdk.authorize({ state, codeChallenge })
…which fires onAuthorized and gives me an authorization code. I try to exchange the authorization for access/refresh tokens, and I get a response of “Invalid authorization code”.
My front-end generates a PKCE pair with this helper:
const VERIFIER_LENGTH = 64;
const STATE_LENGTH = 32;
const CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
function randomString(length) {
const array = new Uint8Array(length);
window.crypto.getRandomValues(array);
let result = "";
for (let i = 0; i < array.length; i++) {
result += CHARSET[array[i] % CHARSET.length];
}
return result;
}
async function sha256(input) {
const encoder = new TextEncoder();
const data = encoder.encode(input);
const hashBuffer = await window.crypto.subtle.digest("SHA-256", data);
return new Uint8Array(hashBuffer);
}
function base64UrlEncode(bytes) {
let binary = "";
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
export async function createPkcePair() {
const codeVerifier = randomString(VERIFIER_LENGTH);
const hash = await sha256(codeVerifier);
const codeChallenge = base64UrlEncode(hash);
const state = randomString(STATE_LENGTH);
return { state, codeVerifier, codeChallenge };
}
Flow:
-
Front-end POSTs
{ state, codeVerifier, uid }to my/pkce-initroute, which saves these values to DB. -
Front-end calls
zoomSdk.authorize({ state, codeChallenge }). -
Front-end listens to
zoomSdk.onAuthorized(event):- receives:
code,state,redirectUri,result,timestamp
- receives:
-
Front-end POSTs
{ code, state, redirectUri }to/token-exchangeroute. -
Backend:
-
Looks up
codeVerifierfrom DB usingstate -
Calls Zoom:
-
POST https://zoom.us/oauth/token
grant_type=authorization_code
code=<from onAuthorized>
code_verifier=<from DB>
redirect_uri=<from onAuthorized>
Authorization: Basic <base64(devClientId:devClientSecret)>
I am testing all of this in the Development environment and using the Development client ID / secret. I have the redirect_uri, as it appears from onAuthorized response, listed in my OAuth Allow List.
What I have already read and tried
I’ve read more than a dozen posts about this same issue, and tried the steps that those developers attempted, or what was suggested by Zoom associates.
And I have already verified:
PKCE pairing (codeVerifier ↔ codeChallenge)
redirectUri matching
OAuth Allow List includes the Home URL
Development client ID/secret are used
Authorization code only used once
App is installed for the user and permissions accepted
No duplicate onAuthorized listeners
Curl test uses a fresh untouched code
Despite all this, in-client authorization codes always return invalid_grant.
Can you suggest what I may be doing wrong?
Thank you.