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
相关产品推荐
相关产品推荐

