PKCE OAuth and the Meeting SDK for Windows

View as Markdown

Authorization Code with Proof Key for Code Exchange (PKCE) is an OAuth flow similar to the standard Authorization Code flow, except it doesn't require that you have a backend server to get the authorization token.

Prerequisites

  • Understanding of C++ or C#.
  • Visual Studio 2019 or later.
  • Zoom Meeting SDK version 5.9.0 or newer. Version 7.0.2 is preferred.
  • Zoom Meeting SDK and OAuth credentials.

After you've created your project, downloaded the Meeting SDK files, and integrated the SDK into your app, set up a custom URL scheme. Then authenticate the users with PKCE, get the ZAK token, and start a meeting with that ZAK token.

Set up a custom URL scheme

Follow the steps on Microsoft's web site to set up a custom URL scheme for Windows .

Authenticate users with PKCE

Now that your app is configured to handle your custom URI scheme, start implementing the PKCE OAuth flow.

Generate the code verifier and challenge

First, generate a code verifier using the code below, then hash the verifier to create a code challenge. We'll host these in a dedicated CodeChallengeHelper class. A field inside the class will also be needed to store the verifier, since the same verifier used to generate the challenge must be provided when requesting an access token.

class CodeChallengeHelper
{
public:
	static std::string verifier;
	static void createCodeVerifier();
	static std::string getCodeChallenge();
};

Within that same class, we'll need to define two methods: createCodeVerifier to generate a new verifier and getCodeChallenge to create a code challenge using the verifier.

Implement the two methods

void CodeChallengeHelper::createCodeVerifier()
{
    char byteArray[32];
    randombytes_buf(byteArray, 32);
    BYTE* Bytes = reinterpret_cast<BYTE*>(const_cast<char*>(byteArray));

    char base64[256];
    DWORD base64Len;
    if (CryptBinaryToStringA(Bytes, 32, CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF,base64, &base64Len))
    {
        verifier = std::string(base64);
        strFindAndReplace(verifier, "+", "-");
        strFindAndReplace(verifier, "/", "_");
        trimPadding(verifier);
    }
}

string CodeChallengeHelper::getCodeChallenge()
{
    const char* byteArray = CodeChallengeHelper::verifier.c_str();

    unsigned char hash[SHA256_DIGEST_LENGTH];
    SHA256_CTX sha256;
    SHA256_Init(&sha256);
    SHA256_Update(&sha256, byteArray, strlen(byteArray));
    SHA256_Final(hash, &sha256);

    char base64[256];
    DWORD base64Len;

    if (CryptBinaryToStringA(hash, 32, CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, base64, &base64Len))
    {
        auto base64Str = string(base64);
        strFindAndReplace(base64Str, "+", "-");
        strFindAndReplace(base64Str, "/", "_");
        trimPadding(base64Str);
        return base64Str;
    }
    return string();
}

In the implementation of the two methods above, we used multiple third party libraries and customized functions. You need to add declarations and definitions to the class source file. For PKCE processing, encode the "verifier" and its "challenge" with BASE64 without padding and be safe to be included in an URL. Use strFindAndReplace to replace characters that are not safe for the URL, and use trimPadding to remove BASE64 padding.

#include <windows.h>
#include "sodium.h" // For generating random bytes.
#include "openssl/sha.h" // For generating SHA256 hash.
#include "wincrypt.h" // For encoding with BASE64.
#pragma comment(lib, "crypt32.lib") // For encoding with BASE64.
#include "CodeChallengeHelper.h"
using namespace std;

string CodeChallengeHelper::verifier = "";

void strFindAndReplace(std::string& str,
    const std::string& oldStr,
    const std::string& newStr)
{
    std::string::size_type pos = 0u;
    while ((pos = str.find(oldStr, pos)) != std::string::npos) {
        str.replace(pos, oldStr.length(), newStr);
        pos += newStr.length();
    }
}

static inline void trimPadding(std::string& s) {
    s.erase(std::find_if(s.rbegin(), s.rend(), [](unsigned char ch) {
        return ch != '=';
        }).base(), s.end());
}

We used third party libraries in the code above. You wil need to download and install them to your Windows app. Using the vcpkg package manager is the best way to download and install them.

Download and install vcpkg

git clone https://github.com/Microsoft/vcpkg.git
cd vcpkg
./bootstrap-vcpkg.sh
./vcpkg integrate install

Install third party libraries with vcpkg

./vcpkg install libsodium
./vcpkg install openssl

