Zoom OAuth 2.0 Proof Key for Code Exchange (PKCE) Flow

Summary

Zoom supports Proof of Key Code Exchange (PKCE), pronounced “Pixy”, industry - standard protocol for authorization when requesting user tokens. This offers better security by enabling clients to use a code challenge and code exchange as part of the initial user authorization request. See rfc7636 for details.

What is PKCE?

PKCE is an extension to the OAuth 2 spec. Its design aims to add an additional layer of security that verifies that the authentication and token exchange requests come from the same client. This is achieved through the use of the code_challenge and code_verifier parameters, sent by the third-party application during the OAuth process.

:dart: The code_verifier parameter’s value uses the SHA 256 Cryptographic Hash Algorithm. SHA 256 creates a unique “signature” for a text or file of any size,

:dart: The code_challenge Base64-encodes the value generated from the code_verifer step.

:warning: Note:

  • Regardless of the input file or text, the generated value is always the same length

  • Guaranteed to be always produce the same result for the same input

  • One way (that is, you can’t reverse engineer it to reveal the original input)

  • Code_challenge value should be equivalent to the code_verifier parameter’s value when using the “plain” code challenge mode, or a cryptographic hash of the code_verifier parameters’ value, though the use of the “plain” code challenge method is largely discouraged.

Generate the Code_verifier and Code_challenge:

:athletic_shoe: You can manually generate the code_verifier by entering a random string into the SHA256 online tool on Github:

SHA256 Tool Screenshot:

:athletic_shoe: Then, manually encode the encrypted value with the Base64 online tool:

Base64encode Tool Screenshot:

Programmatically generate the Code_verifier and Code_challenge

Alternatively, you can programmatically generate the Code_verifier and Code_challenge with the following script:

mkdir crypto-gen
cd crypto-gen
npm init -y

const crypto = require("crypto");
const btoa = require('btoa');


function base64URLEncode(str) {
    return str.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}


function sha256_ASCII(buffer) {
  return crypto.createHash('sha256').update(buffer).digest('base64').toString("ascii");
}

const code_Verifier = base64URLEncode(crypto.randomBytes(32));
const code_ChallengeUrlSafeEncode = base64URLEncode(sha256_ASCII(code_Verifier));

console.log("code_verifier: " + code_Verifier);

# Append trailing "=" to the end code_challenge_url_safe_encode
console.log("URL safe encode code_challenge : " +  code_ChallengeUrlSafeEncode + "=");

PKCE Flow Overview

