Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce credential manager passkey fetching #132

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
3ae6005
Define CredentialManagerHandler type
ThomazFB Jan 18, 2024
60ae305
Introduce initial basic Credential Manager implementation
ThomazFB Jan 18, 2024
94f3cc8
Start migration towards Credential Manager implementation without cor…
ThomazFB Jan 18, 2024
de1cb36
Add async operation structure inside the CredentialManagerHandler
ThomazFB Jan 18, 2024
a3e4efa
Implement expected callback responses for createPasskey function
ThomazFB Jan 18, 2024
1df10ff
Refactor CredentialManagerHandler function visibilities
ThomazFB Jan 18, 2024
a44c7f0
Merge branch 'issue/add-credential-manager' into issue/introduce-cred…
ThomazFB Jan 23, 2024
d84a005
Rename PasskeyCredentialsHandler.kt to Fido2ClientHandler
ThomazFB Jan 23, 2024
a30c603
Adjust Login2FaFragment to correctly reference the FIDO2 handler
ThomazFB Jan 23, 2024
be0f8ae
Introduce PasskeyHandler
ThomazFB Jan 23, 2024
8f53607
Add FIDO2 handling inside PasskeyHandler
ThomazFB Jan 23, 2024
8b67694
Add nullability to PasskeyDataResult
ThomazFB Jan 23, 2024
783d171
Finish FIDO2 basic handling in PasskeyHandler
ThomazFB Jan 23, 2024
2622a56
Correctly handle credential results from Credential Manager
ThomazFB Jan 23, 2024
df492eb
Parse result from CredentialManager
ThomazFB Jan 23, 2024
420e2f0
Adjust CredentialManagerHandler to properly connect with Login2FaFrag…
ThomazFB Jan 23, 2024
d8dd6c4
Add API version check to PasskeyHandler
ThomazFB Jan 24, 2024
6bdd91d
Dispatch challenge payload after interception
ThomazFB Jan 24, 2024
4f5e31e
Force Credential Manager to run on lower API levels
ThomazFB Jan 26, 2024
ae7764e
Adjust unnecessary API check in CredentialManagerHandler
ThomazFB Jan 26, 2024
bc86ad1
Refactor redundant code
ThomazFB Jan 26, 2024
bc6316e
Fix lint issues
ThomazFB Jan 26, 2024
5f23f01
Update FluxC version
ThomazFB Jan 28, 2024
5609ded
Update Credential Manager with raw JSON data from challenge
ThomazFB Jan 28, 2024
2ca49f5
Add logs for errors coming from Credential the Manager API
ThomazFB Jan 29, 2024
8e72577
Refactor CredentialManagerHandler.kt to PasskeyRequest
ThomazFB Jan 30, 2024
7987312
Refactor PasskeyRequest excessive parameters
ThomazFB Jan 30, 2024
dfea9d0
Add log tag into PasskeyRequest
ThomazFB Jan 30, 2024
eb7e240
Introduce PasskeyRequestData model
ThomazFB Jan 30, 2024
cf78f3c
Adjust PasskeyRequest construction strategy
ThomazFB Jan 31, 2024
7208886
Adopt static usage of PasskeyRequest
ThomazFB Jan 31, 2024
e366c49
Remove Password argument from CredentialManager request creation
ThomazFB Jan 31, 2024
6014065
Wrap Credential Manager failure in main thread coroutines call
ThomazFB Jan 31, 2024
8819f7f
Remove the PasskeyHandler
ThomazFB Feb 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@
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.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;
Expand Down Expand Up @@ -125,7 +127,7 @@ public class Login2FaFragment extends LoginBaseFormFragment<LoginListener> imple
private boolean mIsSocialLoginConnect;
private boolean mSentSmsCode;
private List<SupportedAuthTypes> mSupportedAuthTypes;
@Nullable private PasskeyCredentialsHandler mPasskeyCredentialsHandler = null;
@Nullable private Fido2ClientHandler mFido2ClientHandler = null;
@Nullable private ActivityResultLauncher<IntentSenderRequest> mResultLauncher = null;

