PKCE OAuth and the Meeting SDK for Android
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 supported by Zoom. It's 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 Kotlin or Java. Kotlin is preferred.
- Android Studio.
- Android 8.0 or later.
- Zoom Meeting SDK version 5.9.0 or newer. Version 7.0.2 is preferred.
- Zoom Meeting SDK and OAuth credentials.
- An authorization URL from your SDK app. This is a URL where users are directed for OAuth.
NOTE Throughout this guide, credentials are hardcoded for convenience. For security reasons, do not store hardcoded credentials of any type in your production application.
Create the project
- For the project template, we're going to start with an Empty Activity.
- Next, name your project and verify that the Minimum SDK field is set to Android 8.0 or newer.
- Select Kotlin or Java as the project language. Kotlin is recommended, as this guide utilizes coroutines.
Add the required dependencies
Follow the steps to Import the Zoom SDK libraries
In addition to those steps, add these lines to your app's build.gradle.
- [Serialization] (Kotlin only) -
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2") - [OKHttp] -
implementation("com.squareup.okhttp3:okhttp:4.9.0") - [Coroutines] (Kotlin only) -
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2' - [ZoomSDK] -
implementation project(':mobilertc')
Initialize the Meeting SDK
Set up a URL scheme
To register the custom URL scheme to trigger your Android application, simply add the following intent-filter to the activity that you would like to invoke in the AndroidManifest.xml:
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="com.example.app"
/>
</intent-filter>
The intent filter allows you to trigger your app with the URL com.example.app//.
See IETF rfc8252: OAuth 2.0 for Native Apps and Create Deep Links to App Content for details.
Authenticate users using PKCE
Now that your app is configured to handle your custom URL scheme, you can start implementing the PKCE OAuth flow. Since we only want to start OAuth when we know we can use the SDK, it is a good idea to wait for a successful onZoomSDKInitializeResult callback.
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 var inside the companion object 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.
import android.util.Base64
import java.security.MessageDigest
import java.security.SecureRandom
class CodeChallengeHelper {
companion object {
var verifier: String? = null
}
}
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.
companion object {
….
fun createCodeVerifier() {
val secureRandom = SecureRandom()
val code = ByteArray(32)
secureRandom.nextBytes(code)
verifier = Base64.encodeToString(code, Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING)
}
fun getCodeChallenge(verifier: String): String {
val bytes: ByteArray = verifier.toByteArray(Charsets.US_ASCII)
val md: MessageDigest = MessageDigest.getInstance("SHA-256")
md.update(bytes, 0, bytes.size)
val digest: ByteArray = md.digest()
return Base64.encodeToString(digest, Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING)
}
}
When using the CodeChallengeHelper, you need to 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.
val uri = Uri.parse("https://zoom.us/oauth/authorize")
.buildUpon()
.appendQueryParameter("response_type", "code")
.appendQueryParameter("client_id", CLIENT_ID)
.appendQueryParameter("redirect_uri", URL_SCHEME)
.appendQueryParameter("code_challenge", CodeChallengeHelper.getCodeChallenge(CodeChallengeHelper.verifier!!))
.appendQueryParameter("code_challenge_method", "S256")
.build()
Create an intent to allow the user to authenticate
Finally, create and start an intent to allow the user to authenticate through the browser.
val intent = Intent(Intent.ACTION_VIEW, uri)
startActivity(intent)
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 Activity you set up earlier in the Setup URL Scheme step will be started. From here, you will need to request an access token and include the code verifier - client_secret for confidential apps, or code_verifier for public apps - to prove that you were the one who initiated the authorization.
In this example, we're using OkHttp for the request and Coroutines for threading, but feel free to swap them out according to your preferences.
private fun requestAccessToken(code: String, client: OkHttpClient) {
val encoded = Base64.encodeToString(
"$CLIENT_ID:$CLIENT_SECRET".toByteArray(), Base64.NO_WRAP
)
val formBody = FormBody.Builder()
.add("grant_type", "authorization_code")
.add("code", code)
.add("redirect_uri", URL_SCHEME)
.add("code_verifier", CodeChallengeHelper.verifier ?: "")
.build()
val request = Request.Builder()
.addHeader("Authorization", "Basic $encoded")
.url("https://zoom.us/oauth/token")
.post(formBody)
.build()
val response = client.newCall(request).execute()
val body = response.body?.string()
accessToken = Json.decodeFromString<AccessToken>(body.orEmpty()).access_token
}
@Serializable
data class AccessToken(val access_token: String, val token_type: String, val refresh_token: String, val expires_in: Long, val scope: String)
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.
private fun parseCode(dataUri: String): String? {
val uri = Uri.parse(dataUri)
val error = uri?.getQueryParameter("error")
if (error != null) {
handleAuthError(error)
return null
}
return uri?.getQueryParameter(PARAM_CODE)?.also {
getAccessToken(it)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_o_auth)
intent.dataString?.let {
parseCode(it)
}
}
Get a ZAK from the REST API
If everything went well up until this point, you'll 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.
First, build and execute the request to get the ZAK from the token endpoint.
private fun buildZakUrl() = HttpUrl.Builder()
.scheme("https")
.host("api.zoom.us")
.addPathSegment("v2")
.addPathSegment("users")
.addPathSegment("me")
.addPathSegment("token")
.addQueryParameter("type", "zak")
.build()
private fun getZak(client: OkHttpClient) {
val url = buildZakUrl()
val request = Request.Builder()
.addHeader("authorization", "Bearer $accessToken")
.url(url)
.get()
.build()
val response = client.newCall(request).execute()
val json = JSONObject(response.body!!.string())
zak = json.getString("token")
}
Upon a successful result, the next step is to parse the ZAK from the response.
Both the getZak and requestAccessToken should be executed sequentially from the same coroutine, sharing an OkHttpClient instance as such:
private fun getAccessToken(code: String) {
CoroutineScope(Dispatchers.IO).launch {
val client = OkHttpClient()
runCatching {
requestAccessToken(code, client)
getZak(client)
}
}
}
Start a meeting with a ZAK
Now, use the ZAK to start a meeting. This code uses the Meeting SDK to start a meeting, which also starts an activity that contains the default meeting UI. The Meeting SDK must be called from the main thread, so you can't use it in the same job as the network requests in the previous steps.
private fun startMeeting(zak: String) {
val meetingService = ZoomSDK.getInstance().meetingService
val startParams = StartMeetingParamsWithoutLogin().apply {
zoomAccessToken = zak
meetingNo = ""
}
meetingService.addListener(meetingServiceListener)
val result = meetingService.startMeetingWithParams(this, startParams, StartMeetingOptions())
if (result == MeetingError.MEETING_ERROR_SUCCESS) {
// The SDK will attempt to join the meeting.
}
}
To ensure it's being called in the main thread, here is the full coroutine order from the getAccessToken method previously defined.
CoroutineScope(Dispatchers.IO).launch {
runCatching {
val client = OkHttpClient()
requestAccessToken(code, client)
getZak(client)
}
launch(Dispatchers.Main) {
startMeeting(zak)
}
}