Top of pageSkip to main content

PKCE OAuth and the Meeting SDK for iOS

On this page


Authorization Code with Proof Key for Code Exchange (PKCE) is an OAuth flow newly supported by Zoom. It is similar to the standard Authorization Code flow, except it does not require that you have a backend server to get the authorization token. Instead, you’ll get the token directly from Zoom’s OAuth server. The end result and user experience from this flow will be nearly identical to the Authorization Code flow. It may even be a bit faster.

This guide shows how to use PKCE in a new iOS app. You’ll learn how to:

  • Create the project
  • Initialize the Zoom Meeting SDK
  • Authenticate users using PKCE
  • Get a ZAK from the REST API
  • Start a meeting via ZAK


Before we can get started, you’ll need the following:

  • Understanding of Swift
  • Xcode
  • Zoom Meeting SDK 5.9.0 or newer
  • Zoom Meeting SDK credentials
  • An install URL from your SDK app (this is a URL where users are directed for OAuth)

Note that throughout this guide, credentials are being hard-coded for convenience. We strongly discourage storing hard-coded credentials of any type in your production application for security reasons.

Create the project

Make sure you create an iOS project and select Swift as the language. We aren’t creating any UI in this guide, so you can select whatever interface best suits your needs.

Add the Meeting SDK

Follow the steps to Import the Zoom Meeting SDK.

Initialize the Zoom Meeting SDK

Follow the steps to initialize the SDK

Authenticate users using PKCE

Now that your project is set up, let’s walk through the steps of implementing the PKCE OAuth flow.

Generate the code verifier and challenge

First, generate a code verifier using the code below, and then hash the verifier to create a code challenge. We’ll host these in a dedicated CodeChallengeHelper class. First, we’ll define the class with a verifier var, since the same verifier must be used for the auth session and requesting the access token.

import Foundation
import CommonCrypto

