SCAFactors

WebAuthn passkey on Android

This guide provides an overview of how to get started integrating the WebAuthn passkey factor in Android apps, for secure web-based authentication.

On Android, AuthTab is part of the Android Browser library that enables apps to authenticate users via a web service, presenting a secure and user-friendly popup browser for login flows.

Setup

  1. Add the necessary dependencies to your app’s build.gradle.kts file:
1dependencies {
2 implementation("androidx.browser:browser-auth:1.0.0-alpha03")
3 // Other dependencies
4}
  1. Define your custom URL scheme in your AndroidManifest.xml:
1<activity
2 android:name=".YourActivity"
3 android:exported="true"
4 android:launchMode="singleTask">
5 <intent-filter>
6 <action android:name="android.intent.action.VIEW" />
7 <category android:name="android.intent.category.DEFAULT" />
8 <category android:name="android.intent.category.BROWSABLE" />
9 <data android:scheme="mgpandroid" android:host="auth" />
10 </intent-filter>
11</activity>

Full code example

1import android.content.Intent
2import android.net.Uri
3import android.os.Bundle
4import android.util.Log
5import androidx.activity.enableEdgeToEdge
6import androidx.activity.result.ActivityResultLauncher
7import androidx.annotation.OptIn
8import androidx.appcompat.app.AppCompatActivity
9import androidx.browser.auth.AuthTabIntent
10import androidx.browser.auth.ExperimentalAuthTab
11
12// MARK: - Configuration Constants
13
14const val CONST_BASE_URL = "https://example.com" // Set your backend authentication URL here
15const val CONST_SCHEME = "mgpandroid://auth" // Your custom URL scheme
16
17class MainActivity : AppCompatActivity() {
18 private lateinit var authTabLauncher: ActivityResultLauncher<Intent>
19
20 @OptIn(ExperimentalAuthTab::class)
21 override fun onCreate(savedInstanceState: Bundle?) {
22 super.onCreate(savedInstanceState)
23 enableEdgeToEdge()
24 setContentView(R.layout.activity_main)
25
26 // Register the Auth Tab launcher
27 authTabLauncher = AuthTabIntent.registerActivityResultLauncher(this, this::handleAuthResult)
28 }
29
30 override fun onNewIntent(intent: Intent) {
31 super.onNewIntent(intent)
32 // Handle intent when app is opened from CustomTabs via our custom scheme
33 intent.data?.let { uri ->
34 handleIncomingURL(uri)
35 }
36 }
37
38 /**
39 * Starts the web authentication session
40 */
41 @OptIn(ExperimentalAuthTab::class)
42 fun startAuthentication() {
43 val strUrl = CONST_BASE_URL + "&returnUrl=" + CONST_SCHEME
44 val authUrl = Uri.parse(strUrl)
45
46 // Create and launch AuthTab directly
47 val authTabIntent = AuthTabIntent.Builder().build()
48 authTabIntent.launch(authTabLauncher, authUrl, "mgpandroid")
49 }
50
51 /**
52 * Handle the authentication result from AuthTab
53 */
54 @OptIn(ExperimentalAuthTab::class)
55 private fun handleAuthResult(result: AuthTabIntent.AuthResult) {
56 when (result.resultCode) {
57 AuthTabIntent.RESULT_OK -> {
58 // Process the result URI
59 val callbackURL = result.resultUri
60
61 // Handle the callback URL received from the authentication flow
62 handleIncomingURL(callbackURL)
63 }
64 AuthTabIntent.RESULT_CANCELED -> {
65 Log.d("Authentication", "Authentication was canceled")
66 }
67 AuthTabIntent.RESULT_VERIFICATION_FAILED -> {
68 Log.e("Authentication", "Verification failed")
69 }
70 AuthTabIntent.RESULT_VERIFICATION_TIMED_OUT -> {
71 Log.e("Authentication", "Verification timed out")
72 }
73 }
74 }
75
76 /**
77 * Handles the URL returned from the authentication flow
78 * This handles URLs from both the AuthTab result and from CustomTabs fallback
79 */
80 private fun handleIncomingURL(url: Uri?) {
81 if (url == null) {
82 Log.e("Authentication", "No callback URL received")
83 return
84 }
85
86 Log.d("Authentication", "Incoming URL: $url")
87
88 // Ensure the URL uses the expected custom scheme
89 val scheme = url.scheme
90 if (scheme != "mgpandroid") {
91 Log.e("Authentication", "Invalid URL scheme")
92 return
93 }
94
95 // Extract query parameter
96 val controlStatus = url.getQueryParameter("controlStatus") ?: ""
97
98 // Check if authentication was successful
99 if (controlStatus == "VALIDATED") {
100 Log.d("Authentication", "Action succeeded")
101 // Handle successful authentication
102 } else {
103 // Handle authentication failure or unexpected status
104 Log.e("Authentication", "Action Failed: controlStatus=$controlStatus")
105 }
106 }
107}

Automatic fallback mechanism

The androidx.browser.auth.AuthTabIntent library automatically handles the fallback to CustomTabs if AuthTab is not supported on the device. You don’t need to implement any custom logic for this fallback – the library takes care of it.

However, there are differences in how the redirect URL is handled:

  1. When using AuthTab directly, the result comes back through the handleAuthResult callback.
  2. When the library falls back to CustomTabs, the result comes back through your activity’s onNewIntent method.

Therefore, it’s important to implement both methods to handle both cases.

Usage example

To use the authentication in your app:

1// Inside an activity or fragment
2val authButton = findViewById<Button>(R.id.authButton)
3authButton.setOnClickListener {
4 startAuthentication()
5}

Notes

  • Replace CONST_BASE_URL with your backend authentication URL.

Troubleshooting

  • If the callback URL doesn’t trigger your app, ensure the URL scheme in your manifest matches exactly with what you’re using in the code.
  • Make sure your Android manifest has the correct intent filter for handling the callback URL.
  • If authentication fails, check the logs for detailed error information.