When using CodeChallengeHelper, call createCodeVerifierto create a verifier before accessing the verifier field or the return value ofgetCodeChallenge`.

Send a request to authentication server

After you've created the challenge, include it as a query parameter on the request to the authorization server.

string url_encode(const string& value) {
    ostringstream escaped;
    escaped.fill('0');
    escaped << hex;

    for (string::const_iterator i = value.begin(), n = value.end(); i != n; ++i) {
        string::value_type c = (*i);

        // Keep alphanumeric and other accepted characters intact
        if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') {
            escaped << c;
            continue;
        }

        // Any other characters are percent-encoded
        escaped << uppercase;
        escaped << '%' << setw(2) << int((unsigned char)c);
        escaped << nouppercase;
    }

    return escaped.str();
}

string getAuthURL()
{
    std::string url = "https://zoom.us/oauth/authorize";
    url += "?response_type=code";
    url += "&client_id=" + CLIENT_ID or public client ID;
    url += "&code_challenge=" + CodeChallengeHelper::getCodeChallenge();
    url += "&redirect_uri=" + url_encode(REDIRECT_URI);
    url += "&code_challenge_method=" + string("S256");
    return url;
}

Launch the URL in a browser to allow the user to authenticate

Finally, create a Windows process to allow the user to authenticate through the default browser.

#include <shellapi.h>

void launchBrowserForAuth(){
    std::string oAuthUrl = getAuthURL();
    ShellExecuteW(NULL, L"open", std::wstring(oAuthUrl.begin(), oAuthUrl.end()).c_str(), NULL, NULL, SW_SHOW);
}

Handle the response

After your app executes the request in the previous step, the user will see the Zoom login in a browser. Once they've logged in, the Windows Desktop Application you set up earlier in the Setup URI Scheme step will be started. From here, request an access token and include the code verifier to prove that you were the one who initiated the authorization.

#include "cpr/cpr.h"
#include "json/json.h"

string requestAccessToken(string code)
{
  cpr::Response r = cpr::Post(
      cpr::Url{ "https://zoom.us/oauth/token" },
      cpr::Authentication{ CLIENT_ID, CLIENT_SECRET, cpr::AuthMode::BASIC },
      cpr::Payload{
          {"grant_type", "authorization_code"},
          {"code", code},
          {"redirect_uri", REDIRECT_URI},
          {"code_verifier", CodeChallengeHelper::verifier}
      });

    if (r.status_code == 200) {
        Json::Value root;
        Json::Reader reader;
        if (reader.parse(r.text, root))
            return root.get("access_token", "").asString();
    }
    return string();
}

Two third party libraries are used in the code. Install them with vcpkg.

./vcpkg install cpr
./vcpkg install jsoncpp

To get the code used as the code query param, parse it from the URL used to redirect to your app. These snippets rely on credentials obtained from the Zoom Marketplace.

string parseCode(std::string dataUri)
  {
      size_t queryStart = dataUri.find('?');
      if (queryStart == std::string::npos)
          return string();

      string queryString = dataUri.substr(queryStart + 1);
      // Parse all query params
      std::map<string, string> params;
      std::istringstream stream(queryString);
      string pair;
      while (std::getline(stream, pair, '&'))
      {
          size_t eq = pair.find('=');
          if (eq != string::npos)
              params[pair.substr(0, eq)] = pair.substr(eq + 1);
      }

      if (params.count("error"))
      {
          handleAuthError(params["error"]);
          return string();
      }

      if (params.count("code"))
          return params["code"];

      return string();
  }

...

int main(int argc, char** argv)
  {
      if (argc > 1)
      {
          std::string code = parseCode(argv[1]);
      }
      // ...
  }

Get a ZAK from the REST API

If everything went well up until this point, you should now have an access token which - if your app is scoped correctly - gives you access to the user's ZAK. A ZAK can be used to start or join a meeting as that user, so it should be treated as secure credentials.

Build and execute the request to get the ZAK from the token endpoint.

string getZak(string access_token)
{
    string url = "https://api.zoom.us/v2/users/me/token";
    url += "?type=zak";

    cpr::Response r = cpr::Get(cpr::Url{ url },
        cpr::Bearer{ access_token });

    if (r.status_code == 200) {
        Json::Value root;
        Json::Reader reader;
        if (reader.parse(r.text, root))
            return root.get("token", "").asString();
    }
    return string();
}

Start a meeting with a ZAK

Now the only thing left to do is to use the ZAK to start a meeting. The code below uses the Meeting SDK to start a meeting, which also starts a window that contains the default meeting UI.

void startMeeting() {
     ZOOM_SDK_NAMESPACE::StartParam startMeetingParam;
     ZOOM_SDK_NAMESPACE::StartParam4WithoutLogin startMeetingWithoutLoginParam;
     startMeetingParam.userType = ZOOM_SDK_NAMESPACE::SDK_UT_WITHOUT_LOGIN;
     startMeetingWithoutLoginParam.zoomuserType = ZOOM_SDK_NAMESPACE::ZoomUserType_EMAIL_LOGIN;
     startMeetingWithoutLoginParam.meetingNumber = 0; // Use 0 to create instant meeting.
     startMeetingWithoutLoginParam.userZAK = zak;
     startMeetingWithoutLoginParam.userID = L"User ID or email for user";
     startMeetingWithoutLoginParam.userName = L"Display name for user";
     startMeetingParam.param.withoutloginStart = startMeetingWithoutLoginParam;
     ZOOM_SDK_NAMESPACE::SDKError startMeetingCallReturnValue(ZOOM_SDK_NAMESPACE::SDKERR_UNKNOWN);
     startMeetingCallReturnValue = yourMeetingServiceInstance->Start(startMeetingParam);
     if (startMeetingCallReturnValue == ZOOM_SDK_NAMESPACE::SDKError::SDKERR_SUCCESS)
     {
         // Start meeting call succeeded, listen for start meeting result using the onMeetingStatusChanged callback
     }
 }