class CodeChallengeHelper {
    var verifier: String? = nil

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.

func createCodeVerifier() {
    var buffer = [UInt8](repeating: 0, count: 32)
    _ = SecRandomCopyBytes(kSecRandomDefault, buffer.count, &buffer)
    verifier = Data(buffer).base64EncodedString()
        .replacingOccurrences(of: "+", with: "-")
        .replacingOccurrences(of: "/", with: "_")
        .replacingOccurrences(of: "=", with: "")
        .trimmingCharacters(in: .whitespaces)
func getCodeChallenge() -> String {
    guard let data = .ascii) else { return "" }
    var buffer = [UInt8](repeating: 0,  count: Int(CC_SHA256_DIGEST_LENGTH))
    data.withUnsafeBytes {
        _ = CC_SHA256($0, CC_LONG(data.count), &buffer)
    let hash = Data(buffer)
    let challenge = hash.base64EncodedString()
        .replacingOccurrences(of: "+", with: "-")
        .replacingOccurrences(of: "/", with: "_")
        .replacingOccurrences(of: "=", with: "")
        .trimmingCharacters(in: .whitespaces)
    return challenge

Start authentication session

After creating the challenge, we’ll use it to create the authorization URL and pass that URL into an ASWebAuthenticationSession which will help manage the authentication. For the sake of this guide, we won’t go into detail about what this service does. Apple’s documentation provides more information for those who are interested.

First, we’ll setup the ViewController in your project to conform to ASWebAuthenticationPresentationContextProviding and create a couple of constants for later use.

import UIKit
import AuthenticationServices
import MobileRTC

class ViewController: UIViewController, ASWebAuthenticationPresentationContextProviding {
    private let codeChallengeHelper = CodeChallengeHelper()
    private let delegate = UIApplication.shared.delegate as! AppDelegate

Next we’ll need somewhere to kick off the actual PKCE logic. Since we’re using ASWebAuthenticationSession, you must call the code below after the viewDidAppear callback. Otherwise your app will not be able to start the session.

Make note of the TODO lines, as they require you to input your client ID and redirect URI from your Marketplace OAuth app. As a reminder, it is not safe to hard-code credentials in this manner in a production environment.

guard var oauthUrlComp = URLComponents(string: "") else { return }

let codeChallenge = URLQueryItem(name: "code_challenge", value: codeChallengeHelper.getCodeChallenge())
let codeChallengeMethod = URLQueryItem(name: "code_challenge_method", value: "S256")
let responseType = URLQueryItem(name: "response_type", value: "code")
let clientId = URLQueryItem(name: "client_id", value: "") // TODO: Enter your OAuth client ID.
let redirectUri = URLQueryItem(name: "redirect_uri", value: "") // TODO: Enter the redirect URI of your OAuth app.
oauthUrlComp.queryItems = [responseType, clientId, redirectUri, codeChallenge, codeChallengeMethod]
let scheme = "" // TODO: Enter the custom scheme of the redirect URI of your OAuth app.

guard let oauthUrl = oauthUrlComp.url else { return }

let session = ASWebAuthenticationSession(url: oauthUrl, callbackURLScheme: scheme) { callbackUrl, error in
    self.handleAuthResult(callbackUrl: callbackUrl, error: error)

session.presentationContextProvider = self

Since you need to provide an instance of ASWebAuthenticationPresentationContextProviding, you will also need to implement presentationAnchor in your ViewController.

func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
    return view.window!

Handle the response

After the OAuth flow redirects to your app via custom URL scheme, the completion handler you passed into the ASWebAuthenticationSession’s constructor will be invoked. From here, you’ll need to verify that authentication was successful before requesting an access token.

private func handleAuthResult(callbackUrl: URL?, error: Error?) {
    guard let callbackUrl = callbackUrl else { return }
    if (error == nil) {
        guard let url = URLComponents(string: callbackUrl.absoluteString) else { return }
        guard let code = url.queryItems?.first(where: { $ == "code" })?.value else { return }
        self.delegate.requestAccessToken(code: code, codeChallengeHelper: self.codeChallengeHelper)

In your AppDelegate, we’ll handle the remainder of the OAuth flow starting with the requestAccessToken method invoked in the previous snippet:

Note that you must include your client key/secret and redirect URI. As a reminder, we do not recommend including hard-coded credentials in a production application.

func requestAccessToken(code: String, codeChallengeHelper: CodeChallengeHelper) {
    guard let url = self.buildAccessTokenUrl(code: code, verifier: codeChallengeHelper.verifier) else { return }
    let clientKey = "" // TODO: Enter the client key from your OAuth app.
    let clientSecret = "" // TODO: Enter the client secret from your OAuth app.
    guard let encoded = "\(clientKey):\(clientSecret)".data(using: .utf8)?.base64EncodedString() else { return }
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.addValue("Basic \(encoded)", forHTTPHeaderField: "Authorization")
    request.addValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
    request.httpBody = nil
    let task = URLSession.shared.dataTask(with: request) { data, response, error in
        guard self.checkRequestResult(data: data, response: response, error: error) else { return }
        let response = try! JSONDecoder().decode(AccessTokenResponse.self, from: data!)
        self.requestZak(accessToken: response.access_token)
private func buildAccessTokenUrl(code: String, verifier: String?) -> URL? {
    var urlComp = URLComponents()
    urlComp.scheme = "https" = ""
    urlComp.path = "/oauth/token"
    let grantType = URLQueryItem(name: "grant_type", value: "authorization_code")
    let code = URLQueryItem(name: "code", value: code)
    let redirectUri = URLQueryItem(name: "redirect_uri", value: "") // TODO: Input your redirect URI here
    let codeVerifier = URLQueryItem(name: "code_verifier", value: verifier)
    let params = [grantType, code, redirectUri, codeVerifier]
    urlComp.queryItems = params
    return urlComp.url

private struct AccessTokenResponse : Decodable {
    public let access_token: String

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, will give 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.

In both the ZAK request and the access token request, we’ll utilize the checkRequestResult function below to avoid repeating the same code.

private func checkRequestResult(data: Data?, response: URLResponse?, error: Error?) -> Bool {
    if error != nil { return false }
    guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else { return false }
    guard data != nil else { return false }
    return true

First we’ll need to build and execute the request to get the ZAK from the token endpoint.

private func requestZak(accessToken: String) {
    var urlComp = URLComponents()
    urlComp.scheme = "https" = ""
    urlComp.path = "/v2/users/me/token"
    let tokenType = URLQueryItem(name: "type", value: "zak")
    urlComp.queryItems = [tokenType]
    guard let url = urlComp.url else { return }
    var request = URLRequest(url: url)
    request.addValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
    let task = URLSession.shared.dataTask(with: request) { data, response, error in
        guard self.checkRequestResult(data: data, response: response, error: error) else { return }
        let response = try! JSONDecoder().decode(ZakResponse.self, from: data!)
        DispatchQueue.main.async {
            self.startMeeting(zak: response.token)

private struct ZakResponse : Decodable {
    public let token: String

Upon a successful result, the next step is to parse the ZAK from the response.

Start a meeting with 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 presents a ViewController that contains the default meeting UI. Keep in mind that the Meeting SDK must be called from the main thread, so you must dispatch to the main thread from your request’s completion handler.

private func startMeeting(zak: String) {
    let startParams = MobileRTCMeetingStartParam4WithoutLoginUser()
    startParams.zak = zak
    startParams.meetingNumber = "" // TODO: Add your meeting number
    startParams.userID = "" // TODO: Add your display name
    let meetingService = MobileRTC.shared().getMeetingService()
    meetingService?.delegate = self
    let meetingResult = meetingService?.startMeeting(with: startParams)
    if (meetingResult == .success) {
        // The SDK will attempt to join the meeting, see onMeetingStateChange callback.

In order to get the meeting status callbacks, your AppDelegate should conform to the MobileRTCMeetingServiceDelegate protocol.

class AppDelegate: UIResponder, UIApplicationDelegate, MobileRTCAuthDelegate, MobileRTCMeetingServiceDelegate {
    func onMeetingStateChange(_ state: MobileRTCMeetingState) {
        if (state == .inMeeting) {
            // You have successfully joined the meeting.

Need help?

If you're looking for help, try Developer Support or our Developer Forum. Priority support is also available with Premier Developer Support plans.