From 84e4d7e9f58168a5b1564f70784bc1bbc44bf627 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 17:53:53 +0000 Subject: [PATCH 1/6] Initial plan From 17f314686b975be7bf4031baacc102ce86e5d233 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 17:57:45 +0000 Subject: [PATCH 2/6] Add Prove Bot Detection integration Co-authored-by: mustafamizrak <2135310+mustafamizrak@users.noreply.github.com> --- app/build.gradle | 3 + app/src/main/AndroidManifest.xml | 4 + .../AuthClient.kt | 3 + .../EmailPasswordSignInSignUpFragment.kt | 43 ++++++ .../EmailSignInSignUpFragment.kt | 39 +++++ .../ProveBotDetectionHelper.kt | 140 ++++++++++++++++++ .../StrongAuthVerificationContactFragment.kt | 19 +++ settings.gradle | 8 + 8 files changed, 259 insertions(+) create mode 100644 app/src/main/java/com/azuresamples/msalnativeauthandroidkotlinsampleapp/ProveBotDetectionHelper.kt diff --git a/app/build.gradle b/app/build.gradle index 1177bbe..fd76934 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -56,6 +56,9 @@ dependencies { implementation 'com.squareup.okhttp3:okhttp:4.9.0' + // Prove Bot Detection SDK + implementation 'com.prove.sdk:proveauth:+' + implementation "androidx.core:core-ktx:$rootProject.ext.coreKtxVersion" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$rootProject.ext.kotlinXCoroutinesVersion" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 618807a..55c5396 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,6 +4,10 @@ + + + + { + binding.passwordText.text?.clear() + StringUtil.overwriteWithNull(password) + displayDialog("Fraud Detection", "Sign-in blocked: ${botResult.reason}") + return@launch + } + is ProveBotDetectionHelper.BotDetectionResult.Error -> { + Log.w(TAG, "Bot detection error: ${botResult.message}") + // Continue on error — adjust policy as needed + } + is ProveBotDetectionHelper.BotDetectionResult.Passed -> { + Log.i(TAG, "Bot detection passed. Proceeding with sign-in.") + } + } + } + val parameters = NativeAuthSignInParameters(username = email) parameters.password = password val actionResult: SignInResult = authClient.signIn(parameters) @@ -137,6 +159,27 @@ class EmailPasswordSignInSignUpFragment : Fragment() { val password = CharArray(binding.passwordText.length()) binding.passwordText.text?.getChars(0, binding.passwordText.length(), password, 0) + // Prove Bot Detection Gate + val phoneNumber = email // Replace with actual phone number field if available + if (ProveBotDetectionHelper.hasProveKey()) { + val botResult = ProveBotDetectionHelper.performBotDetection(phoneNumber) + when (botResult) { + is ProveBotDetectionHelper.BotDetectionResult.Failed -> { + binding.passwordText.text?.clear() + StringUtil.overwriteWithNull(password) + displayDialog("Fraud Detection", "Sign-up blocked: ${botResult.reason}") + return@launch + } + is ProveBotDetectionHelper.BotDetectionResult.Error -> { + Log.w(TAG, "Bot detection error: ${botResult.message}") + // Continue on error — adjust policy as needed + } + is ProveBotDetectionHelper.BotDetectionResult.Passed -> { + Log.i(TAG, "Bot detection passed. Proceeding with sign-up.") + } + } + } + val parameters = NativeAuthSignUpParameters(username = email) parameters.password = password val actionResult: SignUpResult = authClient.signUp(parameters) diff --git a/app/src/main/java/com/azuresamples/msalnativeauthandroidkotlinsampleapp/EmailSignInSignUpFragment.kt b/app/src/main/java/com/azuresamples/msalnativeauthandroidkotlinsampleapp/EmailSignInSignUpFragment.kt index 5f2d5b3..91b823a 100644 --- a/app/src/main/java/com/azuresamples/msalnativeauthandroidkotlinsampleapp/EmailSignInSignUpFragment.kt +++ b/app/src/main/java/com/azuresamples/msalnativeauthandroidkotlinsampleapp/EmailSignInSignUpFragment.kt @@ -2,6 +2,7 @@ package com.azuresamples.msalnativeauthandroidkotlinsampleapp import android.app.AlertDialog import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -94,6 +95,25 @@ class EmailSignInSignUpFragment : Fragment() { CoroutineScope(Dispatchers.Main).launch { val email = binding.emailText.text.toString() + // Prove Bot Detection Gate + val phoneNumber = email // Replace with actual phone number field if available + if (ProveBotDetectionHelper.hasProveKey()) { + val botResult = ProveBotDetectionHelper.performBotDetection(phoneNumber) + when (botResult) { + is ProveBotDetectionHelper.BotDetectionResult.Failed -> { + displayDialog("Fraud Detection", "Sign-in blocked: ${botResult.reason}") + return@launch + } + is ProveBotDetectionHelper.BotDetectionResult.Error -> { + Log.w(TAG, "Bot detection error: ${botResult.message}") + // Continue on error — adjust policy as needed + } + is ProveBotDetectionHelper.BotDetectionResult.Passed -> { + Log.i(TAG, "Bot detection passed. Proceeding with sign-in.") + } + } + } + val parameters = NativeAuthSignInParameters(username = email) val actionResult = authClient.signIn(parameters) @@ -125,6 +145,25 @@ class EmailSignInSignUpFragment : Fragment() { CoroutineScope(Dispatchers.Main).launch { val email = binding.emailText.text.toString() + // Prove Bot Detection Gate + val phoneNumber = email // Replace with actual phone number field if available + if (ProveBotDetectionHelper.hasProveKey()) { + val botResult = ProveBotDetectionHelper.performBotDetection(phoneNumber) + when (botResult) { + is ProveBotDetectionHelper.BotDetectionResult.Failed -> { + displayDialog("Fraud Detection", "Sign-up blocked: ${botResult.reason}") + return@launch + } + is ProveBotDetectionHelper.BotDetectionResult.Error -> { + Log.w(TAG, "Bot detection error: ${botResult.message}") + // Continue on error — adjust policy as needed + } + is ProveBotDetectionHelper.BotDetectionResult.Passed -> { + Log.i(TAG, "Bot detection passed. Proceeding with sign-up.") + } + } + } + val parameters = NativeAuthSignUpParameters(username = email) val actionResult = authClient.signUp(parameters) diff --git a/app/src/main/java/com/azuresamples/msalnativeauthandroidkotlinsampleapp/ProveBotDetectionHelper.kt b/app/src/main/java/com/azuresamples/msalnativeauthandroidkotlinsampleapp/ProveBotDetectionHelper.kt new file mode 100644 index 0000000..598a102 --- /dev/null +++ b/app/src/main/java/com/azuresamples/msalnativeauthandroidkotlinsampleapp/ProveBotDetectionHelper.kt @@ -0,0 +1,140 @@ +package com.azuresamples.msalnativeauthandroidkotlinsampleapp + +import android.content.Context +import android.util.Log +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import org.json.JSONObject + +/** + * Helper class for integrating Prove Bot Detection into the sign-in/sign-up flows. + * + * Flow overview (per https://developer.prove.com/docs/check-for-prove-key#prompt-for-phone-number-3): + * 1. Initialize the Prove SDK on app start to obtain a device-level "Prove Key". + * 2. Before sending an OTP / completing sign-in, call YOUR backend with the phone number + Prove Key. + * 3. Your backend calls Prove's /v3/verify API with verificationType="bot". + * 4. The backend returns the bot-detection result to the app. + * 5. The app decides whether to proceed with authentication or block the attempt. + */ +object ProveBotDetectionHelper { + + private const val TAG = "ProveBotDetection" + + // IMPORTANT: Replace with your own backend endpoint that proxies to Prove's API. + // NEVER call Prove's API directly from the client — your access token must stay server-side. + private const val BOT_DETECTION_BACKEND_URL = "https://your-backend.example.com/api/prove/bot-detect" + + private val httpClient = OkHttpClient() + private var proveKey: String? = null + + /** + * Initialize the Prove SDK and retrieve the Prove Key (device fingerprint). + * Call this once during app startup (e.g., in AuthClient.initialize). + */ + fun initialize(context: Context) { + try { + // Initialize the Prove SDK — the exact API depends on the SDK version. + // Refer to: https://developer.prove.com/reference/unify-android-sdk + // + // Example (pseudocode — adapt to actual SDK): + // ProveAuth.initialize(context, "YOUR_PROVE_CLIENT_ID") + // proveKey = ProveAuth.getDeviceId() + + Log.i(TAG, "Prove SDK initialized. Prove Key obtained.") + + // TODO: Replace the line below with real SDK call + // proveKey = ProveAuth.getDeviceId() + proveKey = "PLACEHOLDER_PROVE_KEY" // Remove this after real integration + } catch (e: Exception) { + Log.e(TAG, "Failed to initialize Prove SDK", e) + } + } + + /** + * Check whether the Prove Key is available. + */ + fun hasProveKey(): Boolean { + return !proveKey.isNullOrBlank() + } + + /** + * Perform bot detection by calling YOUR backend with the phone number and Prove Key. + * + * Your backend should: + * 1. Obtain a Prove access token (OAuth2 client_credentials grant). + * 2. POST to https://platform.prove.com/v3/verify with: + * { + * "verificationType": "bot", + * "phoneNumber": "", + * "deviceId": "", + * "clientRequestId": "" + * } + * 3. Return the result to the mobile app. + * + * @param phoneNumber The phone number entered by the user (E.164 format recommended) + * @return BotDetectionResult indicating whether the user passed or failed bot detection + */ + suspend fun performBotDetection(phoneNumber: String): BotDetectionResult { + if (!hasProveKey()) { + Log.w(TAG, "Prove Key not available. Cannot perform bot detection.") + return BotDetectionResult.Error("Prove Key not available. Please restart the app.") + } + + return withContext(Dispatchers.IO) { + try { + val requestBody = JSONObject().apply { + put("phoneNumber", phoneNumber) + put("deviceId", proveKey) + put("clientRequestId", java.util.UUID.randomUUID().toString()) + }.toString() + + val request = Request.Builder() + .url(BOT_DETECTION_BACKEND_URL) + .post(requestBody.toRequestBody("application/json".toMediaType())) + .build() + + val response = httpClient.newCall(request).execute() + + if (response.isSuccessful) { + val responseBody = response.body?.string() ?: "{}" + val json = JSONObject(responseBody) + + val isHuman = json.optBoolean("success", false) + val score = json.optDouble("score", 0.0) + + if (isHuman) { + Log.i(TAG, "Bot detection passed. Score: $score") + BotDetectionResult.Passed(score) + } else { + Log.w(TAG, "Bot detection failed. Score: $score") + BotDetectionResult.Failed(score, "Phone number flagged as potential fraud.") + } + } else { + Log.e(TAG, "Bot detection API returned ${response.code}") + BotDetectionResult.Error("Bot detection service unavailable (HTTP ${response.code}).") + } + } catch (e: Exception) { + Log.e(TAG, "Bot detection request failed", e) + BotDetectionResult.Error("Bot detection failed: ${e.message}") + } + } + } + + /** + * Sealed class representing the result of a bot detection check. + */ + sealed class BotDetectionResult { + /** The phone number passed bot detection (appears to be a real human). */ + data class Passed(val score: Double) : BotDetectionResult() + + /** The phone number failed bot detection (potential bot/fraud). */ + data class Failed(val score: Double, val reason: String) : BotDetectionResult() + + /** An error occurred during bot detection. */ + data class Error(val message: String) : BotDetectionResult() + } +} diff --git a/app/src/main/java/com/azuresamples/msalnativeauthandroidkotlinsampleapp/StrongAuthVerificationContactFragment.kt b/app/src/main/java/com/azuresamples/msalnativeauthandroidkotlinsampleapp/StrongAuthVerificationContactFragment.kt index 6a86125..bb85a2a 100644 --- a/app/src/main/java/com/azuresamples/msalnativeauthandroidkotlinsampleapp/StrongAuthVerificationContactFragment.kt +++ b/app/src/main/java/com/azuresamples/msalnativeauthandroidkotlinsampleapp/StrongAuthVerificationContactFragment.kt @@ -2,6 +2,7 @@ package com.azuresamples.msalnativeauthandroidkotlinsampleapp import android.app.AlertDialog import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -59,6 +60,24 @@ class StrongAuthVerificationContactFragment : Fragment() { if (optionalEmail.isBlank()) { optionalEmail = authMethod.loginHint.orEmpty() } + + // Prove Bot Detection Gate for SMS + if (authMethod.challengeChannel.uppercase() == "SMS" && ProveBotDetectionHelper.hasProveKey()) { + val botResult = ProveBotDetectionHelper.performBotDetection(optionalEmail) + when (botResult) { + is ProveBotDetectionHelper.BotDetectionResult.Failed -> { + displayDialog("SMS Fraud Detection", "This phone number has been flagged: ${botResult.reason}") + return@launch + } + is ProveBotDetectionHelper.BotDetectionResult.Error -> { + Log.w("StrongAuth", "Bot detection error: ${botResult.message}") + } + is ProveBotDetectionHelper.BotDetectionResult.Passed -> { + Log.i("StrongAuth", "Bot detection passed for SMS verification.") + } + } + } + val params = NativeAuthChallengeAuthMethodParameters(authMethod, optionalEmail) val actionResult = currentState.challengeAuthMethod(params) diff --git a/settings.gradle b/settings.gradle index e7b4def..7a36e98 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,9 @@ include ':app' + +dependencyResolutionManagement { + repositories { + maven { + url = "https://prove.jfrog.io/artifactory/libs-public-maven/" + } + } +} From 264df6a986c5e79c0f4b42c6d3b8fa57d5c46718 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 17:59:43 +0000 Subject: [PATCH 3/6] Fix Prove repository configuration to use allprojects block Co-authored-by: mustafamizrak <2135310+mustafamizrak@users.noreply.github.com> --- app/build.gradle | 3 +++ settings.gradle | 8 -------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index fd76934..48cae60 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -8,6 +8,9 @@ allprojects { url 'https://pkgs.dev.azure.com/MicrosoftDeviceSDK/DuoSDK-Public/_packaging/Duo-SDK-Feed/maven/v1' name 'Duo-SDK-Feed' } + maven { + url 'https://prove.jfrog.io/artifactory/libs-public-maven/' + } mavenCentral() google() } diff --git a/settings.gradle b/settings.gradle index 7a36e98..e7b4def 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,9 +1 @@ include ':app' - -dependencyResolutionManagement { - repositories { - maven { - url = "https://prove.jfrog.io/artifactory/libs-public-maven/" - } - } -} From 03eb88f0a440d00d4889db0c6ce4af45191992dd Mon Sep 17 00:00:00 2001 From: Toluwalase Cato Date: Fri, 13 Feb 2026 11:13:52 +0000 Subject: [PATCH 4/6] Add Prove manager --- app/src/main/assets/config.json | 6 + .../MFAFragment.kt | 2 + .../MainApplication.kt | 43 +++ .../ProveManager.kt | 256 ++++++++++++++++++ .../main/res/raw/auth_config_native_auth.json | 5 +- auto-config.json | 10 +- build.gradle | 13 +- gradle/versions.gradle | 4 +- 8 files changed, 325 insertions(+), 14 deletions(-) create mode 100644 app/src/main/assets/config.json create mode 100644 app/src/main/java/com/azuresamples/msalnativeauthandroidkotlinsampleapp/MainApplication.kt create mode 100644 app/src/main/java/com/azuresamples/msalnativeauthandroidkotlinsampleapp/ProveManager.kt diff --git a/app/src/main/assets/config.json b/app/src/main/assets/config.json new file mode 100644 index 0000000..a4b4728 --- /dev/null +++ b/app/src/main/assets/config.json @@ -0,0 +1,6 @@ +{ + "Prove": { + "clientId": "nativeauthfraudcheck-poc-e7971c6c-c35b-4e17-80f8-99aefdb611c0-1770372962304", + "clientSecret": "CgvbgUQpZ95TXZOkYgi3TgOs1MITJj6L" + } +} diff --git a/app/src/main/java/com/azuresamples/msalnativeauthandroidkotlinsampleapp/MFAFragment.kt b/app/src/main/java/com/azuresamples/msalnativeauthandroidkotlinsampleapp/MFAFragment.kt index 1fec133..1109d0d 100644 --- a/app/src/main/java/com/azuresamples/msalnativeauthandroidkotlinsampleapp/MFAFragment.kt +++ b/app/src/main/java/com/azuresamples/msalnativeauthandroidkotlinsampleapp/MFAFragment.kt @@ -29,6 +29,7 @@ import com.microsoft.identity.nativeauth.statemachine.states.RegisterStrongAuthS import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import java.util.Arrays class MFAFragment : Fragment() { @@ -100,6 +101,7 @@ class MFAFragment : Fragment() { val parameters = NativeAuthSignInParameters(username = email) parameters.password = password + parameters.scopes = Arrays.asList("openid", "offline_access", "profile", "api://019f8c18-e680-43b3-9ac3-d9e118b69c0d/App.Read") val actionResult: SignInResult = authClient.signIn(parameters) binding.passwordText.text?.clear() StringUtil.overwriteWithNull(password) diff --git a/app/src/main/java/com/azuresamples/msalnativeauthandroidkotlinsampleapp/MainApplication.kt b/app/src/main/java/com/azuresamples/msalnativeauthandroidkotlinsampleapp/MainApplication.kt new file mode 100644 index 0000000..cfa31f5 --- /dev/null +++ b/app/src/main/java/com/azuresamples/msalnativeauthandroidkotlinsampleapp/MainApplication.kt @@ -0,0 +1,43 @@ +package com.azuresamples.msalnativeauthandroidkotlinsampleapp + +import android.app.Application +import android.util.Log +import org.json.JSONObject + +class MainApplication : Application() { + + override fun onCreate() { + super.onCreate() + + Log.d("MS", "Application onCreate called") + initializeProveIfConfigured() + } + + private fun initializeProveIfConfigured() { + // Read Prove credentials directly from the packaged config file: + // `app/src/main/assets/config.json` + val (clientId, clientSecret) = try { + val jsonText = assets.open("config.json").bufferedReader().use { it.readText() } + val proveJson = JSONObject(jsonText).optJSONObject("Prove") + val id = proveJson?.optString("clientId").orEmpty().trim() + val secret = proveJson?.optString("clientSecret").orEmpty().trim() + id to secret + } catch (t: Throwable) { + Log.e("MS", "Failed to read Prove config from assets/config.json", t) + "" to "" + } + + if (clientId.isBlank() || clientSecret.isBlank()) { + Log.w( + "MS", + "Prove not initialized. Set Prove.clientId and Prove.clientSecret in app/src/main/assets/config.json." + ) + return + } + + ProveManager.getInstance().initialize( + clientId = clientId, + clientSecret = clientSecret + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/azuresamples/msalnativeauthandroidkotlinsampleapp/ProveManager.kt b/app/src/main/java/com/azuresamples/msalnativeauthandroidkotlinsampleapp/ProveManager.kt new file mode 100644 index 0000000..c0ded64 --- /dev/null +++ b/app/src/main/java/com/azuresamples/msalnativeauthandroidkotlinsampleapp/ProveManager.kt @@ -0,0 +1,256 @@ +package com.azuresamples.msalnativeauthandroidkotlinsampleapp + +import android.util.Log +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import okhttp3.FormBody +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import org.json.JSONObject +import java.util.UUID +import java.util.concurrent.TimeUnit + + +/** + * ProveManager handles Prove Identity verification for MFA with SMS + * using customer-supplied possession flow. + * + * Uses OkHttp for REST calls because the Prove server SDK (com.prove:proveapi) + * relies on java.net.http.HttpClient which is not available on Android. + * + * Documentation: https://developer.prove.com/docs/check-for-prove-key#prove-possession-mobile + */ +class ProveManager private constructor() { + + companion object { + private const val TAG = "ProveManager" + private const val BASE_URL = "https://platform.uat.proveapis.com" + private const val TOKEN_URL = "$BASE_URL/token" + private const val V3_UNIFY_URL = "$BASE_URL/v3/unify" + private const val V3_VALIDATE_URL = "$BASE_URL/v3/validate" + private val JSON_MEDIA_TYPE = "application/json; charset=utf-8".toMediaType() + + @Volatile + private var instance: ProveManager? = null + + fun getInstance(): ProveManager { + return instance ?: synchronized(this) { + instance ?: ProveManager().also { instance = it } + } + } + } + + // Store the correlation ID from the start response for subsequent calls + private var correlationId: String? = null + private var authToken: String? = null + + private var clientId: String? = null + private var clientSecret: String? = null + + // Cached OAuth access token + private var accessToken: String? = null + + private val httpClient = OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .build() + + /** + * Result class for Prove verification operations + */ + sealed class ProveResult { + data class Success( + val correlationId: String, + val authToken: String? = null, + val phoneVerified: Boolean = false, + val possessionResult: String? = null + ) : ProveResult() + + /** + * The Prove call failed or possession could not be verified. + * + * Note: In this sample we fail closed (possession must verify) before proceeding to Microsoft SMS OTP. + */ + data class Error( + val message: String, + val errorCode: String? = null + ) : ProveResult() + } + + /** + * Initialize the Prove API credentials. + * NOTE: In production, credentials should be fetched from your backend server, + * not stored in the app. + */ + fun initialize(clientId: String, clientSecret: String) { + this.clientId = clientId + this.clientSecret = clientSecret + Log.d(TAG, "Prove API credentials stored") + } + + /** + * Obtain an OAuth2 access token using the client-credentials grant. + * Caches the token for subsequent calls (no expiry handling in this sample). + */ + private fun fetchAccessToken(): String { + Log.d(TAG, "First touch of fetchAccessToken() - fetching new token") + accessToken?.let { return it } + + val id = clientId + ?: throw IllegalStateException("Prove clientId not set. Call initialize() first.") + val secret = clientSecret + ?: throw IllegalStateException("Prove clientSecret not set. Call initialize() 2.") + Log.d(TAG, "2 touch of fetchAccessToken() - fetching new token") + val body = JSONObject().apply { + put("grant_type", "client_credentials") + put("client_id", id) + put("client_secret", secret) + }.toString() + Log.d(TAG, "${body}") + val request = Request.Builder() + .url(TOKEN_URL) + .post(body.toRequestBody("application/json".toMediaType())) + .build() + + Log.d(TAG, "${request}") + httpClient.newCall(request).execute().use { response -> + + if (!response.isSuccessful) { + throw RuntimeException("Token request failed: ${response.code} ${response.body?.string()}") + } + + val json = JSONObject(response.body!!.string()) + Log.d(TAG, "${json}") + val token = json.getString("access_token") + accessToken = token + Log.d(TAG, "${token}") + return token + } + + } + + /** + * Start a V3 Unify session and store authToken / correlationId. + */ + suspend fun initializeWithAuthToken(phoneNumber: String?): ProveResult = withContext(Dispatchers.IO) { + try { + val token = fetchAccessToken() + + val payload = JSONObject().apply { + put("clientRequestId", UUID.randomUUID().toString()) + put("possessionType", "none") + phoneNumber?.let { put("phoneNumber", it) } + } + + val request = Request.Builder() + .url(V3_UNIFY_URL) + .addHeader("Authorization", "Bearer $token") + .post(payload.toString().toRequestBody(JSON_MEDIA_TYPE)) + .build() + + httpClient.newCall(request).execute().use { response -> + val responseBody = response.body?.string() + if (!response.isSuccessful) { + return@withContext ProveResult.Error( + message = "V3 Unify failed: ${response.code} $responseBody", + errorCode = response.code.toString() + ) + } + + val json = JSONObject(responseBody!!) + authToken = json.optString("authToken", null) + correlationId = json.optString("correlationId", null) + + Log.d(TAG, "V3 Unify successful. CorrelationId: $correlationId") + + return@withContext ProveResult.Success( + correlationId = correlationId!!, + authToken = authToken + ) + } + } catch (e: Exception) { + Log.e(TAG, "initializeWithAuthToken failed", e) + ProveResult.Error(message = e.message ?: "Unknown error occurred") + } + } + + /** + * Step 2: Validate/Check for Prove Key (Possession Check) + * + * This checks if the device has Prove Key installed and can verify possession. + * + * Policy for this sample: + * - If Prove Key is available and possession verifies: proceed to Microsoft SMS OTP. + * - If not available / not verified / API error: FAIL CLOSED and do not proceed. + * + * @param correlationId The correlation ID from initializeWithAuthToken + */ + suspend fun checkPossession(correlationId: String? = null): ProveResult = withContext(Dispatchers.IO) { + try { + clearSession() + + val token = fetchAccessToken() + + val corrId = correlationId ?: this@ProveManager.correlationId + ?: return@withContext ProveResult.Error( + message = "No correlation ID available. Call initializeWithAuthToken() first." + ) + + Log.d(TAG, "Checking possession for correlationId: $corrId") + + val payload = JSONObject().apply { + put("correlationId", corrId) + } + + val request = Request.Builder() + .url(V3_VALIDATE_URL) + .addHeader("Authorization", "Bearer $token") + .post(payload.toString().toRequestBody(JSON_MEDIA_TYPE)) + .build() + + httpClient.newCall(request).execute().use { response -> + val responseBody = response.body?.string() + if (!response.isSuccessful) { + return@withContext ProveResult.Error( + message = "V3 Validate failed: ${response.code} $responseBody", + errorCode = response.code.toString() + ) + } + + val json = JSONObject(responseBody!!) + val verified = json.optBoolean("success", false) + + Log.d(TAG, "Possession check result: success=$verified") + + if (verified) { + return@withContext ProveResult.Success( + correlationId = corrId, + phoneVerified = true, + possessionResult = "verified" + ) + } + + val returnedPhone = json.optString("phoneNumber", "") + Log.w(TAG, "Possession not verified. success=$verified, phoneNumber=${returnedPhone.takeLast(4)}") + return@withContext ProveResult.Error( + message = "Prove possession could not be verified. Cannot proceed with SMS OTP." + ) + } + } catch (e: Exception) { + Log.e(TAG, "Possession check exception", e) + ProveResult.Error(message = e.message ?: "Unknown error occurred") + } + } + + /** + * Clear the current session data. + */ + private fun clearSession() { + correlationId = null + authToken = null + Log.d(TAG, "Session cleared") + } +} \ No newline at end of file diff --git a/app/src/main/res/raw/auth_config_native_auth.json b/app/src/main/res/raw/auth_config_native_auth.json index 3c0f378..d8505aa 100644 --- a/app/src/main/res/raw/auth_config_native_auth.json +++ b/app/src/main/res/raw/auth_config_native_auth.json @@ -1,13 +1,14 @@ { - "client_id": "Enter_the_Application_Id_Here", + "client_id": "019f8c18-e680-43b3-9ac3-d9e118b69c0d", "authorities": [ { "type": "CIAM", - "authority_url": "https://Enter_the_Tenant_Subdomain_Here.ciamlogin.com/Enter_the_Tenant_Subdomain_Here.onmicrosoft.com/" + "authority_url": "https://nativetesting.online/628ff5bc-c796-4a6c-930e-f1ed859b77bc/" } ], "challenge_types": ["oob", "password"], "capabilities": ["mfa_required", "registration_required"], + "dc": "ESTS-PUB-WEULR1-FD000-TEST1-100", "logging": { "pii_enabled": false, "log_level": "INFO", diff --git a/auto-config.json b/auto-config.json index a53a508..f919f11 100644 --- a/auto-config.json +++ b/auto-config.json @@ -4,6 +4,12 @@ "Level": 200, "Client": "Android" }, + + "Prove": { + "clientId": "", + "clientSecret": "" + }, + "AppRegistrations": [ { "x-ms-id": "ciam-android-native-app", @@ -33,8 +39,8 @@ { "settingFile": "app/src/main/res/raw/auth_config_native_auth.json", "replaceTokens": { - "appId": "Enter_the_Application_Id_Here", - "tenantName": "Enter_the_Tenant_Subdomain_Here" + "appId": "019f8c18-e680-43b3-9ac3-d9e118b69c0d", + "tenantName": "628ff5bc-c796-4a6c-930e-f1ed859b77bc" } } ] diff --git a/build.gradle b/build.gradle index 18c4ea8..bfdc246 100644 --- a/build.gradle +++ b/build.gradle @@ -6,6 +6,11 @@ buildscript { repositories { google() jcenter() + + mavenCentral() + maven { + url = "https://prove.jfrog.io/artifactory/libs-public-maven/" + } } dependencies { classpath "com.android.tools.build:gradle:$rootProject.ext.gradleVersion" @@ -15,14 +20,6 @@ buildscript { } } -allprojects { - repositories { - google() - jcenter() - - } -} - task clean(type: Delete) { delete rootProject.buildDir } diff --git a/gradle/versions.gradle b/gradle/versions.gradle index 717ded2..886d40a 100644 --- a/gradle/versions.gradle +++ b/gradle/versions.gradle @@ -7,8 +7,8 @@ ext { compileSdkVersion = 34 buildToolsVersion = "28.0.3" coreKtxVersion = "1.6.0" - kotlinXCoroutinesVersion = "1.6.4" - kotlinVersion = '1.7.21' + kotlinXCoroutinesVersion = "1.9.0" + kotlinVersion = '2.1.0' // Plugins gradleVersion = '8.11.1' From a7fde1be6897b28434cb6ae53996f7c61c375d94 Mon Sep 17 00:00:00 2001 From: Toluwalase Cato Date: Fri, 13 Feb 2026 13:02:13 +0000 Subject: [PATCH 5/6] Add Prove manager --- app/src/main/assets/config.json | 6 + .../MainApplication.kt | 43 +++ .../ProveManager.kt | 256 ++++++++++++++++++ 3 files changed, 305 insertions(+) create mode 100644 app/src/main/assets/config.json create mode 100644 app/src/main/java/com/azuresamples/msalnativeauthandroidkotlinsampleapp/MainApplication.kt create mode 100644 app/src/main/java/com/azuresamples/msalnativeauthandroidkotlinsampleapp/ProveManager.kt diff --git a/app/src/main/assets/config.json b/app/src/main/assets/config.json new file mode 100644 index 0000000..e4eb16d --- /dev/null +++ b/app/src/main/assets/config.json @@ -0,0 +1,6 @@ +{ + "Prove": { + "clientId": "nativeauthfraudcheck-poc-e7971c6c-c35b-4e17-80f8-99aefdb611c0-1770372962304", + "clientSecret": "CgvbgUQpZ95TXZOkYgi3TgOs1MITJj6L" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/azuresamples/msalnativeauthandroidkotlinsampleapp/MainApplication.kt b/app/src/main/java/com/azuresamples/msalnativeauthandroidkotlinsampleapp/MainApplication.kt new file mode 100644 index 0000000..cfa31f5 --- /dev/null +++ b/app/src/main/java/com/azuresamples/msalnativeauthandroidkotlinsampleapp/MainApplication.kt @@ -0,0 +1,43 @@ +package com.azuresamples.msalnativeauthandroidkotlinsampleapp + +import android.app.Application +import android.util.Log +import org.json.JSONObject + +class MainApplication : Application() { + + override fun onCreate() { + super.onCreate() + + Log.d("MS", "Application onCreate called") + initializeProveIfConfigured() + } + + private fun initializeProveIfConfigured() { + // Read Prove credentials directly from the packaged config file: + // `app/src/main/assets/config.json` + val (clientId, clientSecret) = try { + val jsonText = assets.open("config.json").bufferedReader().use { it.readText() } + val proveJson = JSONObject(jsonText).optJSONObject("Prove") + val id = proveJson?.optString("clientId").orEmpty().trim() + val secret = proveJson?.optString("clientSecret").orEmpty().trim() + id to secret + } catch (t: Throwable) { + Log.e("MS", "Failed to read Prove config from assets/config.json", t) + "" to "" + } + + if (clientId.isBlank() || clientSecret.isBlank()) { + Log.w( + "MS", + "Prove not initialized. Set Prove.clientId and Prove.clientSecret in app/src/main/assets/config.json." + ) + return + } + + ProveManager.getInstance().initialize( + clientId = clientId, + clientSecret = clientSecret + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/azuresamples/msalnativeauthandroidkotlinsampleapp/ProveManager.kt b/app/src/main/java/com/azuresamples/msalnativeauthandroidkotlinsampleapp/ProveManager.kt new file mode 100644 index 0000000..e366630 --- /dev/null +++ b/app/src/main/java/com/azuresamples/msalnativeauthandroidkotlinsampleapp/ProveManager.kt @@ -0,0 +1,256 @@ +package com.azuresamples.msalnativeauthandroidkotlinsampleapp + +import android.util.Log +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import okhttp3.FormBody +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import org.json.JSONObject +import java.util.UUID +import java.util.concurrent.TimeUnit + + +/** + * ProveManager handles Prove Identity verification for MFA with SMS + * using customer-supplied possession flow. + * + * Uses OkHttp for REST calls because the Prove server SDK (com.prove:proveapi) + * relies on java.net.http.HttpClient which is not available on Android. + * + * Documentation: https://developer.prove.com/docs/check-for-prove-key#prove-possession-mobile + */ +class ProveManager private constructor() { + + companion object { + private const val TAG = "ProveManager" + private const val BASE_URL = "https://platform.uat.proveapis.com" + private const val TOKEN_URL = "$BASE_URL/token" + private const val V3_UNIFY_URL = "$BASE_URL/v3/unify" + private const val V3_VALIDATE_URL = "$BASE_URL/v3/validate" + private val JSON_MEDIA_TYPE = "application/json; charset=utf-8".toMediaType() + + @Volatile + private var instance: ProveManager? = null + + fun getInstance(): ProveManager { + return instance ?: synchronized(this) { + instance ?: ProveManager().also { instance = it } + } + } + } + + // Store the correlation ID from the start response for subsequent calls + private var correlationId: String? = null + private var authToken: String? = null + + private var clientId: String? = null + private var clientSecret: String? = null + + // Cached OAuth access token + private var accessToken: String? = null + + private val httpClient = OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .build() + + /** + * Result class for Prove verification operations + */ + sealed class ProveResult { + data class Success( + val correlationId: String, + val authToken: String? = null, + val phoneVerified: Boolean = false, + val possessionResult: String? = null + ) : ProveResult() + + /** + * The Prove call failed or possession could not be verified. + * + * Note: In this sample we fail closed (possession must verify) before proceeding to Microsoft SMS OTP. + */ + data class Error( + val message: String, + val errorCode: String? = null + ) : ProveResult() + } + + /** + * Initialize the Prove API credentials. + * NOTE: In production, credentials should be fetched from your backend server, + * not stored in the app. + */ + fun initialize(clientId: String, clientSecret: String) { + this.clientId = clientId + this.clientSecret = clientSecret + Log.d(TAG, "Prove API credentials stored") + } + + /** + * Obtain an OAuth2 access token using the client-credentials grant. + * Caches the token for subsequent calls (no expiry handling in this sample). + */ + private fun fetchAccessToken(): String { + Log.d(TAG, "First touch of fetchAccessToken() - fetching new token") + accessToken?.let { return it } + + val id = clientId + ?: throw IllegalStateException("Prove clientId not set. Call initialize() first.") + val secret = clientSecret + ?: throw IllegalStateException("Prove clientSecret not set. Call initialize() 2.") + Log.d(TAG, "2 touch of fetchAccessToken() - fetching new token") + val formBody = FormBody.Builder() + .add("grant_type", "client_credentials") + .add("client_id", id) + .add("client_secret", secret) + .build() + Log.d(TAG, "${formBody}") + val request = Request.Builder() + .url(TOKEN_URL) + .post(formBody) + .build() + + Log.d(TAG, "${request}") + httpClient.newCall(request).execute().use { response -> + + if (!response.isSuccessful) { + throw RuntimeException("Token request failed: ${response.code} ${response.body?.string()}") + } + + val json = JSONObject(response.body!!.string()) + Log.d(TAG, "${json}") + val token = json.getString("access_token") + accessToken = token + Log.d(TAG, "${token}") + return token + } + + } + + /** + * Start a V3 Unify session and store authToken / correlationId. + */ + suspend fun initializeWithAuthToken(phoneNumber: String?): ProveResult = withContext(Dispatchers.IO) { + try { + val token = fetchAccessToken() + + val payload = JSONObject().apply { + put("clientRequestId", UUID.randomUUID().toString()) + put("possessionType", "none") + phoneNumber?.let { put("phoneNumber", it) } + } + + val request = Request.Builder() + .url(V3_UNIFY_URL) + .addHeader("Authorization", "Bearer $token") + .post(payload.toString().toRequestBody(JSON_MEDIA_TYPE)) + .build() + + httpClient.newCall(request).execute().use { response -> + val responseBody = response.body?.string() + if (!response.isSuccessful) { + return@withContext ProveResult.Error( + message = "V3 Unify failed: ${response.code} $responseBody", + errorCode = response.code.toString() + ) + } + + val json = JSONObject(responseBody!!) + authToken = json.optString("authToken", null) + correlationId = json.optString("correlationId", null) + + Log.d(TAG, "V3 Unify successful. CorrelationId: $correlationId") + + return@withContext ProveResult.Success( + correlationId = correlationId!!, + authToken = authToken + ) + } + } catch (e: Exception) { + Log.e(TAG, "initializeWithAuthToken failed", e) + ProveResult.Error(message = e.message ?: "Unknown error occurred") + } + } + + /** + * Step 2: Validate/Check for Prove Key (Possession Check) + * + * This checks if the device has Prove Key installed and can verify possession. + * + * Policy for this sample: + * - If Prove Key is available and possession verifies: proceed to Microsoft SMS OTP. + * - If not available / not verified / API error: FAIL CLOSED and do not proceed. + * + * @param correlationId The correlation ID from initializeWithAuthToken + */ + suspend fun checkPossession(correlationId: String? = null): ProveResult = withContext(Dispatchers.IO) { + try { + clearSession() + + val token = fetchAccessToken() + + val corrId = correlationId ?: this@ProveManager.correlationId + ?: return@withContext ProveResult.Error( + message = "No correlation ID available. Call initializeWithAuthToken() first." + ) + + Log.d(TAG, "Checking possession for correlationId: $corrId") + + val payload = JSONObject().apply { + put("correlationId", corrId) + } + + val request = Request.Builder() + .url(V3_VALIDATE_URL) + .addHeader("Authorization", "Bearer $token") + .post(payload.toString().toRequestBody(JSON_MEDIA_TYPE)) + .build() + + httpClient.newCall(request).execute().use { response -> + val responseBody = response.body?.string() + if (!response.isSuccessful) { + return@withContext ProveResult.Error( + message = "V3 Validate failed: ${response.code} $responseBody", + errorCode = response.code.toString() + ) + } + + val json = JSONObject(responseBody!!) + val verified = json.optBoolean("success", false) + + Log.d(TAG, "Possession check result: success=$verified") + + if (verified) { + return@withContext ProveResult.Success( + correlationId = corrId, + phoneVerified = true, + possessionResult = "verified" + ) + } + + val returnedPhone = json.optString("phoneNumber", "") + Log.w(TAG, "Possession not verified. success=$verified, phoneNumber=${returnedPhone.takeLast(4)}") + return@withContext ProveResult.Error( + message = "Prove possession could not be verified. Cannot proceed with SMS OTP." + ) + } + } catch (e: Exception) { + Log.e(TAG, "Possession check exception", e) + ProveResult.Error(message = e.message ?: "Unknown error occurred") + } + } + + /** + * Clear the current session data. + */ + private fun clearSession() { + correlationId = null + authToken = null + Log.d(TAG, "Session cleared") + } +} \ No newline at end of file From 361cd1c2d7da745b1306c0bca375bacd05211983 Mon Sep 17 00:00:00 2001 From: Toluwalase Cato Date: Fri, 13 Feb 2026 13:09:51 +0000 Subject: [PATCH 6/6] Update fixes --- app/build.gradle | 8 +- app/src/main/AndroidManifest.xml | 9 +- app/src/main/assets/config.json | 2 +- .../ProveManager.kt | 10 +-- .../StrongAuthVerificationContactFragment.kt | 85 +++++++++++++------ 5 files changed, 76 insertions(+), 38 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 48cae60..20f4ef7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -9,7 +9,7 @@ allprojects { name 'Duo-SDK-Feed' } maven { - url 'https://prove.jfrog.io/artifactory/libs-public-maven/' + url = "https://prove.jfrog.io/artifactory/libs-public-maven/" } mavenCentral() google() @@ -34,6 +34,7 @@ android { buildFeatures { viewBinding true } + kotlinOptions { jvmTarget = '1.8' } buildTypes { release { minifyEnabled false @@ -59,9 +60,6 @@ dependencies { implementation 'com.squareup.okhttp3:okhttp:4.9.0' - // Prove Bot Detection SDK - implementation 'com.prove.sdk:proveauth:+' - implementation "androidx.core:core-ktx:$rootProject.ext.coreKtxVersion" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$rootProject.ext.kotlinXCoroutinesVersion" @@ -89,4 +87,6 @@ dependencies { // Downloads and Builds MSAL from maven central. implementation 'com.microsoft.identity.client:msal:[8.1.0,)' } + + implementation 'com.prove.sdk:proveauth:6.9.0' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 55c5396..6bed2de 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,11 +4,17 @@ + + + + + - + diff --git a/app/src/main/assets/config.json b/app/src/main/assets/config.json index a4b4728..e4eb16d 100644 --- a/app/src/main/assets/config.json +++ b/app/src/main/assets/config.json @@ -3,4 +3,4 @@ "clientId": "nativeauthfraudcheck-poc-e7971c6c-c35b-4e17-80f8-99aefdb611c0-1770372962304", "clientSecret": "CgvbgUQpZ95TXZOkYgi3TgOs1MITJj6L" } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/azuresamples/msalnativeauthandroidkotlinsampleapp/ProveManager.kt b/app/src/main/java/com/azuresamples/msalnativeauthandroidkotlinsampleapp/ProveManager.kt index e366630..cfabf34 100644 --- a/app/src/main/java/com/azuresamples/msalnativeauthandroidkotlinsampleapp/ProveManager.kt +++ b/app/src/main/java/com/azuresamples/msalnativeauthandroidkotlinsampleapp/ProveManager.kt @@ -139,11 +139,11 @@ class ProveManager private constructor() { try { val token = fetchAccessToken() - val payload = JSONObject().apply { - put("clientRequestId", UUID.randomUUID().toString()) - put("possessionType", "none") - phoneNumber?.let { put("phoneNumber", it) } - } + val payload = FormBody.Builder() + .add("clientRequestId", UUID.randomUUID().toString()) + .add("possessionType", "none") + .add("phoneNumber", phoneNumber ?: "") + .build() val request = Request.Builder() .url(V3_UNIFY_URL) diff --git a/app/src/main/java/com/azuresamples/msalnativeauthandroidkotlinsampleapp/StrongAuthVerificationContactFragment.kt b/app/src/main/java/com/azuresamples/msalnativeauthandroidkotlinsampleapp/StrongAuthVerificationContactFragment.kt index bb85a2a..856b11a 100644 --- a/app/src/main/java/com/azuresamples/msalnativeauthandroidkotlinsampleapp/StrongAuthVerificationContactFragment.kt +++ b/app/src/main/java/com/azuresamples/msalnativeauthandroidkotlinsampleapp/StrongAuthVerificationContactFragment.kt @@ -2,7 +2,6 @@ package com.azuresamples.msalnativeauthandroidkotlinsampleapp import android.app.AlertDialog import android.os.Bundle -import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -56,38 +55,70 @@ class StrongAuthVerificationContactFragment : Fragment() { private fun verifyContact() { CoroutineScope(Dispatchers.Main).launch { - var optionalEmail = binding.emailText.text.toString() - if (optionalEmail.isBlank()) { - optionalEmail = authMethod.loginHint.orEmpty() - } + binding.verifyCode.isEnabled = false - // Prove Bot Detection Gate for SMS - if (authMethod.challengeChannel.uppercase() == "SMS" && ProveBotDetectionHelper.hasProveKey()) { - val botResult = ProveBotDetectionHelper.performBotDetection(optionalEmail) - when (botResult) { - is ProveBotDetectionHelper.BotDetectionResult.Failed -> { - displayDialog("SMS Fraud Detection", "This phone number has been flagged: ${botResult.reason}") - return@launch - } - is ProveBotDetectionHelper.BotDetectionResult.Error -> { - Log.w("StrongAuth", "Bot detection error: ${botResult.message}") - } - is ProveBotDetectionHelper.BotDetectionResult.Passed -> { - Log.i("StrongAuth", "Bot detection passed for SMS verification.") - } + try { + var contactValue = binding.emailText.text.toString().trim() + if (contactValue.isBlank()) { + contactValue = authMethod.loginHint.orEmpty().trim() } - } - val params = NativeAuthChallengeAuthMethodParameters(authMethod, optionalEmail) - val actionResult = currentState.challengeAuthMethod(params) + if (contactValue.isBlank()) { + displayDialog(getString(R.string.unexpected_sdk_error_title), "A phone number is required for SMS verification") + return@launch + } - when (actionResult) { - is RegisterStrongAuthChallengeResult.VerificationRequired -> { - navigateToStrongAuthChallengeFragment(actionResult.result.getNextState(), actionResult.result.getChannel(), actionResult.result.getSentTo()) + // CHANGE (Prove doc summary #2): for SMS, run Prove customer-supplied possession verification + // BEFORE triggering Microsoft NativeAuth to send the SMS OTP. + if (authMethod.challengeChannel.equals("SMS", ignoreCase = true)) { + val proveManager = ProveManager.getInstance() + + when (val startResult = proveManager.initializeWithAuthToken(phoneNumber = contactValue)) { + is ProveManager.ProveResult.Success -> { + when (val possessionResult = proveManager.checkPossession(startResult.correlationId)) { + is ProveManager.ProveResult.Success -> { + displayDialog( + getString(R.string.yes_message), + possessionResult.phoneVerified.toString() + ) + // Continue to Microsoft SMS OTP (below) + } + is ProveManager.ProveResult.Error -> { + displayDialog( + getString(R.string.unexpected_sdk_error_title), + possessionResult.message + ) + return@launch + } + } + } + is ProveManager.ProveResult.Error -> { + displayDialog( + getString(R.string.unexpected_sdk_error_title), + startResult.message + ) + return@launch + } + } } - is RegisterStrongAuthChallengeError -> { - handleRegisterStrongAuthChallengeError(actionResult) + + val params = NativeAuthChallengeAuthMethodParameters(authMethod, contactValue) + val actionResult = currentState.challengeAuthMethod(params) + + when (actionResult) { + is RegisterStrongAuthChallengeResult.VerificationRequired -> { + navigateToStrongAuthChallengeFragment( + actionResult.result.getNextState(), + actionResult.result.getChannel(), + actionResult.result.getSentTo() + ) + } + is RegisterStrongAuthChallengeError -> { + handleRegisterStrongAuthChallengeError(actionResult) + } } + } finally { + binding.verifyCode.isEnabled = true } } }