From 3ae6005e20db0ef1f0d193815708338d86e37bfa Mon Sep 17 00:00:00 2001 From: ThomazFB Date: Thu, 18 Jan 2024 18:04:58 -0300 Subject: [PATCH 01/33] Define CredentialManagerHandler type --- .../login/webauthn/CredentialManagerHandler.kt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/CredentialManagerHandler.kt diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/CredentialManagerHandler.kt b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/CredentialManagerHandler.kt new file mode 100644 index 0000000000..638450e4cf --- /dev/null +++ b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/CredentialManagerHandler.kt @@ -0,0 +1,14 @@ +package org.wordpress.android.login.webauthn + +import android.content.Context +import android.util.Log +import androidx.annotation.RequiresApi +import androidx.credentials.CredentialManager +import androidx.credentials.GetCredentialRequest +import androidx.credentials.GetCredentialResponse +import androidx.credentials.GetPasswordOption +import androidx.credentials.GetPublicKeyCredentialOption +import androidx.credentials.exceptions.GetCredentialException + +class CredentialManagerHandler { +} From 60ae30546b46cd8727fc7fd798c4f05805dd13d1 Mon Sep 17 00:00:00 2001 From: ThomazFB Date: Thu, 18 Jan 2024 18:05:19 -0300 Subject: [PATCH 02/33] Introduce initial basic Credential Manager implementation --- .../webauthn/CredentialManagerHandler.kt | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/CredentialManagerHandler.kt b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/CredentialManagerHandler.kt index 638450e4cf..228cdbacc2 100644 --- a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/CredentialManagerHandler.kt +++ b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/CredentialManagerHandler.kt @@ -11,4 +11,25 @@ import androidx.credentials.GetPublicKeyCredentialOption import androidx.credentials.exceptions.GetCredentialException class CredentialManagerHandler { + @RequiresApi(34) + private suspend fun CredentialManager.createPasskey( + context: Context, + requestJson: String + ): GetCredentialResponse? { + val password = GetPasswordOption() + val publicKeyCred = GetPublicKeyCredentialOption(requestJson) + val getCredRequest = GetCredentialRequest( + listOf(password, publicKeyCred) + ) + + return try { + getCredential( + request = getCredRequest, + context = context, + ) + } catch (e: GetCredentialException) { + Log.e("Error", e.stackTraceToString()) + null + } + } } From 94f3cc84baae7933ee6b8e44eb526ad72f467249 Mon Sep 17 00:00:00 2001 From: ThomazFB Date: Thu, 18 Jan 2024 18:10:07 -0300 Subject: [PATCH 03/33] Start migration towards Credential Manager implementation without coroutines --- .../webauthn/CredentialManagerHandler.kt | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/CredentialManagerHandler.kt b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/CredentialManagerHandler.kt index 228cdbacc2..e1cfdb4622 100644 --- a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/CredentialManagerHandler.kt +++ b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/CredentialManagerHandler.kt @@ -4,15 +4,20 @@ import android.content.Context import android.util.Log import androidx.annotation.RequiresApi import androidx.credentials.CredentialManager +import androidx.credentials.CredentialManagerCallback import androidx.credentials.GetCredentialRequest import androidx.credentials.GetCredentialResponse import androidx.credentials.GetPasswordOption import androidx.credentials.GetPublicKeyCredentialOption import androidx.credentials.exceptions.GetCredentialException -class CredentialManagerHandler { +class CredentialManagerHandler( + private val context: Context +) { + val credentialManager = CredentialManager.create(context) + @RequiresApi(34) - private suspend fun CredentialManager.createPasskey( + private fun CredentialManager.createPasskey( context: Context, requestJson: String ): GetCredentialResponse? { @@ -23,9 +28,18 @@ class CredentialManagerHandler { ) return try { - getCredential( + getCredentialAsync( request = getCredRequest, context = context, + callback = object : CredentialManagerCallback() { + override fun onError(e: GetCredentialException) { + TODO("Not yet implemented") + } + + override fun onResult(result: GetCredentialResponse) { + TODO("Not yet implemented") + } + } ) } catch (e: GetCredentialException) { Log.e("Error", e.stackTraceToString()) From de1cb362023f33b55585756aae8a97c3bb52f437 Mon Sep 17 00:00:00 2001 From: ThomazFB Date: Thu, 18 Jan 2024 18:16:41 -0300 Subject: [PATCH 04/33] Add async operation structure inside the CredentialManagerHandler --- .../webauthn/CredentialManagerHandler.kt | 28 ++++++++----------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/CredentialManagerHandler.kt b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/CredentialManagerHandler.kt index e1cfdb4622..2091e78c3c 100644 --- a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/CredentialManagerHandler.kt +++ b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/CredentialManagerHandler.kt @@ -1,6 +1,7 @@ package org.wordpress.android.login.webauthn import android.content.Context +import android.os.CancellationSignal import android.util.Log import androidx.annotation.RequiresApi import androidx.credentials.CredentialManager @@ -10,40 +11,33 @@ import androidx.credentials.GetCredentialResponse import androidx.credentials.GetPasswordOption import androidx.credentials.GetPublicKeyCredentialOption import androidx.credentials.exceptions.GetCredentialException +import java.util.concurrent.Executors class CredentialManagerHandler( private val context: Context ) { - val credentialManager = CredentialManager.create(context) + private val credentialManager = CredentialManager.create(context) + private val executor = Executors.newSingleThreadExecutor() @RequiresApi(34) private fun CredentialManager.createPasskey( context: Context, - requestJson: String - ): GetCredentialResponse? { + requestJson: String, + onResult: (Result) -> Unit + ) { val password = GetPasswordOption() val publicKeyCred = GetPublicKeyCredentialOption(requestJson) val getCredRequest = GetCredentialRequest( listOf(password, publicKeyCred) ) - return try { - getCredentialAsync( - request = getCredRequest, - context = context, - callback = object : CredentialManagerCallback() { - override fun onError(e: GetCredentialException) { - TODO("Not yet implemented") - } + val signal = CancellationSignal() + + try { - override fun onResult(result: GetCredentialResponse) { - TODO("Not yet implemented") - } - } - ) } catch (e: GetCredentialException) { Log.e("Error", e.stackTraceToString()) - null + onResult(Result.failure(e)) } } } From a3e4efa1d39c05e8cc0ac03b181f5b99bef8b9cd Mon Sep 17 00:00:00 2001 From: ThomazFB Date: Thu, 18 Jan 2024 18:17:25 -0300 Subject: [PATCH 05/33] Implement expected callback responses for createPasskey function --- .../login/webauthn/CredentialManagerHandler.kt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/CredentialManagerHandler.kt b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/CredentialManagerHandler.kt index 2091e78c3c..fcc78277cd 100644 --- a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/CredentialManagerHandler.kt +++ b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/CredentialManagerHandler.kt @@ -34,7 +34,21 @@ class CredentialManagerHandler( val signal = CancellationSignal() try { + getCredentialAsync( + request = getCredRequest, + context = context, + cancellationSignal = signal, + executor = executor, + callback = object : CredentialManagerCallback { + override fun onError(e: GetCredentialException) { + onResult(Result.failure(e)) + } + override fun onResult(result: GetCredentialResponse) { + onResult(Result.success(result)) + } + } + ) } catch (e: GetCredentialException) { Log.e("Error", e.stackTraceToString()) onResult(Result.failure(e)) From 1df10fff64c1388a44f02dba154c36f7231e12fd Mon Sep 17 00:00:00 2001 From: ThomazFB Date: Thu, 18 Jan 2024 18:17:59 -0300 Subject: [PATCH 06/33] Refactor CredentialManagerHandler function visibilities --- .../android/login/webauthn/CredentialManagerHandler.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/CredentialManagerHandler.kt b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/CredentialManagerHandler.kt index fcc78277cd..318ad3d1fd 100644 --- a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/CredentialManagerHandler.kt +++ b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/CredentialManagerHandler.kt @@ -20,7 +20,7 @@ class CredentialManagerHandler( private val executor = Executors.newSingleThreadExecutor() @RequiresApi(34) - private fun CredentialManager.createPasskey( + fun fetchPasskey( context: Context, requestJson: String, onResult: (Result) -> Unit @@ -34,7 +34,7 @@ class CredentialManagerHandler( val signal = CancellationSignal() try { - getCredentialAsync( + credentialManager.getCredentialAsync( request = getCredRequest, context = context, cancellationSignal = signal, From d84a00526cf2c9abcdfac8d6657b1486cccc60b8 Mon Sep 17 00:00:00 2001 From: ThomazFB Date: Mon, 22 Jan 2024 22:06:18 -0300 Subject: [PATCH 07/33] Rename PasskeyCredentialsHandler.kt to Fido2ClientHandler --- .../{PasskeyCredentialsHandler.kt => Fido2ClientHandler.kt} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/{PasskeyCredentialsHandler.kt => Fido2ClientHandler.kt} (98%) diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/PasskeyCredentialsHandler.kt b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/Fido2ClientHandler.kt similarity index 98% rename from WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/PasskeyCredentialsHandler.kt rename to WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/Fido2ClientHandler.kt index ad92a7dcd8..e19779e5ec 100644 --- a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/PasskeyCredentialsHandler.kt +++ b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/Fido2ClientHandler.kt @@ -17,7 +17,7 @@ interface OnPasskeyRequestReadyListener { fun onPasskeyRequestReady(intentSenderRequest: IntentSenderRequest) } -class PasskeyCredentialsHandler( +class Fido2ClientHandler( private val userId: String, private val challengeInfo: WebauthnChallengeInfo ) { From a30c603954447122b6ca2a8855585fa19ff5f956 Mon Sep 17 00:00:00 2001 From: ThomazFB Date: Mon, 22 Jan 2024 22:07:20 -0300 Subject: [PATCH 08/33] Adjust Login2FaFragment to correctly reference the FIDO2 handler --- .../wordpress/android/login/Login2FaFragment.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/Login2FaFragment.java b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/Login2FaFragment.java index 64c003e9ac..b541c2845d 100644 --- a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/Login2FaFragment.java +++ b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/Login2FaFragment.java @@ -46,7 +46,7 @@ import org.wordpress.android.fluxc.store.AccountStore.WebauthnChallengeReceived; import org.wordpress.android.fluxc.store.AccountStore.WebauthnPasskeyAuthenticated; import org.wordpress.android.login.util.SiteUtils; -import org.wordpress.android.login.webauthn.PasskeyCredentialsHandler; +import org.wordpress.android.login.webauthn.Fido2ClientHandler; import org.wordpress.android.login.widgets.WPLoginInputRow; import org.wordpress.android.login.widgets.WPLoginInputRow.OnEditorCommitListener; import org.wordpress.android.util.AppLog; @@ -125,7 +125,7 @@ public class Login2FaFragment extends LoginBaseFormFragment imple private boolean mIsSocialLoginConnect; private boolean mSentSmsCode; private List mSupportedAuthTypes; - @Nullable private PasskeyCredentialsHandler mPasskeyCredentialsHandler = null; + @Nullable private Fido2ClientHandler mFido2ClientHandler = null; @Nullable private ActivityResultLauncher mResultLauncher = null; public static Login2FaFragment newInstance(String emailAddress, String password) { @@ -628,11 +628,11 @@ public void onWebauthnChallengeReceived(WebauthnChallengeReceived event) { handleAuthError(event.error.type, getString(R.string.login_error_security_key)); return; } - mPasskeyCredentialsHandler = new PasskeyCredentialsHandler( + mFido2ClientHandler = new Fido2ClientHandler( event.mUserId, event.mChallengeInfo ); - mPasskeyCredentialsHandler.createIntentSender( + mFido2ClientHandler.createIntentSender( requireContext(), intent -> { if (mResultLauncher != null) { @@ -644,7 +644,7 @@ public void onWebauthnChallengeReceived(WebauthnChallengeReceived event) { private void onCredentialsResultAvailable(@NonNull Intent resultData) { if (resultData.hasExtra(Fido.FIDO2_KEY_CREDENTIAL_EXTRA)) { byte[] credentialBytes = resultData.getByteArrayExtra(Fido.FIDO2_KEY_CREDENTIAL_EXTRA); - if (credentialBytes == null || mPasskeyCredentialsHandler == null) { + if (credentialBytes == null || mFido2ClientHandler == null) { handleWebauthnError(); return; } @@ -652,7 +652,7 @@ private void onCredentialsResultAvailable(@NonNull Intent resultData) { PublicKeyCredential credentials = PublicKeyCredential.deserializeFromBytes(credentialBytes); FinishWebauthnChallengePayload payload = - mPasskeyCredentialsHandler.onCredentialsAvailable(credentials); + mFido2ClientHandler.onCredentialsAvailable(credentials); mDispatcher.dispatch( AuthenticationActionBuilder.newFinishSecurityKeyChallengeAction(payload)); } From be0f8ae635ac8a848a6895734364d59f470f4c57 Mon Sep 17 00:00:00 2001 From: ThomazFB Date: Mon, 22 Jan 2024 22:07:27 -0300 Subject: [PATCH 09/33] Introduce PasskeyHandler --- .../android/login/webauthn/PasskeyHandler.kt | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/PasskeyHandler.kt diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/PasskeyHandler.kt b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/PasskeyHandler.kt new file mode 100644 index 0000000000..4b25b25a32 --- /dev/null +++ b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/PasskeyHandler.kt @@ -0,0 +1,25 @@ +package org.wordpress.android.login.webauthn + +import android.app.Activity +import androidx.activity.result.ActivityResult +import androidx.activity.result.contract.ActivityResultContracts.StartIntentSenderForResult +import androidx.fragment.app.Fragment + +class PasskeyHandler { + fun fetch(requestingFragment: Fragment, onResult: (PasskeyDataResult) -> Unit) { + requestingFragment.registerForActivityResult( + StartIntentSenderForResult() + ) + { result: ActivityResult -> + if (result.resultCode == Activity.RESULT_OK && result.data != null) { + onResult(PasskeyDataResult(isFailure = false)) + } else { + onResult(PasskeyDataResult(isFailure = true)) + } + } + } + + data class PasskeyDataResult( + val isFailure: Boolean + ) +} From 8f5360708dc9dad55c9cb64bb2b8267556312762 Mon Sep 17 00:00:00 2001 From: ThomazFB Date: Mon, 22 Jan 2024 22:20:27 -0300 Subject: [PATCH 10/33] Add FIDO2 handling inside PasskeyHandler --- .../android/login/webauthn/PasskeyHandler.kt | 33 +++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/PasskeyHandler.kt b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/PasskeyHandler.kt index 4b25b25a32..d0c3fa7081 100644 --- a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/PasskeyHandler.kt +++ b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/PasskeyHandler.kt @@ -1,12 +1,21 @@ package org.wordpress.android.login.webauthn import android.app.Activity +import android.content.Intent import androidx.activity.result.ActivityResult import androidx.activity.result.contract.ActivityResultContracts.StartIntentSenderForResult import androidx.fragment.app.Fragment +import com.google.android.gms.fido.Fido +import com.google.android.gms.fido.fido2.api.common.PublicKeyCredential +import org.wordpress.android.fluxc.network.rest.wpcom.auth.webauthn.WebauthnChallengeInfo class PasskeyHandler { - fun fetch(requestingFragment: Fragment, onResult: (PasskeyDataResult) -> Unit) { + fun fetch( + requestingFragment: Fragment, + userId: String, + challengeInfo: WebauthnChallengeInfo, + onResult: (PasskeyDataResult) -> Unit + ) { requestingFragment.registerForActivityResult( StartIntentSenderForResult() ) @@ -19,7 +28,27 @@ class PasskeyHandler { } } + private fun parseFIDO2IntentData( + resultData: Intent, + userId: String, + challengeInfo: WebauthnChallengeInfo + ): PasskeyDataResult? = + resultData.takeIf { it.hasExtra(Fido.FIDO2_KEY_CREDENTIAL_EXTRA) } + ?.getByteArrayExtra(Fido.FIDO2_KEY_CREDENTIAL_EXTRA) + ?.let { PublicKeyCredential.deserializeFromBytes(it).toJson() } + ?.let { clientData -> + PasskeyDataResult( + isFailure = false, + userId = userId, + twoStepNonce = challengeInfo.twoStepNonce, + clientData = clientData + ) + } + data class PasskeyDataResult( - val isFailure: Boolean + val isFailure: Boolean, + val userId: String, + val twoStepNonce: String, + val clientData: String ) } From 8b67694f42e872e8d546dc12c3f29a5e0920b8cc Mon Sep 17 00:00:00 2001 From: ThomazFB Date: Mon, 22 Jan 2024 22:20:57 -0300 Subject: [PATCH 11/33] Add nullability to PasskeyDataResult --- .../org/wordpress/android/login/webauthn/PasskeyHandler.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/PasskeyHandler.kt b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/PasskeyHandler.kt index d0c3fa7081..7fd7ed784d 100644 --- a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/PasskeyHandler.kt +++ b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/PasskeyHandler.kt @@ -47,8 +47,8 @@ class PasskeyHandler { data class PasskeyDataResult( val isFailure: Boolean, - val userId: String, - val twoStepNonce: String, - val clientData: String + val userId: String? = null, + val twoStepNonce: String? = null, + val clientData: String? = null ) } From 783d171dd3f755184717ea48b70ee689f845a8c0 Mon Sep 17 00:00:00 2001 From: ThomazFB Date: Mon, 22 Jan 2024 22:22:29 -0300 Subject: [PATCH 12/33] Finish FIDO2 basic handling in PasskeyHandler --- .../android/login/webauthn/PasskeyHandler.kt | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/PasskeyHandler.kt b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/PasskeyHandler.kt index 7fd7ed784d..8636626683 100644 --- a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/PasskeyHandler.kt +++ b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/PasskeyHandler.kt @@ -21,7 +21,7 @@ class PasskeyHandler { ) { result: ActivityResult -> if (result.resultCode == Activity.RESULT_OK && result.data != null) { - onResult(PasskeyDataResult(isFailure = false)) + onResult(parseFIDO2IntentData(result.data, userId, challengeInfo)) } else { onResult(PasskeyDataResult(isFailure = true)) } @@ -29,11 +29,12 @@ class PasskeyHandler { } private fun parseFIDO2IntentData( - resultData: Intent, + resultData: Intent?, userId: String, challengeInfo: WebauthnChallengeInfo - ): PasskeyDataResult? = - resultData.takeIf { it.hasExtra(Fido.FIDO2_KEY_CREDENTIAL_EXTRA) } + ): PasskeyDataResult = + resultData + ?.takeIf { it.hasExtra(Fido.FIDO2_KEY_CREDENTIAL_EXTRA) } ?.getByteArrayExtra(Fido.FIDO2_KEY_CREDENTIAL_EXTRA) ?.let { PublicKeyCredential.deserializeFromBytes(it).toJson() } ?.let { clientData -> @@ -43,7 +44,7 @@ class PasskeyHandler { twoStepNonce = challengeInfo.twoStepNonce, clientData = clientData ) - } + } ?: PasskeyDataResult(isFailure = true) data class PasskeyDataResult( val isFailure: Boolean, From 2622a56c5e82af08ae9e59ab16c1c077e6ce3a0b Mon Sep 17 00:00:00 2001 From: ThomazFB Date: Tue, 23 Jan 2024 15:27:28 -0300 Subject: [PATCH 13/33] Correctly handle credential results from Credential Manager --- .../login/webauthn/CredentialManagerHandler.kt | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/CredentialManagerHandler.kt b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/CredentialManagerHandler.kt index 318ad3d1fd..dfbb8b827e 100644 --- a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/CredentialManagerHandler.kt +++ b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/CredentialManagerHandler.kt @@ -11,6 +11,7 @@ import androidx.credentials.GetCredentialResponse import androidx.credentials.GetPasswordOption import androidx.credentials.GetPublicKeyCredentialOption import androidx.credentials.exceptions.GetCredentialException +import org.wordpress.android.fluxc.store.AccountStore.FinishWebauthnChallengePayload import java.util.concurrent.Executors class CredentialManagerHandler( @@ -21,9 +22,10 @@ class CredentialManagerHandler( @RequiresApi(34) fun fetchPasskey( - context: Context, + userId: String, + twoStepNonce: String, requestJson: String, - onResult: (Result) -> Unit + onResult: (Result) -> Unit ) { val password = GetPasswordOption() val publicKeyCred = GetPublicKeyCredentialOption(requestJson) @@ -45,7 +47,11 @@ class CredentialManagerHandler( } override fun onResult(result: GetCredentialResponse) { - onResult(Result.success(result)) + FinishWebauthnChallengePayload().apply { + mUserId = userId + mTwoStepNonce = twoStepNonce + mClientData = result.toString() + }.let { onResult(Result.success(it)) } } } ) From df492ebd9070e28dbdf947ca18512be0614f949b Mon Sep 17 00:00:00 2001 From: ThomazFB Date: Tue, 23 Jan 2024 15:47:27 -0300 Subject: [PATCH 14/33] Parse result from CredentialManager --- .../login/webauthn/CredentialManagerHandler.kt | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/CredentialManagerHandler.kt b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/CredentialManagerHandler.kt index dfbb8b827e..71bf94149e 100644 --- a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/CredentialManagerHandler.kt +++ b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/CredentialManagerHandler.kt @@ -10,6 +10,7 @@ import androidx.credentials.GetCredentialRequest import androidx.credentials.GetCredentialResponse import androidx.credentials.GetPasswordOption import androidx.credentials.GetPublicKeyCredentialOption +import androidx.credentials.PublicKeyCredential import androidx.credentials.exceptions.GetCredentialException import org.wordpress.android.fluxc.store.AccountStore.FinishWebauthnChallengePayload import java.util.concurrent.Executors @@ -50,7 +51,7 @@ class CredentialManagerHandler( FinishWebauthnChallengePayload().apply { mUserId = userId mTwoStepNonce = twoStepNonce - mClientData = result.toString() + mClientData = result.toJson().orEmpty() }.let { onResult(Result.success(it)) } } } @@ -60,4 +61,14 @@ class CredentialManagerHandler( onResult(Result.failure(e)) } } + + private fun GetCredentialResponse.toJson(): String? { + return when (val credential = this.credential) { + is PublicKeyCredential -> credential.authenticationResponseJson + else -> { + Log.e("Credential Manager", "Unexpected type of credential") + null + } + } + } } From 420e2f059895ec9e0572d30f43ed39a7d67d22c0 Mon Sep 17 00:00:00 2001 From: ThomazFB Date: Tue, 23 Jan 2024 15:53:37 -0300 Subject: [PATCH 15/33] Adjust CredentialManagerHandler to properly connect with Login2FaFragment --- .../android/login/Login2FaFragment.java | 39 ++++++++++++++----- .../webauthn/CredentialManagerHandler.kt | 9 +++-- 2 files changed, 34 insertions(+), 14 deletions(-) diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/Login2FaFragment.java b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/Login2FaFragment.java index b541c2845d..3bee4956ff 100644 --- a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/Login2FaFragment.java +++ b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/Login2FaFragment.java @@ -3,6 +3,8 @@ import android.content.ClipboardManager; import android.content.Context; import android.content.Intent; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; import android.os.Bundle; import android.text.Editable; import android.text.TextUtils; @@ -46,6 +48,7 @@ import org.wordpress.android.fluxc.store.AccountStore.WebauthnChallengeReceived; import org.wordpress.android.fluxc.store.AccountStore.WebauthnPasskeyAuthenticated; import org.wordpress.android.login.util.SiteUtils; +import org.wordpress.android.login.webauthn.CredentialManagerHandler; import org.wordpress.android.login.webauthn.Fido2ClientHandler; import org.wordpress.android.login.widgets.WPLoginInputRow; import org.wordpress.android.login.widgets.WPLoginInputRow.OnEditorCommitListener; @@ -628,17 +631,33 @@ public void onWebauthnChallengeReceived(WebauthnChallengeReceived event) { handleAuthError(event.error.type, getString(R.string.login_error_security_key)); return; } - mFido2ClientHandler = new Fido2ClientHandler( - event.mUserId, - event.mChallengeInfo - ); - mFido2ClientHandler.createIntentSender( - requireContext(), - intent -> { - if (mResultLauncher != null) { - mResultLauncher.launch(intent); +// mFido2ClientHandler = new Fido2ClientHandler( +// event.mUserId, +// event.mChallengeInfo +// ); +// mFido2ClientHandler.createIntentSender( +// requireContext(), +// intent -> { +// if (mResultLauncher != null) { +// mResultLauncher.launch(intent); +// } +// }); + CredentialManagerHandler handler = new CredentialManagerHandler(requireContext()); + if (VERSION.SDK_INT >= VERSION_CODES.UPSIDE_DOWN_CAKE) { + handler.fetchPasskey( + event.mUserId, + event.mChallengeInfo.getTwoStepNonce(), + event.mChallengeInfo.getChallenge(), + result -> { + FinishWebauthnChallengePayload payload = result; + return null; + }, + error -> { + handleWebauthnError(); + return null; } - }); + ); + } } private void onCredentialsResultAvailable(@NonNull Intent resultData) { diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/CredentialManagerHandler.kt b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/CredentialManagerHandler.kt index 71bf94149e..70871e330b 100644 --- a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/CredentialManagerHandler.kt +++ b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/CredentialManagerHandler.kt @@ -26,7 +26,8 @@ class CredentialManagerHandler( userId: String, twoStepNonce: String, requestJson: String, - onResult: (Result) -> Unit + onSuccess: (FinishWebauthnChallengePayload) -> Unit, + onFailure: (Throwable) -> Unit ) { val password = GetPasswordOption() val publicKeyCred = GetPublicKeyCredentialOption(requestJson) @@ -44,7 +45,7 @@ class CredentialManagerHandler( executor = executor, callback = object : CredentialManagerCallback { override fun onError(e: GetCredentialException) { - onResult(Result.failure(e)) + onFailure(e) } override fun onResult(result: GetCredentialResponse) { @@ -52,13 +53,13 @@ class CredentialManagerHandler( mUserId = userId mTwoStepNonce = twoStepNonce mClientData = result.toJson().orEmpty() - }.let { onResult(Result.success(it)) } + }.let { onSuccess(it) } } } ) } catch (e: GetCredentialException) { Log.e("Error", e.stackTraceToString()) - onResult(Result.failure(e)) + onFailure(e) } } From d8dd6c4a12d68fb901fece9c857c1e1b51825e48 Mon Sep 17 00:00:00 2001 From: ThomazFB Date: Wed, 24 Jan 2024 18:25:03 -0300 Subject: [PATCH 16/33] Add API version check to PasskeyHandler --- .../wordpress/android/login/webauthn/PasskeyHandler.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/PasskeyHandler.kt b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/PasskeyHandler.kt index 8636626683..4dbfe16e74 100644 --- a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/PasskeyHandler.kt +++ b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/PasskeyHandler.kt @@ -10,12 +10,18 @@ import com.google.android.gms.fido.fido2.api.common.PublicKeyCredential import org.wordpress.android.fluxc.network.rest.wpcom.auth.webauthn.WebauthnChallengeInfo class PasskeyHandler { - fun fetch( + fun onFragmentCreation( requestingFragment: Fragment, userId: String, challengeInfo: WebauthnChallengeInfo, onResult: (PasskeyDataResult) -> Unit ) { + // run only if API is 34 or above + if (android.os.Build.VERSION.SDK_INT >= 34) { + onResult(PasskeyDataResult(isFailure = true)) + return + } + requestingFragment.registerForActivityResult( StartIntentSenderForResult() ) From 6bdd91d625e1c9d5488b88970ed0879134aa8402 Mon Sep 17 00:00:00 2001 From: ThomazFB Date: Wed, 24 Jan 2024 18:25:17 -0300 Subject: [PATCH 17/33] Dispatch challenge payload after interception --- .../main/java/org/wordpress/android/login/Login2FaFragment.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/Login2FaFragment.java b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/Login2FaFragment.java index 3bee4956ff..db104f4289 100644 --- a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/Login2FaFragment.java +++ b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/Login2FaFragment.java @@ -650,6 +650,8 @@ public void onWebauthnChallengeReceived(WebauthnChallengeReceived event) { event.mChallengeInfo.getChallenge(), result -> { FinishWebauthnChallengePayload payload = result; + mDispatcher.dispatch( + AuthenticationActionBuilder.newFinishSecurityKeyChallengeAction(payload)); return null; }, error -> { From 4f5e31ea74b4237215f88595853b86ecab72a6ad Mon Sep 17 00:00:00 2001 From: ThomazFB Date: Fri, 26 Jan 2024 13:36:08 -0300 Subject: [PATCH 18/33] Force Credential Manager to run on lower API levels --- .../android/login/Login2FaFragment.java | 35 +++++++++---------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/Login2FaFragment.java b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/Login2FaFragment.java index db104f4289..ce14823335 100644 --- a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/Login2FaFragment.java +++ b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/Login2FaFragment.java @@ -1,5 +1,6 @@ package org.wordpress.android.login; +import android.annotation.SuppressLint; import android.content.ClipboardManager; import android.content.Context; import android.content.Intent; @@ -623,7 +624,7 @@ private void doAuthWithSecurityKeyAction() { .newStartSecurityKeyChallengeAction(payload)); } - @SuppressWarnings("unused") + @SuppressLint("NewApi") @SuppressWarnings("unused") @Subscribe(threadMode = ThreadMode.MAIN) public void onWebauthnChallengeReceived(WebauthnChallengeReceived event) { if (event.isError()) { @@ -643,23 +644,21 @@ public void onWebauthnChallengeReceived(WebauthnChallengeReceived event) { // } // }); CredentialManagerHandler handler = new CredentialManagerHandler(requireContext()); - if (VERSION.SDK_INT >= VERSION_CODES.UPSIDE_DOWN_CAKE) { - handler.fetchPasskey( - event.mUserId, - event.mChallengeInfo.getTwoStepNonce(), - event.mChallengeInfo.getChallenge(), - result -> { - FinishWebauthnChallengePayload payload = result; - mDispatcher.dispatch( - AuthenticationActionBuilder.newFinishSecurityKeyChallengeAction(payload)); - return null; - }, - error -> { - handleWebauthnError(); - return null; - } - ); - } + handler.fetchPasskey( + event.mUserId, + event.mChallengeInfo.getTwoStepNonce(), + event.mChallengeInfo.getChallenge(), + result -> { + FinishWebauthnChallengePayload payload = result; + mDispatcher.dispatch( + AuthenticationActionBuilder.newFinishSecurityKeyChallengeAction(payload)); + return null; + }, + error -> { + handleWebauthnError(); + return null; + } + ); } private void onCredentialsResultAvailable(@NonNull Intent resultData) { From ae7764e19fa46b7213d7d80be0edaf7c7bf76a59 Mon Sep 17 00:00:00 2001 From: ThomazFB Date: Fri, 26 Jan 2024 13:36:59 -0300 Subject: [PATCH 19/33] Adjust unnecessary API check in CredentialManagerHandler --- .../main/java/org/wordpress/android/login/Login2FaFragment.java | 1 - .../wordpress/android/login/webauthn/CredentialManagerHandler.kt | 1 - 2 files changed, 2 deletions(-) diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/Login2FaFragment.java b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/Login2FaFragment.java index ce14823335..293b921de5 100644 --- a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/Login2FaFragment.java +++ b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/Login2FaFragment.java @@ -624,7 +624,6 @@ private void doAuthWithSecurityKeyAction() { .newStartSecurityKeyChallengeAction(payload)); } - @SuppressLint("NewApi") @SuppressWarnings("unused") @Subscribe(threadMode = ThreadMode.MAIN) public void onWebauthnChallengeReceived(WebauthnChallengeReceived event) { if (event.isError()) { diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/CredentialManagerHandler.kt b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/CredentialManagerHandler.kt index 70871e330b..c3d916a5d7 100644 --- a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/CredentialManagerHandler.kt +++ b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/CredentialManagerHandler.kt @@ -21,7 +21,6 @@ class CredentialManagerHandler( private val credentialManager = CredentialManager.create(context) private val executor = Executors.newSingleThreadExecutor() - @RequiresApi(34) fun fetchPasskey( userId: String, twoStepNonce: String, From bc86ad18c9c9f870eaf67881844938262cf2a9f0 Mon Sep 17 00:00:00 2001 From: ThomazFB Date: Fri, 26 Jan 2024 13:37:24 -0300 Subject: [PATCH 20/33] Refactor redundant code --- .../java/org/wordpress/android/login/Login2FaFragment.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/Login2FaFragment.java b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/Login2FaFragment.java index 293b921de5..7c5eac0ea3 100644 --- a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/Login2FaFragment.java +++ b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/Login2FaFragment.java @@ -648,9 +648,8 @@ public void onWebauthnChallengeReceived(WebauthnChallengeReceived event) { event.mChallengeInfo.getTwoStepNonce(), event.mChallengeInfo.getChallenge(), result -> { - FinishWebauthnChallengePayload payload = result; mDispatcher.dispatch( - AuthenticationActionBuilder.newFinishSecurityKeyChallengeAction(payload)); + AuthenticationActionBuilder.newFinishSecurityKeyChallengeAction(result)); return null; }, error -> { From bc6316ebd3543757b5de2a355479b24a5fc3eb90 Mon Sep 17 00:00:00 2001 From: ThomazFB Date: Fri, 26 Jan 2024 14:21:59 -0300 Subject: [PATCH 21/33] Fix lint issues --- .../java/org/wordpress/android/login/Login2FaFragment.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/Login2FaFragment.java b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/Login2FaFragment.java index 7c5eac0ea3..668bae03ba 100644 --- a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/Login2FaFragment.java +++ b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/Login2FaFragment.java @@ -1,11 +1,8 @@ package org.wordpress.android.login; -import android.annotation.SuppressLint; import android.content.ClipboardManager; import android.content.Context; import android.content.Intent; -import android.os.Build.VERSION; -import android.os.Build.VERSION_CODES; import android.os.Bundle; import android.text.Editable; import android.text.TextUtils; From 5f23f01b128aa815563dfd0a3127286435880704 Mon Sep 17 00:00:00 2001 From: ThomazFB Date: Sun, 28 Jan 2024 20:29:21 -0300 Subject: [PATCH 22/33] Update FluxC version --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index e76e21941e..d973ba5bd6 100644 --- a/build.gradle +++ b/build.gradle @@ -15,7 +15,7 @@ ext { // libs wordpressLintVersion = '2.0.0' wordpressUtilsVersion = '3.5.0' - wordpressFluxCVersion = '2.57.0' + wordpressFluxCVersion = '2949-e180ead348373382d73e2f3d06a19c2709073e84' // main androidxAppCompatVersion = '1.6.1' From 5609ded0a47ae189d35c3f5dab97c328a0889be8 Mon Sep 17 00:00:00 2001 From: ThomazFB Date: Sun, 28 Jan 2024 20:30:06 -0300 Subject: [PATCH 23/33] Update Credential Manager with raw JSON data from challenge --- .../main/java/org/wordpress/android/login/Login2FaFragment.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/Login2FaFragment.java b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/Login2FaFragment.java index 668bae03ba..ad9e55f81a 100644 --- a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/Login2FaFragment.java +++ b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/Login2FaFragment.java @@ -643,7 +643,7 @@ public void onWebauthnChallengeReceived(WebauthnChallengeReceived event) { handler.fetchPasskey( event.mUserId, event.mChallengeInfo.getTwoStepNonce(), - event.mChallengeInfo.getChallenge(), + event.mRawChallengeInfoJson, result -> { mDispatcher.dispatch( AuthenticationActionBuilder.newFinishSecurityKeyChallengeAction(result)); From 2ca49f533c052549869335fe7ef77b9d981fabac Mon Sep 17 00:00:00 2001 From: ThomazFB Date: Sun, 28 Jan 2024 22:14:43 -0300 Subject: [PATCH 24/33] Add logs for errors coming from Credential the Manager API --- .../android/login/webauthn/CredentialManagerHandler.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/CredentialManagerHandler.kt b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/CredentialManagerHandler.kt index c3d916a5d7..ed37104be8 100644 --- a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/CredentialManagerHandler.kt +++ b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/CredentialManagerHandler.kt @@ -3,7 +3,6 @@ package org.wordpress.android.login.webauthn import android.content.Context import android.os.CancellationSignal import android.util.Log -import androidx.annotation.RequiresApi import androidx.credentials.CredentialManager import androidx.credentials.CredentialManagerCallback import androidx.credentials.GetCredentialRequest @@ -45,6 +44,7 @@ class CredentialManagerHandler( callback = object : CredentialManagerCallback { override fun onError(e: GetCredentialException) { onFailure(e) + Log.e("Credential Manager error", e.stackTraceToString()) } override fun onResult(result: GetCredentialResponse) { @@ -57,7 +57,7 @@ class CredentialManagerHandler( } ) } catch (e: GetCredentialException) { - Log.e("Error", e.stackTraceToString()) + Log.e("Credential Manager error", e.stackTraceToString()) onFailure(e) } } From 8e725776c88f15d4f5f0d2fd6b5a232dad26dfa5 Mon Sep 17 00:00:00 2001 From: ThomazFB Date: Tue, 30 Jan 2024 12:02:44 -0300 Subject: [PATCH 25/33] Refactor CredentialManagerHandler.kt to PasskeyRequest --- .../android/login/Login2FaFragment.java | 23 ++++++----------- ...ialManagerHandler.kt => PasskeyRequest.kt} | 25 ++++++++----------- 2 files changed, 18 insertions(+), 30 deletions(-) rename WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/{CredentialManagerHandler.kt => PasskeyRequest.kt} (85%) diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/Login2FaFragment.java b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/Login2FaFragment.java index ad9e55f81a..9ce2fb3bd8 100644 --- a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/Login2FaFragment.java +++ b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/Login2FaFragment.java @@ -46,7 +46,7 @@ import org.wordpress.android.fluxc.store.AccountStore.WebauthnChallengeReceived; import org.wordpress.android.fluxc.store.AccountStore.WebauthnPasskeyAuthenticated; import org.wordpress.android.login.util.SiteUtils; -import org.wordpress.android.login.webauthn.CredentialManagerHandler; +import org.wordpress.android.login.webauthn.PasskeyRequest; import org.wordpress.android.login.webauthn.Fido2ClientHandler; import org.wordpress.android.login.widgets.WPLoginInputRow; import org.wordpress.android.login.widgets.WPLoginInputRow.OnEditorCommitListener; @@ -628,32 +628,23 @@ public void onWebauthnChallengeReceived(WebauthnChallengeReceived event) { handleAuthError(event.error.type, getString(R.string.login_error_security_key)); return; } -// mFido2ClientHandler = new Fido2ClientHandler( -// event.mUserId, -// event.mChallengeInfo -// ); -// mFido2ClientHandler.createIntentSender( -// requireContext(), -// intent -> { -// if (mResultLauncher != null) { -// mResultLauncher.launch(intent); -// } -// }); - CredentialManagerHandler handler = new CredentialManagerHandler(requireContext()); - handler.fetchPasskey( + + new PasskeyRequest( + requireContext(), event.mUserId, event.mChallengeInfo.getTwoStepNonce(), event.mRawChallengeInfoJson, result -> { mDispatcher.dispatch( - AuthenticationActionBuilder.newFinishSecurityKeyChallengeAction(result)); + AuthenticationActionBuilder.newFinishSecurityKeyChallengeAction( + result)); return null; }, error -> { handleWebauthnError(); return null; } - ); + ); } private void onCredentialsResultAvailable(@NonNull Intent resultData) { diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/CredentialManagerHandler.kt b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/PasskeyRequest.kt similarity index 85% rename from WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/CredentialManagerHandler.kt rename to WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/PasskeyRequest.kt index ed37104be8..9244e7af3c 100644 --- a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/CredentialManagerHandler.kt +++ b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/PasskeyRequest.kt @@ -14,27 +14,24 @@ import androidx.credentials.exceptions.GetCredentialException import org.wordpress.android.fluxc.store.AccountStore.FinishWebauthnChallengePayload import java.util.concurrent.Executors -class CredentialManagerHandler( - private val context: Context +class PasskeyRequest( + context: Context, + userId: String, + twoStepNonce: String, + requestJson: String, + onSuccess: (FinishWebauthnChallengePayload) -> Unit, + onFailure: (Throwable) -> Unit ) { - private val credentialManager = CredentialManager.create(context) - private val executor = Executors.newSingleThreadExecutor() - - fun fetchPasskey( - userId: String, - twoStepNonce: String, - requestJson: String, - onSuccess: (FinishWebauthnChallengePayload) -> Unit, - onFailure: (Throwable) -> Unit - ) { + init { + val credentialManager = CredentialManager.create(context) + val executor = Executors.newSingleThreadExecutor() + val signal = CancellationSignal() val password = GetPasswordOption() val publicKeyCred = GetPublicKeyCredentialOption(requestJson) val getCredRequest = GetCredentialRequest( listOf(password, publicKeyCred) ) - val signal = CancellationSignal() - try { credentialManager.getCredentialAsync( request = getCredRequest, From 79873127b8b3010da4ed48f7f1842ab42efdf498 Mon Sep 17 00:00:00 2001 From: ThomazFB Date: Tue, 30 Jan 2024 12:30:37 -0300 Subject: [PATCH 26/33] Refactor PasskeyRequest excessive parameters --- .../android/login/Login2FaFragment.java | 4 +- .../android/login/webauthn/PasskeyRequest.kt | 42 +++++++++---------- 2 files changed, 21 insertions(+), 25 deletions(-) diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/Login2FaFragment.java b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/Login2FaFragment.java index 9ce2fb3bd8..d1e0231f9e 100644 --- a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/Login2FaFragment.java +++ b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/Login2FaFragment.java @@ -631,9 +631,7 @@ public void onWebauthnChallengeReceived(WebauthnChallengeReceived event) { new PasskeyRequest( requireContext(), - event.mUserId, - event.mChallengeInfo.getTwoStepNonce(), - event.mRawChallengeInfoJson, + event, result -> { mDispatcher.dispatch( AuthenticationActionBuilder.newFinishSecurityKeyChallengeAction( diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/PasskeyRequest.kt b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/PasskeyRequest.kt index 9244e7af3c..63e91bfef4 100644 --- a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/PasskeyRequest.kt +++ b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/PasskeyRequest.kt @@ -12,46 +12,44 @@ import androidx.credentials.GetPublicKeyCredentialOption import androidx.credentials.PublicKeyCredential import androidx.credentials.exceptions.GetCredentialException import org.wordpress.android.fluxc.store.AccountStore.FinishWebauthnChallengePayload +import org.wordpress.android.fluxc.store.AccountStore.WebauthnChallengeReceived import java.util.concurrent.Executors class PasskeyRequest( context: Context, - userId: String, - twoStepNonce: String, - requestJson: String, + challengeEvent: WebauthnChallengeReceived, onSuccess: (FinishWebauthnChallengePayload) -> Unit, onFailure: (Throwable) -> Unit ) { init { - val credentialManager = CredentialManager.create(context) val executor = Executors.newSingleThreadExecutor() val signal = CancellationSignal() - val password = GetPasswordOption() - val publicKeyCred = GetPublicKeyCredentialOption(requestJson) val getCredRequest = GetCredentialRequest( - listOf(password, publicKeyCred) + listOf(GetPasswordOption(), GetPublicKeyCredentialOption(challengeEvent.mRawChallengeInfoJson)) ) + val passkeyRequestCallback = object : CredentialManagerCallback { + override fun onError(e: GetCredentialException) { + onFailure(e) + Log.e("Credential Manager error", e.stackTraceToString()) + } + + override fun onResult(result: GetCredentialResponse) { + FinishWebauthnChallengePayload().apply { + mUserId = challengeEvent.mUserId + mTwoStepNonce = challengeEvent.mChallengeInfo.twoStepNonce + mClientData = result.toJson().orEmpty() + }.let { onSuccess(it) } + } + } + try { - credentialManager.getCredentialAsync( + CredentialManager.create(context).getCredentialAsync( request = getCredRequest, context = context, cancellationSignal = signal, executor = executor, - callback = object : CredentialManagerCallback { - override fun onError(e: GetCredentialException) { - onFailure(e) - Log.e("Credential Manager error", e.stackTraceToString()) - } - - override fun onResult(result: GetCredentialResponse) { - FinishWebauthnChallengePayload().apply { - mUserId = userId - mTwoStepNonce = twoStepNonce - mClientData = result.toJson().orEmpty() - }.let { onSuccess(it) } - } - } + callback = passkeyRequestCallback ) } catch (e: GetCredentialException) { Log.e("Credential Manager error", e.stackTraceToString()) From dfea9d086bae29a0040f6178bdddcf884c03d4c6 Mon Sep 17 00:00:00 2001 From: ThomazFB Date: Tue, 30 Jan 2024 12:39:27 -0300 Subject: [PATCH 27/33] Add log tag into PasskeyRequest --- .../wordpress/android/login/webauthn/PasskeyRequest.kt | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/PasskeyRequest.kt b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/PasskeyRequest.kt index 63e91bfef4..f812554271 100644 --- a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/PasskeyRequest.kt +++ b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/PasskeyRequest.kt @@ -31,7 +31,7 @@ class PasskeyRequest( val passkeyRequestCallback = object : CredentialManagerCallback { override fun onError(e: GetCredentialException) { onFailure(e) - Log.e("Credential Manager error", e.stackTraceToString()) + Log.e(TAG, e.stackTraceToString()) } override fun onResult(result: GetCredentialResponse) { @@ -52,7 +52,7 @@ class PasskeyRequest( callback = passkeyRequestCallback ) } catch (e: GetCredentialException) { - Log.e("Credential Manager error", e.stackTraceToString()) + Log.e(TAG, e.stackTraceToString()) onFailure(e) } } @@ -61,9 +61,13 @@ class PasskeyRequest( return when (val credential = this.credential) { is PublicKeyCredential -> credential.authenticationResponseJson else -> { - Log.e("Credential Manager", "Unexpected type of credential") + Log.e(TAG, "Unexpected type of credential") null } } } + + companion object { + const val TAG = "PasskeyRequest" + } } From eb7e2407c2d173c79d6337183606c888257e11a2 Mon Sep 17 00:00:00 2001 From: ThomazFB Date: Tue, 30 Jan 2024 12:42:35 -0300 Subject: [PATCH 28/33] Introduce PasskeyRequestData model --- .../wordpress/android/login/Login2FaFragment.java | 9 ++++++++- .../android/login/webauthn/PasskeyRequest.kt | 15 ++++++++++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/Login2FaFragment.java b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/Login2FaFragment.java index d1e0231f9e..15f86c747a 100644 --- a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/Login2FaFragment.java +++ b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/Login2FaFragment.java @@ -48,6 +48,7 @@ import org.wordpress.android.login.util.SiteUtils; import org.wordpress.android.login.webauthn.PasskeyRequest; import org.wordpress.android.login.webauthn.Fido2ClientHandler; +import org.wordpress.android.login.webauthn.PasskeyRequest.PasskeyRequestData; import org.wordpress.android.login.widgets.WPLoginInputRow; import org.wordpress.android.login.widgets.WPLoginInputRow.OnEditorCommitListener; import org.wordpress.android.util.AppLog; @@ -629,9 +630,15 @@ public void onWebauthnChallengeReceived(WebauthnChallengeReceived event) { return; } + PasskeyRequestData passkeyRequestData = new PasskeyRequestData( + event.mUserId, + event.mChallengeInfo.getTwoStepNonce(), + event.mRawChallengeInfoJson + ); + new PasskeyRequest( requireContext(), - event, + passkeyRequestData, result -> { mDispatcher.dispatch( AuthenticationActionBuilder.newFinishSecurityKeyChallengeAction( diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/PasskeyRequest.kt b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/PasskeyRequest.kt index f812554271..32ec570921 100644 --- a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/PasskeyRequest.kt +++ b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/PasskeyRequest.kt @@ -12,12 +12,11 @@ import androidx.credentials.GetPublicKeyCredentialOption import androidx.credentials.PublicKeyCredential import androidx.credentials.exceptions.GetCredentialException import org.wordpress.android.fluxc.store.AccountStore.FinishWebauthnChallengePayload -import org.wordpress.android.fluxc.store.AccountStore.WebauthnChallengeReceived import java.util.concurrent.Executors class PasskeyRequest( context: Context, - challengeEvent: WebauthnChallengeReceived, + requestData: PasskeyRequestData, onSuccess: (FinishWebauthnChallengePayload) -> Unit, onFailure: (Throwable) -> Unit ) { @@ -25,7 +24,7 @@ class PasskeyRequest( val executor = Executors.newSingleThreadExecutor() val signal = CancellationSignal() val getCredRequest = GetCredentialRequest( - listOf(GetPasswordOption(), GetPublicKeyCredentialOption(challengeEvent.mRawChallengeInfoJson)) + listOf(GetPasswordOption(), GetPublicKeyCredentialOption(requestData.requestJson)) ) val passkeyRequestCallback = object : CredentialManagerCallback { @@ -36,8 +35,8 @@ class PasskeyRequest( override fun onResult(result: GetCredentialResponse) { FinishWebauthnChallengePayload().apply { - mUserId = challengeEvent.mUserId - mTwoStepNonce = challengeEvent.mChallengeInfo.twoStepNonce + mUserId = requestData.userId + mTwoStepNonce = requestData.twoStepNonce mClientData = result.toJson().orEmpty() }.let { onSuccess(it) } } @@ -67,6 +66,12 @@ class PasskeyRequest( } } + data class PasskeyRequestData( + val userId: String, + val twoStepNonce: String, + val requestJson: String + ) + companion object { const val TAG = "PasskeyRequest" } From cf78f3c66cd7b9a0fb3703f19e71353f055ab2dc Mon Sep 17 00:00:00 2001 From: ThomazFB Date: Wed, 31 Jan 2024 19:12:11 -0300 Subject: [PATCH 29/33] Adjust PasskeyRequest construction strategy --- .../wordpress/android/login/webauthn/PasskeyRequest.kt | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/PasskeyRequest.kt b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/PasskeyRequest.kt index 32ec570921..01cad89146 100644 --- a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/PasskeyRequest.kt +++ b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/PasskeyRequest.kt @@ -14,7 +14,7 @@ import androidx.credentials.exceptions.GetCredentialException import org.wordpress.android.fluxc.store.AccountStore.FinishWebauthnChallengePayload import java.util.concurrent.Executors -class PasskeyRequest( +class PasskeyRequest private constructor( context: Context, requestData: PasskeyRequestData, onSuccess: (FinishWebauthnChallengePayload) -> Unit, @@ -74,5 +74,13 @@ class PasskeyRequest( companion object { const val TAG = "PasskeyRequest" + fun create( + context: Context, + requestData: PasskeyRequestData, + onSuccess: (FinishWebauthnChallengePayload) -> Unit, + onFailure: (Throwable) -> Unit + ) { + PasskeyRequest(context, requestData, onSuccess, onFailure) + } } } From 7208886d45b48c5b1da189adc7712e8926b41cde Mon Sep 17 00:00:00 2001 From: ThomazFB Date: Wed, 31 Jan 2024 19:17:32 -0300 Subject: [PATCH 30/33] Adopt static usage of PasskeyRequest --- .../java/org/wordpress/android/login/Login2FaFragment.java | 2 +- .../org/wordpress/android/login/webauthn/PasskeyRequest.kt | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/Login2FaFragment.java b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/Login2FaFragment.java index 15f86c747a..14df389354 100644 --- a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/Login2FaFragment.java +++ b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/Login2FaFragment.java @@ -636,7 +636,7 @@ public void onWebauthnChallengeReceived(WebauthnChallengeReceived event) { event.mRawChallengeInfoJson ); - new PasskeyRequest( + PasskeyRequest.create( requireContext(), passkeyRequestData, result -> { diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/PasskeyRequest.kt b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/PasskeyRequest.kt index 01cad89146..e1df99828e 100644 --- a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/PasskeyRequest.kt +++ b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/PasskeyRequest.kt @@ -73,7 +73,9 @@ class PasskeyRequest private constructor( ) companion object { - const val TAG = "PasskeyRequest" + private const val TAG = "PasskeyRequest" + + @JvmStatic fun create( context: Context, requestData: PasskeyRequestData, From e366c4927b6c47f4031ba615e840d30f1905ebb0 Mon Sep 17 00:00:00 2001 From: ThomazFB Date: Wed, 31 Jan 2024 19:18:10 -0300 Subject: [PATCH 31/33] Remove Password argument from CredentialManager request creation --- .../java/org/wordpress/android/login/webauthn/PasskeyRequest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/PasskeyRequest.kt b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/PasskeyRequest.kt index e1df99828e..5032b740b6 100644 --- a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/PasskeyRequest.kt +++ b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/PasskeyRequest.kt @@ -24,7 +24,7 @@ class PasskeyRequest private constructor( val executor = Executors.newSingleThreadExecutor() val signal = CancellationSignal() val getCredRequest = GetCredentialRequest( - listOf(GetPasswordOption(), GetPublicKeyCredentialOption(requestData.requestJson)) + listOf(GetPublicKeyCredentialOption(requestData.requestJson)) ) val passkeyRequestCallback = object : CredentialManagerCallback { From 60140658e0f43875f242982759f6e19bec88d7d5 Mon Sep 17 00:00:00 2001 From: ThomazFB Date: Wed, 31 Jan 2024 20:00:32 -0300 Subject: [PATCH 32/33] Wrap Credential Manager failure in main thread coroutines call --- .../org/wordpress/android/login/webauthn/PasskeyRequest.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/PasskeyRequest.kt b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/PasskeyRequest.kt index 5032b740b6..21e83e31ba 100644 --- a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/PasskeyRequest.kt +++ b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/PasskeyRequest.kt @@ -11,6 +11,9 @@ import androidx.credentials.GetPasswordOption import androidx.credentials.GetPublicKeyCredentialOption import androidx.credentials.PublicKeyCredential import androidx.credentials.exceptions.GetCredentialException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import org.wordpress.android.fluxc.store.AccountStore.FinishWebauthnChallengePayload import java.util.concurrent.Executors @@ -29,7 +32,7 @@ class PasskeyRequest private constructor( val passkeyRequestCallback = object : CredentialManagerCallback { override fun onError(e: GetCredentialException) { - onFailure(e) + CoroutineScope(Dispatchers.Main).launch { onFailure(e) } Log.e(TAG, e.stackTraceToString()) } From 8819f7f2765c4544256578ec21d38ec0540ab12a Mon Sep 17 00:00:00 2001 From: ThomazFB Date: Thu, 1 Feb 2024 22:14:11 -0300 Subject: [PATCH 33/33] Remove the PasskeyHandler --- .../android/login/webauthn/PasskeyHandler.kt | 61 ------------------- 1 file changed, 61 deletions(-) delete mode 100644 WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/PasskeyHandler.kt diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/PasskeyHandler.kt b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/PasskeyHandler.kt deleted file mode 100644 index 4dbfe16e74..0000000000 --- a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/webauthn/PasskeyHandler.kt +++ /dev/null @@ -1,61 +0,0 @@ -package org.wordpress.android.login.webauthn - -import android.app.Activity -import android.content.Intent -import androidx.activity.result.ActivityResult -import androidx.activity.result.contract.ActivityResultContracts.StartIntentSenderForResult -import androidx.fragment.app.Fragment -import com.google.android.gms.fido.Fido -import com.google.android.gms.fido.fido2.api.common.PublicKeyCredential -import org.wordpress.android.fluxc.network.rest.wpcom.auth.webauthn.WebauthnChallengeInfo - -class PasskeyHandler { - fun onFragmentCreation( - requestingFragment: Fragment, - userId: String, - challengeInfo: WebauthnChallengeInfo, - onResult: (PasskeyDataResult) -> Unit - ) { - // run only if API is 34 or above - if (android.os.Build.VERSION.SDK_INT >= 34) { - onResult(PasskeyDataResult(isFailure = true)) - return - } - - requestingFragment.registerForActivityResult( - StartIntentSenderForResult() - ) - { result: ActivityResult -> - if (result.resultCode == Activity.RESULT_OK && result.data != null) { - onResult(parseFIDO2IntentData(result.data, userId, challengeInfo)) - } else { - onResult(PasskeyDataResult(isFailure = true)) - } - } - } - - private fun parseFIDO2IntentData( - resultData: Intent?, - userId: String, - challengeInfo: WebauthnChallengeInfo - ): PasskeyDataResult = - resultData - ?.takeIf { it.hasExtra(Fido.FIDO2_KEY_CREDENTIAL_EXTRA) } - ?.getByteArrayExtra(Fido.FIDO2_KEY_CREDENTIAL_EXTRA) - ?.let { PublicKeyCredential.deserializeFromBytes(it).toJson() } - ?.let { clientData -> - PasskeyDataResult( - isFailure = false, - userId = userId, - twoStepNonce = challengeInfo.twoStepNonce, - clientData = clientData - ) - } ?: PasskeyDataResult(isFailure = true) - - data class PasskeyDataResult( - val isFailure: Boolean, - val userId: String? = null, - val twoStepNonce: String? = null, - val clientData: String? = null - ) -}