You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android平台Fido2开发:如何获取返回的userHandle(用户ID)

Android FIDO2身份验证中userHandle为空的问题与解决方案

问题描述

我在Android平台基于Fido2开发身份验证功能,已完成凭证注册、签名及响应处理的代码编写,但在处理签名响应时,AuthenticatorAssertionResponse.userHandle始终返回null。虽有资料显示这可能是Android平台的限制,但我怀疑是否存在操作失误。另外WebAuthn规范提到可通过扩展实现设备内密钥存储并返回userHandle,但找不到在Kotlin代码中添加该扩展的方法,特此寻求解决方案。

现有代码

注册代码

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")
}

签名代码

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")
}

响应处理代码

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

解决方案

1. 原因分析

Android平台的FIDO2实现中,默认创建的是非驻留密钥(non-resident key),这类密钥不会在断言响应中返回userHandle。只有当凭证是**驻留密钥(resident key,即设备内存储的密钥)**时,身份验证器才会在签名响应中携带userHandle。

2. 注册阶段添加扩展启用驻留密钥

要获取userHandle,需在注册阶段添加credProps扩展,并配置要求创建驻留密钥。修改注册代码如下:

// 构建credProps扩展,请求启用设备内存储
val credPropsExtension = AuthenticationExtensionsClientInputs.Builder()
    .setCredProps(true)
    .build()

val options = PublicKeyCredentialCreationOptions
    .Builder()
    .setRp(PublicKeyCredentialRpEntity(params.rpId, params.rpName, null))
    .setUser(
        PublicKeyCredentialUserEntity(
            Base64.decode(params.userId, Base64.URL_SAFE),
            params.userName,
            params.userName,
            params.userName
        )
    )
    .setChallenge(Base64.decode(params.challenge, Base64.URL_SAFE))
    .setParameters(
        listOf(
            PublicKeyCredentialParameters(
                PublicKeyCredentialType.PUBLIC_KEY.toString(),
                params.pubKeyAlg
            ),
        )
    )
    // 添加扩展
    .setExtensions(credPropsExtension)
    // 要求创建驻留密钥
    .setRequireResidentKey(true)
    .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")
}

3. 签名阶段的注意事项

签名阶段无需额外扩展配置,但需确保请求的allowList中包含已注册的驻留密钥凭证。

4. 响应处理的调整

修改注册流程后,签名响应中的userHandle应该不再为空,但仍需保留空值判断以兼容特殊情况:

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
                if (userHandle != null) {
                    retrievePromise!!.resolve(Base64.encodeToString(userHandle, Base64.URL_SAFE))
                } else {
                    retrievePromise!!.reject("userHandleNull", "User handle not returned", null)
                }
            }
        }
    }
}

关键说明

  • setRequireResidentKey(true):强制身份验证器创建驻留密钥,部分不支持驻留密钥的设备可能会导致注册失败,需做好异常处理。
  • credProps扩展:用于确认凭证是否为驻留密钥,注册完成后可通过credential.getExtensions().getCredProps()验证是否成功启用。

内容的提问来源于stack exchange,提问作者Oscar Franco

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.10 09:42:05