:dart: During the initial step ( https://zoom.us/oauth/authorize ) of the OAuth procedure, the code_challenge parameter is provided by the third-party client to the OAuth provider.

:dart: After the user has manually authorized the application, the OAuth provider should then respond with an authorization code, which must be returned to the OAuth provider along with the code_verifier parameter, by the third-party client, in exchange for an access token.

:dart: If the code_verifier parameter does not match the code_challenge provided by the client in the initial authorization request, this may indicate an attempted code injection attack, and so the OAuth process should be abandoned by the provider and an error returned to the third-party client.

:dart: If Zoom verifies that the code_challenge and the code_verifier match, the token endpoint (https://zoom.us/oauth/token) continues processing.

1 Like

Zoom OAuth 2.0 PKCE Flow: In Postman

Step 1: Getting an Access Token

A. Direct the user to https://zoom.us/oauth/authorize with the following query parameters:

Postman Authorization Tab Configuration:

  1. Under the Authorization tab of any request, select OAuth 2.0.Select Get New Access Token.

  2. From there, select a Grant Type of Authorization Code (With PKCE), and click Authorize using the browser checkbox.

  3. In the Auth URL field, enter Zoom Authorization endpoint with response_type and code_challenge params values included:

      https://zoom.us/oauth/authorize?response_type=code&code_challenge= {{A challenge derived from the code verifier}}
    
  4. In the Access Token URL field, enter the access token endpoint

    https://zoom.us/oauth/token
    
  5. In the clientID field, enter Zoom Marketplace OAuth app clientID.

  6. In the Code Challenge Method field, select SHA-256 and click Get New Access token.

  7. Postman will open a browser tab that redirects to the endpoint entered for the Zoom Marketplace redirect URL. Appended to the end of the Redirect URL, you will find the Authorization Code, which must be returned to the OAuth provider (Zoom) to get Access Token. This example leverages Postman’s redirect endpoint (https://oauth.pstmn.io/v1/callback):

Postman Authorization Tab:

Here is a screenshot of what the populated Authorization tab looks like:

Step 2: Request Access Token:

A. Create Post to https://zoom.us/oauth/token with the following headers and query parameters:

Postman Params Tab Configuration :

  1. On the Postman Params Tab, set code, grant_type, redirect_url, and code_verifer param values:

  1. On the Postman Headers Tab , set Content-Type and Authorization headers, then click Send:

For reference, here is a video demonstrating the PKCE Flow:

(Coming Soon)

:books: Resources

:star2: OAuth with Zoom Help Documentation

PKCE will be enforced in May 2022. Is there a way to enforce this sooner for an app, so that the PKCE enhancements can be checked so we can be absolutely sure that when the May 2022 switch over occurs, we’re good. I have checked the app settings in our test apps and can’t find a way to enforce PKCE and the form encoded token requests, and so although we have coded and tested the changes, we are not able to see the effect of toggling the enforcement, so we are left to assume our changes will just work.

1 Like

Also looking at this as well. Will this be able to be tested?

Greeting@Danz & @smurray,

Thank you both for this question. Let me check internally, and I’ll let you know what I learn.

Best,
Donte

1 Like

Thanks. Can you also check your code sample. It seems to be producing a code_challenge that doesn’t line up with most other implementations in industry.

I think the place where it diverges is with the btoa(...) call which produces a code_challenge that is different than other algorithms do. This threw me off for a while.

See: javascript - Creating a code verifier and challenge for PKCE auth on Spotify API in ReactJS - Stack Overflow

Also a good resource for others looking at this. A PKCE validator/generator:

https://example-app.com/pkce

UPDATE–

After some more testing, it seems that only your implementation above seems to work with the OAuth flow. If you try any other standard implementation to get a code_challenge from a code_verifier, they all fail on authentication with an invalid_grant response but if I use your code above (which produces different code_challenge results). I can successfully do PKCE auth flow. Can you give some guidance on this? It seems you are using a non-standard implementation.
@donte.S ?

@Danz I don’t know if you have seen this but it looks like if you send the code_challenge with the initial auth request, it enforces that you have the correct code_verifier on the token request. So you can test out your implementation now. They just don’t enforce it yet. It will at least give you confidence that your solution works.

@smurray agreed and we’ve implemented it and have seen behaviour to indicate that this is all being processed and our solution works. However, we don’t have the peace of mind of having tested the situation when they flip the switch. So, we are left to assume we’ll be good rather than know for sure.

Hey @donte.zoom

I dug a little more into getting this working for us and it seems that the core of the issue with your (Zoom’s) implementation of PKCE vs. other API providers who are implementing this is here:

const code_challenge = btoa(sha256(code_verifier));

sha256(code_verifier) returns a string object in javascript so then the input to this:
btoa(hashed_code_verifier) is a string as well. This leads to the resulting output being twice as long (64 bytes/characters if you start with a 32 byte code_verifier) than what other implementations are doing. The rest produce the same size code_challenge as code_verifier.

I imagine for anyone using javascript/NodeJS this isn’t much of a problem because they can just copy your code. Since our code is in C# (and any other language like it, Java, C++ will have to make the same adjustment) I had to write some extra coding to get your result.

Example that works with Zoom’s current implementation where a 32 bit code_verifier produces a 64 bit code_challenge due to the byte[]string conversion that happens.

public static string GetZoomCodeChallengeFromVerifier(string codeVerifier)
{
    string codeChallenge = string.Empty;
    using (SHA256 sha = SHA256.Create())
    {
        var bytes = Encoding.UTF8.GetBytes(codeVerifier);
        var hash = sha.ComputeHash(bytes);

        // Here is where Zoom goes out into left field due to their Javascript implementation
        // they do this:
        // code_challenge = btoa(sha256(code_verifier))
        // so that the type passed to btoa is a string representation of the hash,
        // not a byte representation
        // So the input is roughtly twice as long
        // so btoa() makes a longer string that is base 64 encoded
        // So we have to follow and covert the hash from bytes to a string to match the behavior
        string hashString = BitConverter.ToString(hash).Replace("-", "").ToLower();
        byte[] bytesNew = Encoding.GetEncoding(28591).GetBytes(hashString);
        codeChallenge = System.Convert.ToBase64String(bytesNew);
    }

    return codeChallenge;
}

Example of what common practice seems to be elsewhere to produce the code_challenge. For example a 32 bit code_verifier produces a 32 bit code_challenge

public static string GetZoomCodeChallengeFromVerifier(string codeVerifier)
{
    string codeChallenge = string.Empty;
    using (SHA256 sha = SHA256.Create())
    {
        var bytes = Encoding.UTF8.GetBytes(codeVerifier);
        byte[] hash = sha.ComputeHash(bytes);
        // Here the hash is taken in directly as a byte array
        codeChallenge = System.Convert.ToBase64String(hash);
    }

    return codeChallenge;
}

The OAuth.com PKCE page gives the following javascript implementation to produce the code_challenge. This lines up with other examples I have seen:

// Dependency: Node.js crypto module
// https://nodejs.org/api/crypto.html#crypto_crypto
function sha256(buffer) {
    return crypto.createHash('sha256').update(buffer).digest();
}
var challenge = base64URLEncode(sha256(verifier));

As an example. The following shows what happens with a given code_verifier and results of Zoom’s implementation and OAuth.com’s one.

code_verifier:            OoRepCjjX8P4perVeWHK-5TKSfyZLPuCjirkjY3hh7I
Zoom's code_challenge: YWRmMTUyNGZkODZlMDFiMTI4ZTQ1M2Y0NzExYWNkNWJjZDc2NWU2ZTRjMDYxMWVjYmJiZmZhMmNkYzRhOWYyOA==
OAuth's code_challenge: rfFST9huAbEo5FP0cRrNW812Xm5MBhHsu7_6LNxKnyg

Can you speak the the difference in what the OAuth.com page itself recommends and why your implementation is different and produces different results?

2 Likes

@smurray,

Thanks for letting us know about your situation. You’re right! The current implementation does not follow other API providers’ implementation. I can understand that it’s frustrating to have to make adjustments to your code to get the expected code_challenge result. Our engineers are aware of this and are making plans to make changes to better align with a more consistent PKCE flow standard. For now, the javascript/Nodejs above can serve as an implementation reference for completing the PKCE flow.

Thanks again for taking the time to talk to us! I appreciate your feedback on this matter, and I’ll be sure to keep you updated. You can also stay up-to-date with any new or upcoming features, by following our changelog and upcoming changes pages.

Best,
Donte

1 Like

@Danz & @smurray,

As a follow-up, it looks like the Zoom OAuth Security requirement will be pushed back to a later date. The date has not been set yet, however, I will provide another update when it is published.

Warm Regards,
Donte

1 Like

Thanks @donte.zoom

So to confirm, May 2022 is no longer the date when PKCE will be enforced and it will be some later date?

Correct, @smurray! The May 2022 date is under review and will likely no longer be when PKCE is enforced. A new date hasn’t been set at the moment. However, I will provide another update as soon as it is.

@smurray & @Danz,

Please see the updated notice on Zoom OAuth Security Update.

Zoom OAuth Security Updates Link

With Care,
Donte

Thank you for drawing our attention to this I appreciate it!

1 Like

My pleasure @Danz!

With Care,
Donte

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.