Skip to content

Commit 9223731

Browse files
committed
omit null values from options
1 parent 4cad537 commit 9223731

6 files changed

Lines changed: 71 additions & 3 deletions

File tree

src/Http/Controllers/PasskeyConfirmationController.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public function index(Request $request, GenerateVerificationOptions $generate):
4040
$request->session()->put('passkey.verification_options', $serialized);
4141

4242
return response()->json([
43-
'options' => json_decode($serialized, true),
43+
'options' => WebAuthn::toBrowserArray($options),
4444
]);
4545
}
4646

src/Http/Controllers/PasskeyLoginController.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public function index(Request $request, GenerateVerificationOptions $generate):
3333
$request->session()->put('passkey.verification_options', $serialized);
3434

3535
return response()->json([
36-
'options' => json_decode($serialized, true),
36+
'options' => WebAuthn::toBrowserArray($options),
3737
]);
3838
}
3939

src/Http/Controllers/PasskeyRegistrationController.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public function index(Request $request, GenerateRegistrationOptions $generate):
3838
$request->session()->put('passkey.registration_options', $serialized);
3939

4040
return response()->json([
41-
'options' => json_decode($serialized, true),
41+
'options' => WebAuthn::toBrowserArray($options),
4242
]);
4343
}
4444

src/Support/WebAuthn.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
namespace Laravel\Passkeys\Support;
66

77
use Laravel\Passkeys\Passkeys;
8+
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
9+
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
810
use Symfony\Component\Serializer\SerializerInterface;
11+
use UnexpectedValueException;
912
use Webauthn\AttestationStatement\AttestationStatementSupportManager;
1013
use Webauthn\AttestationStatement\NoneAttestationStatementSupport;
1114
use Webauthn\AuthenticatorAssertionResponseValidator;
@@ -37,6 +40,27 @@ public static function toJson(mixed $data): string
3740
return self::serializer()->serialize($data, 'json');
3841
}
3942

43+
/**
44+
* Serialize data to a browser-facing array, omitting null values.
45+
*
46+
* @return array<array-key, mixed>
47+
*/
48+
public static function toBrowserArray(mixed $data): array
49+
{
50+
$serializer = self::serializer();
51+
assert($serializer instanceof NormalizerInterface);
52+
53+
$normalized = $serializer->normalize($data, 'json', [
54+
AbstractObjectNormalizer::SKIP_NULL_VALUES => true,
55+
]);
56+
57+
if (! is_array($normalized)) {
58+
throw new UnexpectedValueException('Serialized WebAuthn data must normalize to an array.');
59+
}
60+
61+
return $normalized;
62+
}
63+
4064
/**
4165
* Deserialize JSON to a specific class.
4266
*

tests/Feature/Controllers/PasskeyRegistrationTest.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,24 @@
3333
]);
3434
});
3535

36+
it('omits null values from browser-facing registration options', function (): void {
37+
$user = User::create([
38+
'name' => 'Test User',
39+
'email' => 'test@example.com',
40+
]);
41+
42+
$response = $this->actingAs($user)
43+
->withSession(['auth.password_confirmed_at' => time()])
44+
->getJson('/user/passkeys/options')
45+
->assertOk();
46+
47+
expect($response->json('options.authenticatorSelection.authenticatorAttachment'))->toBeNull();
48+
expect($response->json('options.authenticatorSelection'))
49+
->not->toHaveKey('authenticatorAttachment')
50+
->toHaveKey('residentKey', 'required')
51+
->toHaveKey('userVerification', 'required');
52+
});
53+
3654
it('stores registration options in session', function (): void {
3755
$user = User::create([
3856
'name' => 'Test User',

tests/Feature/WebAuthnTest.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<?php
22

33
use Laravel\Passkeys\Support\WebAuthn;
4+
use Webauthn\AuthenticatorSelectionCriteria;
45
use Webauthn\PublicKeyCredentialCreationOptions;
56
use Webauthn\PublicKeyCredentialRequestOptions;
67
use Webauthn\PublicKeyCredentialRpEntity;
@@ -30,6 +31,31 @@
3031
expect($restored->user->displayName)->toBe('Test User');
3132
});
3233

34+
it('serializes browser arrays without null values', function (): void {
35+
$options = PublicKeyCredentialCreationOptions::create(
36+
rp: PublicKeyCredentialRpEntity::create(name: 'Test App', id: 'localhost'),
37+
user: PublicKeyCredentialUserEntity::create(
38+
name: 'test@example.com',
39+
id: 'user-id-123',
40+
displayName: 'Test User',
41+
),
42+
challenge: random_bytes(32),
43+
authenticatorSelection: AuthenticatorSelectionCriteria::create(
44+
authenticatorAttachment: AuthenticatorSelectionCriteria::AUTHENTICATOR_ATTACHMENT_NO_PREFERENCE,
45+
residentKey: AuthenticatorSelectionCriteria::RESIDENT_KEY_REQUIREMENT_REQUIRED,
46+
),
47+
excludeCredentials: [],
48+
);
49+
50+
$array = WebAuthn::toBrowserArray($options);
51+
52+
expect($array)->not->toHaveKey('attestation');
53+
expect($array)->toHaveKey('excludeCredentials', []);
54+
expect($array['authenticatorSelection'])
55+
->not->toHaveKey('authenticatorAttachment')
56+
->toHaveKey('residentKey', 'required');
57+
});
58+
3359
it('serializes and deserializes verification options', function (): void {
3460
$options = PublicKeyCredentialRequestOptions::create(
3561
challenge: random_bytes(32),

0 commit comments

Comments
 (0)