# PKCE OAuth and the Plugin SDK for Windows 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 Plugin SDK for Windows. - Zoom Plugin SDK and [OAuth credentials](/docs/integrations/oauth/) for a Marketplace app with the scopes your plugin needs. > **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 Plugin SDK files, and integrated the SDK into your app, set up a custom URL scheme. Then authenticate the user with PKCE and use the returned access token to call the Zoom REST APIs your plugin's scopes authorize. ### 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. 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, define methods `createCodeVerifier` to generate a new verifier, and `getCodeChallenge` to create a code challenge using the verifier. ```cpp class CodeChallengeHelper { public: static std::string verifier; static void createCodeVerifier(); static std::string getCodeChallenge(); }; ``` ```csharp using System.Security.Cryptography; using System.Text; namespace WinFormsApp1 { internal class CodeChallengeHelper { static public String verifier { get; private set; } } } ``` Within that same class, define methods `createCodeVerifier` to generate a new verifier, and `getCodeChallenge` to create a code challenge using the verifier. ```csharp 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`. ### Implement the two methods ```cpp void CodeChallengeHelper::createCodeVerifier() { char byteArray[32]; randombytes_buf(byteArray, 32); BYTE* Bytes = reinterpret_cast(const_cast(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. ```cpp #include #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 will 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. ```shell git clone https://github.com/Microsoft/vcpkg.git cd vcpkg ./bootstrap-vcpkg.sh ./vcpkg integrate install ``` Install third party libraries with vcpkg. ```shell ./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 a request to authentication server After you've created the challenge, include it as a query parameter on the request to the authorization server. ```cpp 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; } ``` ```csharp 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); // public client ID from your Marketplace app 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. ```cpp #include void launchBrowserForAuth(){ std::string oAuthUrl = getAuthURL(); ShellExecuteW(NULL, L"open", std::wstring(oAuthUrl.begin(), oAuthUrl.end()).c_str(), NULL, NULL, SW_SHOW); } ``` ```csharp 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 and consented to your app's scopes, the Windows Desktop Application you set up earlier in the Setup URI Scheme step will be started. From here, you will need to exchange the authorization code for an access token and include the code verifier to prove that you were the one who initiated the authorization. ```cpp #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. ```shell ./vcpkg install cpr ./vcpkg install jsoncpp ``` ```csharp 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 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("grant_type", "authorization_code"), new KeyValuePair("code", code), new KeyValuePair("redirect_uri", REDIRECT_URI), new KeyValuePair("code_verifier", CodeChallengeHelper.verifier) }); var response = await client.SendAsync(requestMessage); var accessTokenResponse = await response.Content.ReadFromJsonAsync(); return accessTokenResponse?.access_token ?? ""; } ``` 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. ```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 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(); } ``` ```cpp int main(int argc, char** argv) { if (argc > 1) { std::string code = parseCode(argv[1]); } // ... } ``` ```csharp private async Task 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]); } } ``` The returned `access_token` is a user-level OAuth 2.0 Bearer token. Send it on Zoom REST API requests as `Authorization: Bearer `. It will only be able to call endpoints covered by the scopes granted to your Marketplace app.