Authentication and App Links
The SDK opens OpenTester login in the system browser. After the user approves the connection, it exchanges the one-time callback code with PKCE verification for an app-scoped token. Passwords and Google login tokens are never passed to the host app.
Android app → Browser login → Enrollment approval check
← Registered callback + code + state
Android SDK → Code exchange → App-scoped token → Access check / report submission1. Register a callback
Register this kind of HTTPS callback in the developer console and set the exact same OpenTesterConfig.redirectUri:
https://your-app.example.com/opentester/callbackThe SDK validates scheme, authority, path, and state. The configured URL must not contain a query or fragment. The server adds code and state query parameters to the callback response.
2. Configure Android App Links
For the debug-build example, put this in app/src/debug/AndroidManifest.xml. Merge it with your existing configuration and replace the package name and domain:
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application android:name="com.example.app.TesterApplication">
<activity
android:name="com.example.app.TesterActivity"
android:exported="true"
android:launchMode="singleTask">
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="your-app.example.com"
android:path="/opentester/callback" />
</intent-filter>
</activity>
</application>
</manifest>Android's link declaration includes HTTP and HTTPS, but the callback registered with OpenTester must use HTTPS. The SDK rejects a different response scheme. See the Android App Links guide.
Serve /.well-known/assetlinks.json on your callback domain with the installed app's application ID and signing certificate's SHA-256 fingerprint:
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.example.app",
"sha256_cert_fingerprints": ["YOUR_SIGNING_CERTIFICATE_SHA256"]
}
}
]Replace the fingerprint placeholder. With Play App Signing, use the certificate that signs the app installed by users. For debug testing, configure the debug application ID and certificate on your test domain. See website association setup.
3. Handle login and callbacks
This Activity uses TesterApplication from the quick start. Place it at app/src/debug/java/com/example/app/TesterActivity.kt and open it from your test screen. It uses the SDK's Kotlin Coroutines dependency:
package com.example.app
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.widget.Button
import android.widget.LinearLayout
import android.widget.TextView
import dev.opentester.sdk.OpenTesterException
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
class TesterActivity : Activity() {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
private val tester get() = (application as TesterApplication).tester
private lateinit var status: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
status = TextView(this)
val login = Button(this).apply {
text = "Tester login"
setOnClickListener { tester.authenticate(this@TesterActivity) }
}
setContentView(LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
addView(login)
addView(status)
})
consumeCallback(intent)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
consumeCallback(intent)
}
private fun consumeCallback(incoming: Intent) {
val uri = incoming.data ?: return
incoming.data = null // Do not process the callback again after Activity recreation.
scope.launch {
try {
val grant = tester.handleCallback(uri)
status.text = "${grant.user.name}, you can start testing."
} catch (e: CancellationException) {
throw e
} catch (e: OpenTesterException) {
status.text = e.message
}
}
}
override fun onDestroy() {
scope.cancel()
super.onDestroy()
}
}For your own system-browser launcher, use createAuthenticationUri(). Each call starts a new login attempt, so do not repeatedly call it to inspect the URL. Do not embed the login screen in a WebView.
Session lifetime and reauthentication
| Item | Behavior |
|---|---|
| Pending authentication | Held in SDK memory for up to 15 minutes |
| Server one-time code | Valid for 2 minutes, exchangeable once |
| App-scoped token | Valid for 1 hour |
| Process termination | Token and PKCE state are lost; sign in again |
| Starting a new login | Clears the old SDK session and creates new PKCE state |
logout() | Removes the in-memory token and pending authentication |
Automatic token refresh is not supported. Restart login even after a network error in handleCallback(). logout() does not clear browser cookies or immediately revoke previously issued server tokens.
Check enrollment access
When entering a test screen, call this from a coroutine:
val grant = tester.checkAccess()It checks the current server-side approval. Report submissions also check approval and are rejected if access was revoked. Do not cache AccessGrant as offline authorization.
SDK authentication only represents OpenTester enrollment access. Authenticate and authorize your app's own backend separately. The SDK does not determine Google Play opt-in or consecutive participation days.