PKCE OAuth and the Meeting SDK for Windows
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
- 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.
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 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.
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();
};
Implementation of 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 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(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 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, 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 "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]);
}
// ...
}
Generate the code verifier and challenge
First, generate a code verifier using the code below. Then, hash the verifier to create a code challenge. 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.
using System.Security.Cryptography;
using System.Text;
namespace WinFormsApp1
{
internal class CodeChallengeHelper
{
static public String verifier { get; private set; }
}
}
Within that same class, define two methods: createCodeVerifier to generate a new verifier; and getCodeChallenge to create a code challenge using the verifier.
static public void createCodeVerifier()
{
var rng = RandomNumberGenerator.Create();
var byteArray = new byte[32];
rng.GetBytes(byteArray);
char[] padding = { '=' };
verifier = Convert.ToBase64String(byteArray).TrimEnd(padding).Replace('+', '-').Replace('/', '_'); ;
}
static public String getCodeChallenge(String verifier)
{
var byteArray = Encoding.ASCII.GetBytes(verifier);
var md = HashAlgorithm.Create("SHA-256");
md?.TransformFinalBlock(byteArray, 0, byteArray.Length);
var digest = md?.Hash;
char[] padding = { '=' };
return Convert.ToBase64String(digest).TrimEnd(padding).Replace('+', '-').Replace('/', '_');
}
When using CodeChallengeHelper, call createCodeVerifier to create a verifier before accessing the verifier field or the return value of getCodeChallenge.
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.
private String getAuthURL()
{
var baseURL = new Uri("https://zoom.us/oauth/authorize");
var queryStrings = HttpUtility.ParseQueryString(baseURL.Query);
queryStrings.Add("response_type", "code");
queryStrings.Add("client_id", CLIENT_ID); //only if using PKCE
queryStrings.Add("code_challenge", CodeChallengeHelper.getCodeChallenge(CodeChallengeHelper.verifier));
queryStrings.Add("redirect_uri", REDIRECT_URI);
queryStrings.Add("code_challenge_method", "S256");
var uriBuilder = new UriBuilder(baseURL);
uriBuilder.Query = queryStrings.ToString();
return uriBuilder.Uri.ToString();
}
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.
var oAuthUrl = getAuthURL();
var ps = new ProcessStartInfo(oAuthUrl)
{
UseShellExecute = true,
Verb = "open"
};
Process.Start(ps);
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 Form Application you set up earlier in the Setup URI Scheme step will be started. 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.
public class AccessTokenResponse
{
public String access_token { get; set; }
public String token_type { get; set; }
public String refresh_token { get; set; }
public long expires_in { get; set; }
public String scope { get; set; }
}
private async Task<String> requestAccessToken(String code)
{
var requestMessage = new HttpRequestMessage(HttpMethod.Post, "https://zoom.us/oauth/token");
var encoded_credential = Convert.ToBase64String(
Encoding.ASCII.GetBytes(CLIENT_ID + ":" + CLIENT_SECRET));
requestMessage.Headers.Authorization =
new AuthenticationHeaderValue("Basic", encoded_credential);
requestMessage.Content = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("grant_type", "authorization_code"),
new KeyValuePair<string, string>("code", code),
new KeyValuePair<string, string>("redirect_uri", REDIRECT_URI),
new KeyValuePair<string, string>("code_verifier", CodeChallengeHelper.verifier)
});
var response = await client.SendAsync(requestMessage);
var accessTokenResponse = await response.Content.ReadFromJsonAsync<AccessTokenResponse>();
return accessTokenResponse?.access_token ?? "";
}
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.
private async Task<String> parseCode(String dataUri)
{
var uri = new Uri(dataUri);
var queryStrings = HttpUtility.ParseQueryString(uri.Query);
var error = queryStrings.Get("error");
if (error != null)
{
handleAuthError(error);
return "";
}
var code = queryStrings.Get("code");
return code ?? "";
}
private async void Form1_Load(object sender, EventArgs e)
{
string[] args = Environment.GetCommandLineArgs();
if (args.Length > 1)
{
var code = await parseCode(args[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();
}
public class ZakResponse
{
public String token { get; set; }
}
private async Task<String> getZak(String access_token)
{
var baseURL = new Uri("https://api.zoom.us/v2/users/me/token");
var queryStrings = HttpUtility.ParseQueryString(baseURL.Query);
queryStrings.Add("type", "zak");
var uriBuilder = new UriBuilder(baseURL);
uriBuilder.Query = queryStrings.ToString();
var url = uriBuilder.Uri.ToString();
var requestMessage = new HttpRequestMessage(HttpMethod.Get, url);
requestMessage.Headers.Authorization =
new AuthenticationHeaderValue("Bearer", access_token);
var response = await client.SendAsync(requestMessage);
var zak = await response.Content.ReadFromJsonAsync<ZakResponse>();
return zak?.token ?? "";
}
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
}
}
private void startMeeting()
{
ZOOM_SDK_DOTNET_WRAP.StartParam param = new ZOOM_SDK_DOTNET_WRAP.StartParam();
param.userType = ZOOM_SDK_DOTNET_WRAP.SDKUserType.SDK_UT_WITHOUT_LOGIN;
ZOOM_SDK_DOTNET_WRAP.StartParam4WithoutLogin start_withoutlogin_param = new ZOOM_SDK_DOTNET_WRAP.StartParam4WithoutLogin();
start_withoutlogin_param.meetingNumber = 0; // Use 0 to create instant meeting.
start_withoutlogin_param.userID = "demouser@example.com"; // User ID or email for the user.
start_withoutlogin_param.userZAK = zak;
start_withoutlogin_param.userName = "A display name";
start_withoutlogin_param.zoomuserType = ZOOM_SDK_DOTNET_WRAP.ZoomUserType.ZoomUserType_EMAIL_LOGIN;
param.withoutloginStart = start_withoutlogin_param;
RegisterCallBack();
ZOOM_SDK_DOTNET_WRAP.SDKError err = ZOOM_SDK_DOTNET_WRAP.CZoomSDKeDotNetWrap.Instance.GetMeetingServiceWrap().Start(param);
if (ZOOM_SDK_DOTNET_WRAP.SDKError.SDKERR_SUCCESS == err)
{
// The SDK will attempt to join the meeting.
}
}
private void RegisterCallBack()
{
ZOOM_SDK_DOTNET_WRAP.CZoomSDKeDotNetWrap.Instance.GetMeetingServiceWrap().Add_CB_onMeetingStatusChanged(onMeetingStatusChanged);
// Register other callback as needed.
}