Daniel L
Daniel L

Reputation: 263

Webauthn expecting userHandle web-auth/webauthn-lib v5

I am trying to now authenticate my users, but it seems like web-auth/webauthn-lib (v5) is now expecting a userHandle even though when it has been set to null?

{"error":"Invalid user handle"}

This is odd because i am setting:

$publicKeyCredentialSource = AuthenticatorAssertionResponseValidator::create(
                (new CeremonyStepManagerFactory())->requestCeremony()
            )->check(
                publicKeyCredentialSource: $credentialSource,
                authenticatorAssertionResponse: $publicKeyCredential->response,
                publicKeyCredentialRequestOptions: $requestOptions,
                host: $request->getHost(),
                userHandle: null,
            );

Although, in the database I can see the source, which has the following:

"userHandle": "TVE",

This, if we decode it, it will return it to a base64: "MQ", which, if decoded, translates to 1.

This is confusing, and I am not sure why its checking the user handle if I am setting it to null

I tried adding $request->user()->id but that didn't work either. Even hardcoding TVE or MQ won't work. It shows the same error:

{"error":"Invalid user handle"}

In my registration, I am doing the following (in case this helps):

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

Finally, when I store it:

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

Cheers.

Upvotes: 1

Views: 94

Answers (0)

Related Questions