androidkotlinfido

Fido2 on Android, how to get the user handle (user id) back


I'm trying to use Fido2 on Android. I've managed to create a credential with the following code:

val options = PublicKeyCredentialCreationOptions
    .Builder()
    .setRp(PublicKeyCredentialRpEntity(params.rpId, params.rpName, null))
    .setUser(
        PublicKeyCredentialUserEntity(
            Base64.decode(params.userId, Base64.URL_SAFE),
            params.userName,
            params.userName, // TODO ask declan about icon?
            params.userName
        )
    )
    .setChallenge(Base64.decode(params.challenge, Base64.URL_SAFE))
    .setParameters(
        listOf(
            PublicKeyCredentialParameters(
                PublicKeyCredentialType.PUBLIC_KEY.toString(),
                params.pubKeyAlg
            ),
        )
    )
    .build()

val fido2ApiClient = context.reactContext?.let { Fido.getFido2ApiClient(it) }
val task = fido2ApiClient?.getRegisterPendingIntent(options)

task?.addOnSuccessListener {
    try {

        context.currentActivity!!.startIntentSenderForResult(
            it.intentSender,
            REGISTER_REQUEST_CODE,
            null,
            0,
            0,
            0
        )

    } catch (e: IntentSender.SendIntentException) {
        RNLog.w(context.reactContext as ReactContext ?, "ROPO ERROR $e")
    }

}

task?.addOnFailureListener {
    RNLog.w(context.reactContext as ReactContext?, "Failure to start intent $it")
}

And retrieval (signing) is done via:

val allowedKeys = ArrayList<PublicKeyCredentialDescriptor>()
for (i in 0 until params.allowCredentials.size) {
    val keyHandle: String = params.allowCredentials[i]
    val keyHandleByte = Base64.decode(keyHandle, Base64.URL_SAFE)
    allowedKeys.add(
        PublicKeyCredentialDescriptor(
            PublicKeyCredentialType.PUBLIC_KEY.toString(),
            keyHandleByte,
            null
        )
    )
}

val options = PublicKeyCredentialRequestOptions.Builder()
    .setChallenge(params.challenge.toByteArray())
    .setRpId(params.rpId)
    .setAllowList(allowedKeys)
    .build()

val fido2ApiClient = context.reactContext?.let { Fido.getFido2ApiClient(it) }
val task = fido2ApiClient?.getSignPendingIntent(options)

task?.addOnSuccessListener {
    try {

        context.currentActivity!!.startIntentSenderForResult(
            it.intentSender,
            SIGN_REQUEST_CODE,
            null,
            0,
            0,
            0
        )

    } catch (e: IntentSender.SendIntentException) {
        RNLog.w(context.reactContext as ReactContext ?, "ROPO ERROR $e")
    }

}

task?.addOnFailureListener {
    RNLog.w(context.reactContext as ReactContext?, "Failure to start intent $it")
}

This calls both work with activity listeners. Here is the piece of code that handles the response of the signing/retrieval:

when (resultCode) {
    Activity.RESULT_CANCELED -> {
        retrievePromise!!.reject("authenticationCanceled", null, null)
    }

    Activity.RESULT_OK -> {
        if (!intent!!.hasExtra(Fido.FIDO2_KEY_CREDENTIAL_EXTRA)) {
            retrievePromise!!.reject("authenticationError", null, null)
        } else {
            val credential = PublicKeyCredential.deserializeFromBytes(intent.getByteArrayExtra(Fido.FIDO2_KEY_CREDENTIAL_EXTRA)!!)
            val response = credential.response
            if(response is AuthenticatorErrorResponse) {
                retrievePromise!!.reject("authenticationError", response.errorMessage, null)
            } else {
                val userHandle = (response as AuthenticatorAssertionResponse).userHandle // THIS IS NULL!!!
                retrievePromise!!.resolve(Base64.encodeToString(userHandle, Base64.URL_SAFE))
            }

        }
    }
}

However the problem is when I try to access the AuthenticatorAssertionResponse.userHandle the value is not there, it is null.

enter image description here

Decompiled class, zze property should contain the bytes for the user handle

This response seems to suggest on Android it is not possible to get access to the user handle.

I'm really wondering if I'm doing something wrong here.

As additional data point the specification clearly states an extension can be added to indicate in-device saving of the key, which should then store/return the user handle property. However, I cannot seem to find a way to add this extension on the Kotlin code:

enter image description here

Any help is greatly appreciated! Thanks!


Solution

  • I got it to work. As is turns out you need to specify the resident key in the as an Authenticator Selection when building the PublicKeyCredential:

    .setAuthenticatorSelection(
        AuthenticatorSelectionCriteria
            .Builder()
            .setAttachment(Attachment.CROSS_PLATFORM)
            .setRequireResidentKey(true)
            .build()
    )
    

    Then Android will only show the possible authentication methods that support resident keys (NFC/USB) and the user handle will be populated.