# In-client OAuth + PKCE: Always getting invalid\_grant (“Invalid authorization code”)

**URL:** <https://devforum.zoom.us/t/in-client-oauth-pkce-always-getting-invalid-grant-invalid-authorization-code/139261>\
**Category:** API and Webhooks\
**Created:** [December 4, 2025, 7:15pm UTC](https://devforum.zoom.us/t/in-client-oauth-pkce-always-getting-invalid-grant-invalid-authorization-code/139261 "2025-12-04T19:15:57Z")\
**Posts on this page:** 1\
**Page:** 1

<div class="post-metadata">

**Author:** ![John34](https://sea2.discourse-cdn.com/flex016/user_avatar/devforum.zoom.us/john34/32/50617_2.png) [@John34](https://devforum.zoom.us/u/John34)\
**Post date:** [December 4, 2025, 7:15pm UTC](https://devforum.zoom.us/t/in-client-oauth-pkce-always-getting-invalid-grant-invalid-authorization-code/139261/1 "2025-12-04T19:15:57Z")

</div>

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:

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

```javascript
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:**

1. Front-end POSTs `{ state, codeVerifier, uid }` to my `/pkce-init` route, which saves these values to DB.

2. Front-end calls `zoomSdk.authorize({ state, codeChallenge })`.

3. Front-end listens to `zoomSdk.onAuthorized(event)`:

4. Front-end POSTs `{ code, state, redirectUri }` to `/token-exchange` route.

5. Backend:

```auto
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.
