web-auth/webauthn-lib v5认证时userHandle设为null仍报错
问题描述
使用web-auth/webauthn-lib(v5)实现用户认证时,遇到以下问题:
- 即使将
userHandle设置为null,仍收到{"error":"Invalid user handle"}错误 - 数据库存储的凭证源中
userHandle值为"TVE",解码后得到"MQ",最终对应用户ID"1" - 尝试传入
$request->user()->id、硬编码"TVE"或"MQ",均触发相同错误
以下是注册和存储环节的代码:
注册选项接口代码
public function registerOptions(Request $request) { $userId = $request->user()->id; $challenge = Str::random(); // Encode user id and challenge in base64url // $encodedUserId = Base64Url::encode($userId); // $encodedChallenge = Base64Url::encode($challenge); $options = PublicKeyCredentialCreationOptions::create( rp: new PublicKeyCredentialRpEntity( name: 'Authen', id: parse_url(config('app.url'), PHP_URL_HOST), icon: null ), user: new PublicKeyCredentialUserEntity( name: $request->user()->email, id: $request->user()->id, displayName: $request->user()->name, ), challenge: Str::random(), authenticatorSelection: new AuthenticatorSelectionCriteria( // authenticatorAttachment: AuthenticatorSelectionCriteria::AUTHENTICATOR_ATTACHMENT_NO_PREFERENCE, ), ); return JsonSerializer::serialize($options); }
存储凭证代码
public function store(Request $request) { $data = $request->validate([ 'passkey' => ['required', 'json'], 'device_name' => ['required', 'string', 'max:255'], 'options' => ['required'], ]); /** @var PublicKeyCredential $publicKeyCredential */ $publicKeyCredential = (new WebauthnSerializerFactory(AttestationStatementSupportManager::create())) ->create() ->deserialize($data['passkey'], PublicKeyCredential::class, 'json'); $optionsData = json_decode($data['options']); $rp = new PublicKeyCredentialRpEntity( $optionsData->rp->name, $optionsData->rp->id, $optionsData->rp->icon ?? null // Optional parameter ); $user = new PublicKeyCredentialUserEntity( $optionsData->user->name, $optionsData->user->id, $optionsData->user->displayName, null ); $authenticatorSelection = new AuthenticatorSelectionCriteria( $optionsData->authenticatorSelection->authenticatorAttachment, $optionsData->authenticatorSelection->userVerification, $optionsData->authenticatorSelection->residentKey, ); $options = new PublicKeyCredentialCreationOptions( $rp, $user, Base64Url::decode($optionsData->challenge), $optionsData->pubKeyCredParams, $authenticatorSelection, null, // Timeout (optional, can be null) $optionsData->excludeCredentials, null, // Attestation (optional, can be null) null // Extensions (optional, can be null) ); if(!$publicKeyCredential->response instanceof AuthenticatorAttestationResponse) { return response()->json(['error' => 'invalid response'], 400); } try { $publicKeyCredentialSource = AuthenticatorAttestationResponseValidator::create( (new CeremonyStepManagerFactory())->creationCeremony(), )->check( authenticatorAttestationResponse: $publicKeyCredential->response, publicKeyCredentialCreationOptions: $options, host: $request->getHost(), ); $agent = new Agent(); $device = $request->user()->devices()->create([ 'device_name' => $data['device_name'], 'device_type' => $agent->deviceType(), ]); $passkey = $device->passkey()->create([ 'user_id' => $request->user()->id, 'device_id' => $device->id, 'credential_id' => $publicKeyCredentialSource->publicKeyCredentialId, 'public_key_credential_source' => JsonSerializer::serialize($publicKeyCredentialSource), // Triggers the mutator 'device_type' => $agent->deviceType(), ]); // Update the device to associate the passkey $device->update(['passkey_id' => $passkey->id]); } catch (\Exception $exception){ return response()->json(['error' => $exception->getMessage()], 400); } return response()->json(['message' => 'success'], 200); }
问题排查与解决方案
核心原因
WebAuthn规范要求PublicKeyCredentialUserEntity的id参数必须是base64url编码的二进制数据,当前代码直接传入字符串格式的用户ID(如"1"),导致:
- 前端生成凭证时,会将字符串ID转换为二进制后再编码,最终存储的
userHandle与预期的用户ID编码不匹配 - 后续认证环节,无论传入原始ID、存储的
userHandle还是其解码值,都无法与凭证源中存储的userHandle完成校验
修复步骤
1. 注册环节正确编码用户ID
修改registerOptions方法,对用户ID和挑战值进行base64url编码:
public function registerOptions(Request $request) { $userId = $request->user()->id; // 对用户ID和挑战值进行base64url编码 $encodedUserId = Base64Url::encode($userId); $challenge = Str::random(); $encodedChallenge = Base64Url::encode($challenge); $options = PublicKeyCredentialCreationOptions::create( rp: new PublicKeyCredentialRpEntity( name: 'Authen', id: parse_url(config('app.url'), PHP_URL_HOST), icon: null ), user: new PublicKeyCredentialUserEntity( name: $request->user()->email, id: $encodedUserId, // 使用编码后的用户ID displayName: $request->user()->name, ), challenge: $encodedChallenge, // 使用编码后的挑战值 authenticatorSelection: new AuthenticatorSelectionCriteria( // authenticatorAttachment: AuthenticatorSelectionCriteria::AUTHENTICATOR_ATTACHMENT_NO_PREFERENCE, ), ); return JsonSerializer::serialize($options); }
2. 存储环节保持编码逻辑一致
store方法中,前端传回的options里的user.id已是编码后的字符串,直接传入PublicKeyCredentialUserEntity即可:
$user = new PublicKeyCredentialUserEntity( $optionsData->user->name, $optionsData->user->id, // 直接使用前端传回的编码后ID $optionsData->user->displayName, null );
3. 认证环节补充处理
认证时若需传入userHandle,需使用与注册环节一致的base64url编码后的用户ID;若保持userHandle为null,需确保注册时开启了resident key选项(依赖凭证自动关联用户)。
额外注意事项
- 数据库中已有的旧凭证记录需删除后重新注册,旧凭证的
userHandle基于错误编码生成,无法兼容修复后的逻辑 - 挑战值必须进行base64url编码,这是WebAuthn规范的强制要求
内容的提问来源于stack exchange,提问作者Daniel L
相关产品推荐
相关产品推荐

