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

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"),导致:

  1. 前端生成凭证时,会将字符串ID转换为二进制后再编码,最终存储的userHandle与预期的用户ID编码不匹配
  2. 后续认证环节,无论传入原始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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 18:14:57