PKCE OAuth and the Meeting SDK for macOS
As of April 17 2026, the Meeting SDK supports using PKCE with a public client ID.
Beginning March 2 2026, apps joining meetings outside their account must be authorized. Authorize apps by using either ZAK or OBF tokens, or RTMS. Learn more.
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
- Visual Studio Code or IDE of your choice.
- Zoom Meeting SDK 5.9.0 or newer. Version 7.0.2 is preferred.
- Zoom Meeting SDK & OAuth credentials.
NOTE Throughout this guide, credentials are hardcoded for convenience. For security reasons, do not store hardcoded credentials of any type in your production application.
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 to set up a custom URL scheme for Linux.
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.
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.
class CodeChallengeHelper
{
public:
static std::string verifier;
static void createCodeVerifier();
static std::string getCodeChallenge();
};
Implement the two methods
#include <string>
#include <vector>
#include <openssl/rand.h>
#include <openssl/sha.h>
#include <openssl/evp.h>
class CodeChallengeHelper
{
public:
static std::string verifier;
static void createCodeVerifier();
static std::string getCodeChallenge();
private:
static std::string base64UrlEncode(const unsigned char* data, size_t len);
};
std::string CodeChallengeHelper::verifier;
static void replaceAll(std::string& s, char from, char to)
{
for (char& c : s) {
if (c == from) c = to;
}
}
std::string CodeChallengeHelper::base64UrlEncode(const unsigned char* data, size_t len)
{
std::vector<unsigned char> out(4 * ((len + 2) / 3) + 1);
int outLen = EVP_EncodeBlock(out.data(), data, static_cast<int>(len));
std::string s(reinterpret_cast<char*>(out.data()), outLen);
replaceAll(s, '+', '-');
replaceAll(s, '/', '_');
while (!s.empty() && s.back() == '=') {
s.pop_back();
}
return s;
}
void CodeChallengeHelper::createCodeVerifier()
{
unsigned char bytes[32];
if (RAND_bytes(bytes, sizeof(bytes)) != 1) {
verifier.clear();
return;
}
verifier = base64UrlEncode(bytes, sizeof(bytes));
}
std::string CodeChallengeHelper::getCodeChallenge()
{
unsigned char hash[SHA256_DIGEST_LENGTH];
SHA256(reinterpret_cast<const unsigned char*>(verifier.data()),
verifier.size(),
hash);
return base64UrlEncode(hash, sizeof(hash));
}
In this method's implementation, 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 <string>
#include <algorithm>
#include <sodium.h>
#include <openssl/sha.h>
#include <openssl/evp.h>
#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 will need to download and install them to your Linux project. On an Ubuntu 22.04 machine, install the proper packages with this code. Use the equivalent commands on your Linux Distro.
sudo apt update
sudo apt install libsodium-dev libssl-dev
When using the CodeChallengeHelper, call createCodeVerifier to create a verifier before accessing the verifier field or the return value of getCodeChallenge.
Send 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(static_cast<unsigned char>(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 Linux process to allow the user to authenticate through the default browser. For headless Linux environments, do not attempt to open the default browser locally. Instead, generate the authorization URL and present it to the user so they can open it in a browser on another machine.
#include <iostream> #include <string>
void launchBrowserForAuth()
{
std::string oAuthUrl = getAuthURL();
std::cout << "Open this URL in a browser:\n" << oAuthUrl << std::endl;
}
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, your configured redirect endpoint or callback flow will receive the authorization code. From here, you will need to request an access token and include the code verifier to prove that you were the one who initiated the authorization.
#include <string>
#include <sstream>
#include <curl/curl.h>
#include <json/json.h>
using namespace std;
static size_t WriteCallback(void* contents, size_t size, size_t nmemb, void* userp)
{
size_t totalSize = size * nmemb;
static_cast<string*>(userp)->append(static_cast<char*>(contents), totalSize);
return totalSize;
}
string requestAccessToken(const string& code)
{
CURL* curl = curl_easy_init();
if (!curl) {
return string();
}
string responseBody;
string accessToken;
char* encodedCode = curl_easy_escape(curl, code.c_str(), 0);
char* encodedRedirectUri = curl_easy_escape(curl, REDIRECT_URI.c_str(), 0);
char* encodedVerifier = curl_easy_escape(curl, CodeChallengeHelper::verifier.c_str(), 0);
if (!encodedCode || !encodedRedirectUri || !encodedVerifier) {
if (encodedCode) curl_free(encodedCode);
if (encodedRedirectUri) curl_free(encodedRedirectUri);
if (encodedVerifier) curl_free(encodedVerifier);
curl_easy_cleanup(curl);
return string();
}
string postFields =
"grant_type=authorization_code"
"&code=" + string(encodedCode) +
"&redirect_uri=" + string(encodedRedirectUri) +
"&code_verifier=" + string(encodedVerifier);
curl_free(encodedCode);
curl_free(encodedRedirectUri);
curl_free(encodedVerifier);
curl_easy_setopt(curl, CURLOPT_URL, "https://zoom.us/oauth/token");
curl_easy_setopt(curl, CURLOPT_POST, 1L);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postFields.c_str());
curl_easy_setopt(curl, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_easy_setopt(curl, CURLOPT_USERNAME, CLIENT_ID.c_str());
curl_easy_setopt(curl, CURLOPT_PASSWORD, CLIENT_SECRET.c_str());
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &responseBody);
struct curl_slist* headers = nullptr;
headers = curl_slist_append(headers, "Content-Type: application/x-www-form-urlencoded");
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
CURLcode res = curl_easy_perform(curl);
long statusCode = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &statusCode);
if (res == CURLE_OK && statusCode == 200) {
Json::Value root;
Json::CharReaderBuilder builder;
string errors;
istringstream stream(responseBody);
if (Json::parseFromStream(builder, stream, &root, &errors)) {
accessToken = root.get("access_token", "").asString();
}
}
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
return accessToken;
}
Two third party libraries are used in the code. Install them with `apt-get` or equivalent.
sudo apt install libcurl4-openssl-dev libjsoncpp-dev
To get the code used as the code query parameter, parse it from the URL used to redirect to your app. These snippets rely on credentials obtained from the Zoom Marketplace.
```cpp
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
You should now have an access token which - if your app is scoped correctly - gives you access to the user's ZAK token. A ZAK token can be used to start or join a meeting as that user, so treat it as secure credentials.
Build and execute the request to get the ZAK token from the token endpoint.
#include <string>
#include <sstream>
#include <curl/curl.h>
#include <json/json.h>
using namespace std;
static size_t WriteCallback(void* contents, size_t size, size_t nmemb, void* userp)
{
size_t totalSize = size * nmemb;
static_cast<string*>(userp)->append(static_cast<char*>(contents), totalSize);
return totalSize;
}
string getZak(const string& access_token)
{
CURL* curl = curl_easy_init();
if (!curl) {
return string();
}
string responseBody;
string zak;
curl_easy_setopt(curl, CURLOPT_URL, "https://api.zoom.us/v2/users/me/token?type=zak");
curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &responseBody);
struct curl_slist* headers = nullptr;
string authHeader = "Authorization: Bearer " + access_token;
headers = curl_slist_append(headers, authHeader.c_str());
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
CURLcode res = curl_easy_perform(curl);
long statusCode = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &statusCode);
if (res == CURLE_OK && statusCode == 200) {
Json::Value root;
Json::CharReaderBuilder builder;
string errors;
istringstream stream(responseBody);
if (Json::parseFromStream(builder, stream, &root, &errors)) {
zak = root.get("token", "").asString();
}
}
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
return zak;
}
sudo apt install libcurl4-openssl-dev libjsoncpp-dev
Start a meeting with a ZAK
Now the only thing left to do is to use the ZAK to start a meeting. This code starts a meeting using the default UI in a new window.
#include <cstring>
#include <string>
#include "zoom_sdk.h"
#include "meeting_service_interface.h"
extern ZOOMSDK::IMeetingService* yourMeetingServiceInstance;
extern std::string zak;
void startMeeting()
{
ZOOMSDK::StartParam startMeetingParam;
ZOOMSDK::StartParam4WithoutLogin startMeetingWithoutLoginParam;
std::memset(&startMeetingWithoutLoginParam, 0, sizeof(startMeetingWithoutLoginParam));
startMeetingParam.userType = ZOOMSDK::SDK_UT_WITHOUT_LOGIN;
startMeetingWithoutLoginParam.zoomuserType = ZOOMSDK::ZoomUserType_EMAIL_LOGIN;
startMeetingWithoutLoginParam.meetingNumber = 0; // Use 0 to create instant meeting.
startMeetingWithoutLoginParam.userZAK = zak.c_str();
startMeetingWithoutLoginParam.userName = "Display name for user";
startMeetingWithoutLoginParam.isVideoOff = false;
startMeetingWithoutLoginParam.isAudioOff = false;
startMeetingParam.param.withoutloginStart = startMeetingWithoutLoginParam;
ZOOMSDK::SDKError startMeetingCallReturnValue =
yourMeetingServiceInstance->Start(startMeetingParam);
if (startMeetingCallReturnValue == ZOOMSDK::SDKERR_SUCCESS)
{
// Listen for the start result via onMeetingStatusChanged.
}
}