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

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