# PKCE OAuth and the Meeting SDK for iOS > As of **April 17 2026**, the Meeting SDK supports [using PKCE with a public client ID](/docs/integrations/oauth/#using-proof-key-for-code-exchange-pkce). > **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](/docs/meeting-sdk/obf-faq/). 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. ## Prerequisites - Understanding of Swift. - Xcode version 26 or later. - A physical 64-bit iOS device, either iPhone or iPad, with iOS version 15.0 or above. For iOS 26 and Xcode 26 or later, you may need to remove the `-ld_classic` in other link flags from your project. - Zoom Meeting SDK version 5.9.0 or newer. Version 7.0.2 is preferred. - Zoom Meeting SDK 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, integrated the SDK into your app, and gotten the authentication URL from your SDK app, you can authenticate the users with PKCE, get the ZAK token, and start a meeting with that ZAK token. ## 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. ```swift 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. ```swift 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) } ``` ```swift func getCodeChallenge() -> String { guard let verifier = verifier, let data = verifier.data(using: .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 the 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](https://developer.apple.com/documentation/authenticationservices/aswebauthenticationsession) provides more information for those who are interested. First, we'll set up the **`ViewController`** in your project to conform to **`ASWebAuthenticationPresentationContextProviding`** and create a couple of constants for later use. ```swift 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`**, call this next code sample 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, **do not hardcode credentials in a production environment**. ```swift codeChallengeHelper.createCodeVerifier() guard var oauthUrlComp = URLComponents(string: "https://zoom.us/oauth/authorize") 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 session.start() ``` Since you need to provide an instance of `ASWebAuthenticationPresentationContextProviding`, you will also need to implement `presentationAnchor` in your `ViewController`. ```swift 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, verify that authentication was successful before requesting an access token. ```swift 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 } if let errorParam = url.queryItems?.first(where: { $0.name == "error" })?.value { handleAuthError(errorParam) return } guard let code = url.queryItems?.first(where: { $0.name == "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 and secret and redirect URI. As a reminder, **do not hardcode credentials in a production application**. ```swift private func buildAccessTokenBody(code: String, verifier: String?) -> Data? { var urlComp = URLComponents() urlComp.queryItems = [ URLQueryItem(name: "grant_type", value: "authorization_code"), URLQueryItem(name: "code", value: code), URLQueryItem(name: "redirect_uri", value: ""), // TODO: Enter your redirect URI URLQueryItem(name: "code_verifier", value: verifier) ] return urlComp.query?.data(using: .utf8) } func requestAccessToken(code: String, codeChallengeHelper: CodeChallengeHelper) { guard let url = URL(string: "https://zoom.us/oauth/token") else { return } let clientKey = "" // TODO let clientSecret = "" // TODO 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 = buildAccessTokenBody(code: code, verifier: codeChallengeHelper.verifier) 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) } task.resume() } 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 - 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. In both the ZAK request and the access token request, utilize the `checkRequestResult` function to avoid repeating the same code. ```swift 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, build and execute the request to get the ZAK from the [token endpoint](/docs/api/users/#tag/users/GET/users/me/zak). ```swift private func requestZak(accessToken: String) { var urlComp = URLComponents() urlComp.scheme = "https" urlComp.host = "api.zoom.us" 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) } } task.resume() } private struct ZakResponse : Decodable { public let token: String } ``` After receiving a successful result, parse the ZAK from the response. ## Start a meeting with ZAK Now, 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. The Meeting SDK must be called from the main thread, so dispatch to the main thread from your request's completion handler. ```swift private func startMeeting(zak: String) { let startParams = MobileRTCMeetingStartParam4WithoutLoginUser() startParams.zak = zak startParams.meetingNumber = "" // TODO: Add your meeting number startParams.userName = "" // 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. } } ``` To get the meeting status callbacks, your `AppDelegate` should conform to the `MobileRTCMeetingServiceDelegate` protocol. ```swift class AppDelegate: UIResponder, UIApplicationDelegate, MobileRTCAuthDelegate, MobileRTCMeetingServiceDelegate { func onMeetingStateChange(_ state: MobileRTCMeetingState) { if (state == .inMeeting) { // You have successfully joined the meeting. } } } ```