public static Login2FaFragment newInstance(String emailAddress, String password) {
Expand Down Expand Up @@ -620,39 +622,48 @@ private void doAuthWithSecurityKeyAction() {
.newStartSecurityKeyChallengeAction(payload));
}

@SuppressWarnings("unused")
@Subscribe(threadMode = ThreadMode.MAIN)
public void onWebauthnChallengeReceived(WebauthnChallengeReceived event) {
if (event.isError()) {
endProgress();
handleAuthError(event.error.type, getString(R.string.login_error_security_key));
return;
}
mPasskeyCredentialsHandler = new PasskeyCredentialsHandler(

PasskeyRequestData passkeyRequestData = new PasskeyRequestData(
event.mUserId,
event.mChallengeInfo
event.mChallengeInfo.getTwoStepNonce(),
event.mRawChallengeInfoJson
);
mPasskeyCredentialsHandler.createIntentSender(

PasskeyRequest.create(
requireContext(),
intent -> {
if (mResultLauncher != null) {
mResultLauncher.launch(intent);
}
});
passkeyRequestData,
result -> {
mDispatcher.dispatch(
AuthenticationActionBuilder.newFinishSecurityKeyChallengeAction(
result));
return null;
},
error -> {
handleWebauthnError();
return null;
}
);
}

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;
}

PublicKeyCredential credentials =
PublicKeyCredential.deserializeFromBytes(credentialBytes);
FinishWebauthnChallengePayload payload =
mPasskeyCredentialsHandler.onCredentialsAvailable(credentials);
mFido2ClientHandler.onCredentialsAvailable(credentials);
mDispatcher.dispatch(
AuthenticationActionBuilder.newFinishSecurityKeyChallengeAction(payload));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ interface OnPasskeyRequestReadyListener {
fun onPasskeyRequestReady(intentSenderRequest: IntentSenderRequest)
}

class PasskeyCredentialsHandler(
class Fido2ClientHandler(
private val userId: String,
private val challengeInfo: WebauthnChallengeInfo
) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package org.wordpress.android.login.webauthn

import android.content.Context
import android.os.CancellationSignal
import android.util.Log
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.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

class PasskeyRequest private constructor(
context: Context,
requestData: PasskeyRequestData,
onSuccess: (FinishWebauthnChallengePayload) -> Unit,
onFailure: (Throwable) -> Unit
) {
init {
val executor = Executors.newSingleThreadExecutor()
val signal = CancellationSignal()
val getCredRequest = GetCredentialRequest(
listOf(GetPublicKeyCredentialOption(requestData.requestJson))
)

val passkeyRequestCallback = object : CredentialManagerCallback<GetCredentialResponse, GetCredentialException> {
override fun onError(e: GetCredentialException) {
CoroutineScope(Dispatchers.Main).launch { onFailure(e) }
Log.e(TAG, e.stackTraceToString())
}

override fun onResult(result: GetCredentialResponse) {
FinishWebauthnChallengePayload().apply {
mUserId = requestData.userId
mTwoStepNonce = requestData.twoStepNonce
mClientData = result.toJson().orEmpty()
}.let { onSuccess(it) }
}
}

try {
CredentialManager.create(context).getCredentialAsync(
request = getCredRequest,
context = context,
cancellationSignal = signal,
executor = executor,
callback = passkeyRequestCallback
)
} catch (e: GetCredentialException) {
Log.e(TAG, e.stackTraceToString())
onFailure(e)
}
}

private fun GetCredentialResponse.toJson(): String? {
return when (val credential = this.credential) {
is PublicKeyCredential -> credential.authenticationResponseJson
else -> {
Log.e(TAG, "Unexpected type of credential")
null
}
}
}

data class PasskeyRequestData(
val userId: String,
val twoStepNonce: String,
val requestJson: String
)

companion object {
private const val TAG = "PasskeyRequest"

@JvmStatic
fun create(
context: Context,
requestData: PasskeyRequestData,
onSuccess: (FinishWebauthnChallengePayload) -> Unit,
onFailure: (Throwable) -> Unit
) {
PasskeyRequest(context, requestData, onSuccess, onFailure)
}
}
}
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Loading