diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f565f05f66..8dcb4ec5f9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -42,7 +42,7 @@ jobs: apk add --update --no-cache \ php\$V-cli php\$V-mysqli php\$V-session php\$V-mbstring php\$V-ctype php\$V-sodium \ php\$V-iconv php\$V-xml php\$V-tokenizer php\$V-xmlwriter php\$V-simplexml \ - php\$V-dom php\$V-json php\$V-bz2 php\$V-curl php\$V-gd php\$V-zip \ + php\$V-dom php\$V-json php\$V-bz2 php\$V-curl php\$V-gd php\$V-zip php\$V-bcmath \ musl-locales musl-locales-lang \ gettext composer git && \ composer config version "$(php -r "define('VERSION_SUFFIX', ''); require_once('src/Version.php'); echo \PhpMyAdmin\Version::VERSION;")" && \ diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 59e31267d3..8ba6ecf8f5 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -5467,7 +5467,7 @@ parameters: - message: "#^Parameter \\#1 \\$identifier of static method PhpMyAdmin\\\\Util\\:\\:backquote\\(\\) expects string\\|Stringable\\|null, mixed given\\.$#" - count: 4 + count: 3 path: src/CreateAddField.php - @@ -5491,8 +5491,8 @@ parameters: path: src/CreateAddField.php - - message: "#^Parameter \\#1 \\$string of function trim expects string, mixed given\\.$#" - count: 1 + message: "#^Parameter \\#1 \\$string of function rtrim expects string, mixed given\\.$#" + count: 2 path: src/CreateAddField.php - @@ -15715,3 +15715,38 @@ parameters: count: 1 path: tests/unit/WebAuthn/CBORDecoderTest.php + - + message: "#^Call to static method PHPUnit\\\\Framework\\\\Assert\\:\\:assertArrayHasKey\\(\\) with 'attestation' and array\\{challenge\\: non\\-falsy\\-string, rp\\: array\\{name\\: string, id\\: string\\}, user\\: array\\{id\\: string, name\\: string, displayName\\: string\\}, pubKeyCredParams\\: non\\-empty\\-array\\, authenticatorSelection\\: array\\, timeout\\: int\\<1, max\\>, attestation\\: non\\-empty\\-string\\} will always evaluate to true\\.$#" + count: 1 + path: tests/unit/WebAuthn/CustomServerTest.php + + - + message: "#^Call to static method PHPUnit\\\\Framework\\\\Assert\\:\\:assertArrayHasKey\\(\\) with 'challenge' and array\\{challenge\\: non\\-empty\\-string, rp\\: array\\{name\\: string, id\\: string\\}, user\\: array\\{id\\: string, name\\: string, displayName\\: string\\}, pubKeyCredParams\\: array\\, authenticatorSelection\\: array\\, timeout\\: int\\<1, max\\>, attestation\\: non\\-empty\\-string\\} will always evaluate to true\\.$#" + count: 1 + path: tests/unit/WebAuthn/CustomServerTest.php + + - + message: "#^Call to static method PHPUnit\\\\Framework\\\\Assert\\:\\:assertArrayHasKey\\(\\) with 'pubKeyCredParams' and array\\{challenge\\: non\\-falsy\\-string, rp\\: array\\{name\\: string, id\\: string\\}, user\\: array\\{id\\: string, name\\: string, displayName\\: string\\}, pubKeyCredParams\\: array\\, authenticatorSelection\\: array\\, timeout\\: int\\<1, max\\>, attestation\\: non\\-empty\\-string\\} will always evaluate to true\\.$#" + count: 1 + path: tests/unit/WebAuthn/CustomServerTest.php + + - + message: "#^Call to static method PHPUnit\\\\Framework\\\\Assert\\:\\:assertNull\\(\\) with null will always evaluate to true\\.$#" + count: 1 + path: tests/unit/WebAuthn/CustomServerTest.php + + - + message: "#^Call to static method PHPUnit\\\\Framework\\\\Assert\\:\\:assertArrayHasKey\\(\\) with 'attestation' and array\\{challenge\\: non\\-falsy\\-string, rp\\: array\\{name\\: string, id\\: string\\}, user\\: array\\{id\\: string, name\\: string, displayName\\: string\\}, pubKeyCredParams\\: non\\-empty\\-array\\, authenticatorSelection\\: array\\, timeout\\: int\\<1, max\\>, attestation\\: non\\-empty\\-string\\} will always evaluate to true\\.$#" + count: 1 + path: tests/unit/WebAuthn/WebauthnLibServerTest.php + + - + message: "#^Call to static method PHPUnit\\\\Framework\\\\Assert\\:\\:assertArrayHasKey\\(\\) with 'challenge' and array\\{challenge\\: non\\-empty\\-string, rp\\: array\\{name\\: string, id\\: string\\}, user\\: array\\{id\\: string, name\\: string, displayName\\: string\\}, pubKeyCredParams\\: array\\, authenticatorSelection\\: array\\, timeout\\: int\\<1, max\\>, attestation\\: non\\-empty\\-string\\} will always evaluate to true\\.$#" + count: 1 + path: tests/unit/WebAuthn/WebauthnLibServerTest.php + + - + message: "#^Call to static method PHPUnit\\\\Framework\\\\Assert\\:\\:assertArrayHasKey\\(\\) with 'pubKeyCredParams' and array\\{challenge\\: non\\-falsy\\-string, rp\\: array\\{name\\: string, id\\: string\\}, user\\: array\\{id\\: string, name\\: string, displayName\\: string\\}, pubKeyCredParams\\: array\\, authenticatorSelection\\: array\\, timeout\\: int\\<1, max\\>, attestation\\: non\\-empty\\-string\\} will always evaluate to true\\.$#" + count: 1 + path: tests/unit/WebAuthn/WebauthnLibServerTest.php + diff --git a/resources/js/src/modules/indexes.ts b/resources/js/src/modules/indexes.ts index cdef7ea4bd..ae3694d364 100644 --- a/resources/js/src/modules/indexes.ts +++ b/resources/js/src/modules/indexes.ts @@ -193,7 +193,7 @@ function addColumnToIndex (sourceArray, arrayIndex, indexChoice, colIndex): void columnNames.push($('input[name="field_name[' + this.col_index + ']"]').val()); }); - displayName = '[' + columnNames.join(', ') + ']'; + displayName = '[' + columnNames.join(', ').trimRight() + ']'; } $.each(columns, function () { diff --git a/src/CreateAddField.php b/src/CreateAddField.php index d6e4dabcc9..a576ba4c53 100644 --- a/src/CreateAddField.php +++ b/src/CreateAddField.php @@ -13,6 +13,7 @@ use function implode; use function in_array; use function json_decode; use function preg_replace; +use function rtrim; use function trim; /** @@ -64,7 +65,7 @@ class CreateAddField } $definition = $this->getStatementPrefix($isCreateTable) . Table::generateFieldSpec( - trim($_POST['field_name'][$i]), + rtrim($_POST['field_name'][$i]), $_POST['field_type'][$i], $_POST['field_length'][$i], $_POST['field_attribute'][$i], @@ -153,7 +154,7 @@ class CreateAddField $indexFields = []; foreach ($index['columns'] as $key => $column) { - $indexFields[$key] = Util::backquote($_POST['field_name'][$column['col_index']]); + $indexFields[$key] = Util::backquote(rtrim($_POST['field_name'][$column['col_index']])); if (! $column['size']) { continue; } diff --git a/src/WebAuthn/CustomServer.php b/src/WebAuthn/CustomServer.php index e3c958a1c4..603f5e16e0 100644 --- a/src/WebAuthn/CustomServer.php +++ b/src/WebAuthn/CustomServer.php @@ -120,8 +120,8 @@ final class CustomServer implements Server ): array { try { $attestationCredential = $this->getAttestationCredential($attestationResponse); - } catch (Throwable) { - throw new WebAuthnException('Invalid authenticator response.'); + } catch (Throwable $exception) { + throw new WebAuthnException('Invalid authenticator response.', (int) $exception->getCode(), $exception); } $creationOptions = json_decode($credentialCreationOptions, true); diff --git a/tests/unit/WebAuthn/CustomServerTest.php b/tests/unit/WebAuthn/CustomServerTest.php new file mode 100644 index 0000000000..b2ed6d60f3 --- /dev/null +++ b/tests/unit/WebAuthn/CustomServerTest.php @@ -0,0 +1,129 @@ +getCredentialCreationOptions('user_name', 'user_id', 'test.localhost'); + self::assertArrayHasKey('challenge', $options); + self::assertNotEmpty($options['challenge']); + self::assertArrayHasKey('pubKeyCredParams', $options); + self::assertNotEmpty($options['pubKeyCredParams']); + self::assertArrayHasKey('attestation', $options); + self::assertNotEmpty($options['attestation']); + self::assertSame('phpMyAdmin (test.localhost)', $options['rp']['name']); + self::assertSame('test.localhost', $options['rp']['id']); + self::assertSame('user_name', $options['user']['name']); + self::assertSame('user_name', $options['user']['displayName']); + self::assertSame('user_id', $options['user']['id']); + self::assertArrayHasKey('authenticatorAttachment', $options['authenticatorSelection']); + self::assertSame('cross-platform', $options['authenticatorSelection']['authenticatorAttachment']); + } + + public function testGetCredentialRequestOptions(): void + { + $server = new CustomServer(); + $options = $server->getCredentialRequestOptions( + 'user_name', + 'userHandle1', + 'test.localhost', + [['type' => 'public-key', 'id' => 'cHVibGljS2V5Q3JlZGVudGlhbElkMQ']], + ); + self::assertNotEmpty($options['challenge']); + self::assertSame( + [['type' => 'public-key', 'id' => 'cHVibGljS2V5Q3JlZGVudGlhbElkMQ==']], + $options['allowCredentials'], + ); + self::assertSame(60000, $options['timeout']); + self::assertSame('none', $options['attestation']); + self::assertSame('discouraged', $options['userVerification']); + } + + /** @see https://github.com/web-auth/webauthn-framework/blob/v3.3.12/tests/library/Functional/AssertionTest.php#L46 */ + public function testParseAndValidateAssertionResponse(): void + { + $server = new CustomServer(); + + $uriStub = self::createStub(UriInterface::class); + $uriStub->method('getHost')->willReturn('localhost'); + $request = self::createStub(ServerRequest::class); + $request->method('getUri')->willReturn($uriStub); + + // phpcs:ignore Generic.Files.LineLength.TooLong + $authenticatorResponse = '{"id":"eHouz_Zi7-BmByHjJ_tx9h4a1WZsK4IzUmgGjkhyOodPGAyUqUp_B9yUkflXY3yHWsNtsrgCXQ3HjAIFUeZB-w","type":"public-key","rawId":"eHouz/Zi7+BmByHjJ/tx9h4a1WZsK4IzUmgGjkhyOodPGAyUqUp/B9yUkflXY3yHWsNtsrgCXQ3HjAIFUeZB+w==","response":{"authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4/krrmihjLHmVzzuoMdl2MBAAAAew==","clientDataJSON":"eyJjaGFsbGVuZ2UiOiJHMEpiTExuZGVmM2EwSXkzUzJzU1FBOHVPNFNPX3plNkZaTUF1UEk2LXhJIiwiY2xpZW50RXh0ZW5zaW9ucyI6e30sImhhc2hBbGdvcml0aG0iOiJTSEEtMjU2Iiwib3JpZ2luIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6ODQ0MyIsInR5cGUiOiJ3ZWJhdXRobi5nZXQifQ==","signature":"MEUCIEY/vcNkbo/LdMTfLa24ZYLlMMVMRd8zXguHBvqud9AJAiEAwCwpZpvcMaqCrwv85w/8RGiZzE+gOM61ffxmgEDeyhM=","userHandle":null}}'; + $challenge = 'G0JbLLndef3a0Iy3S2sSQA8uO4SO/ze6FZMAuPI6+xI='; + + $allowedCredentials = [ + [ + 'type' => 'public-key', + 'id' => 'eHouz_Zi7-BmByHjJ_tx9h4a1WZsK4IzUmgGjkhyOodPGAyUqUp_B9yUkflXY3yHWsNtsrgCXQ3HjAIFUeZB-w', + ], + ]; + + $throwable = null; + try { + $server->parseAndValidateAssertionResponse( + $authenticatorResponse, + $allowedCredentials, + $challenge, + $request, + ); + } catch (Throwable $throwable) { + throw $throwable; + } + + /** @psalm-suppress RedundantCondition */ + self::assertNull($throwable); + } + + /** @see https://github.com/web-auth/webauthn-framework/blob/v3.3.12/tests/library/Functional/NoneAttestationStatementTest.php#L45 */ + public function testParseAndValidateAttestationResponse(): void + { + $uriStub = self::createStub(UriInterface::class); + $uriStub->method('getHost')->willReturn('localhost'); + $request = self::createStub(ServerRequest::class); + $request->method('getUri')->willReturn($uriStub); + + // phpcs:ignore Generic.Files.LineLength.TooLong + $options = '{"rp":{"name":"My Application"},"pubKeyCredParams":[{"type":"public-key","alg":-7}],"challenge":"9WqgpRIYvGMCUYiFT20o1U7hSD193k11zu4tKP7wRcrE26zs1zc4LHyPinvPGS86wu6bDvpwbt8Xp2bQ3VBRSQ==","attestation":"none","user":{"name":"test@foo.com","id":"MJr5sD0WitVwZM0eoSO6kWhyseT67vc3oQdk\/k1VdZQ=","displayName":"Test PublicKeyCredentialUserEntity"},"authenticatorSelection":{"requireResidentKey":false,"userVerification":"preferred"}}'; + // phpcs:ignore Generic.Files.LineLength.TooLong + $response = '{"id":"mMihuIx9LukswxBOMjMHDf6EAONOy7qdWhaQQ7dOtViR2cVB_MNbZxURi2cvgSvKSILb3mISe9lPNG9sYgojuY5iNinYOg6hRVxmm0VssuNG2pm1-RIuTF9DUtEJZEEK","type":"public-key","rawId":"mMihuIx9LukswxBOMjMHDf6EAONOy7qdWhaQQ7dOtViR2cVB/MNbZxURi2cvgSvKSILb3mISe9lPNG9sYgojuY5iNinYOg6hRVxmm0VssuNG2pm1+RIuTF9DUtEJZEEK","response":{"clientDataJSON":"eyJjaGFsbGVuZ2UiOiI5V3FncFJJWXZHTUNVWWlGVDIwbzFVN2hTRDE5M2sxMXp1NHRLUDd3UmNyRTI2enMxemM0TEh5UGludlBHUzg2d3U2YkR2cHdidDhYcDJiUTNWQlJTUSIsImNsaWVudEV4dGVuc2lvbnMiOnt9LCJoYXNoQWxnb3JpdGhtIjoiU0hBLTI1NiIsIm9yaWdpbiI6Imh0dHBzOi8vbG9jYWxob3N0Ojg0NDMiLCJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIn0=","attestationObject":"o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjkSZYN5YgOjGh0NBcPZHZgW4/krrmihjLHmVzzuoMdl2NBAAAAAAAAAAAAAAAAAAAAAAAAAAAAYJjIobiMfS7pLMMQTjIzBw3+hADjTsu6nVoWkEO3TrVYkdnFQfzDW2cVEYtnL4ErykiC295iEnvZTzRvbGIKI7mOYjYp2DoOoUVcZptFbLLjRtqZtfkSLkxfQ1LRCWRBCqUBAgMmIAEhWCAcPxwKyHADVjTgTsat4R/Jax6PWte50A8ZasMm4w6RxCJYILt0FCiGwC6rBrh3ySNy0yiUjZpNGAhW+aM9YYyYnUTJ"}}'; + + $server = new CustomServer(); + $credential = $server->parseAndValidateAttestationResponse($response, $options, $request); + + self::assertSame( + [ + // phpcs:ignore Generic.Files.LineLength.TooLong + 'publicKeyCredentialId' => 'mMihuIx9LukswxBOMjMHDf6EAONOy7qdWhaQQ7dOtViR2cVB_MNbZxURi2cvgSvKSILb3mISe9lPNG9sYgojuY5iNinYOg6hRVxmm0VssuNG2pm1-RIuTF9DUtEJZEEK', + 'type' => 'public-key', + 'transports' => [], + 'attestationType' => 'none', + 'aaguid' => hex2bin('00000000000000000000000000000000'), + // phpcs:ignore Generic.Files.LineLength.TooLong + 'credentialPublicKey' => 'pQECAyYgASFYIBw_HArIcANWNOBOxq3hH8lrHo9a17nQDxlqwybjDpHEIlggu3QUKIbALqsGuHfJI3LTKJSNmk0YCFb5oz1hjJidRMk', + 'userHandle' => 'MJr5sD0WitVwZM0eoSO6kWhyseT67vc3oQdk_k1VdZQ', + 'counter' => 0, + ], + $credential, + ); + } +} diff --git a/tests/unit/WebAuthn/WebauthnLibServerTest.php b/tests/unit/WebAuthn/WebauthnLibServerTest.php index 25fbef682c..51b80b4a88 100644 --- a/tests/unit/WebAuthn/WebauthnLibServerTest.php +++ b/tests/unit/WebAuthn/WebauthnLibServerTest.php @@ -4,10 +4,12 @@ declare(strict_types=1); namespace PhpMyAdmin\Tests\WebAuthn; +use PhpMyAdmin\Http\ServerRequest; use PhpMyAdmin\TwoFactor; use PhpMyAdmin\WebAuthn\WebauthnLibServer; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; +use Psr\Http\Message\UriInterface; use Webauthn\Server as WebauthnServer; use Webauthn\TrustPath\EmptyTrustPath; @@ -15,7 +17,7 @@ use function base64_encode; use function class_exists; #[CoversClass(WebauthnLibServer::class)] -class WebauthnLibServerTest extends TestCase +final class WebauthnLibServerTest extends TestCase { protected function setUp(): void { @@ -32,8 +34,11 @@ class WebauthnLibServerTest extends TestCase { $server = new WebauthnLibServer(self::createStub(TwoFactor::class)); $options = $server->getCredentialCreationOptions('user_name', 'user_id', 'test.localhost'); + self::assertArrayHasKey('challenge', $options); self::assertNotEmpty($options['challenge']); + self::assertArrayHasKey('pubKeyCredParams', $options); self::assertNotEmpty($options['pubKeyCredParams']); + self::assertArrayHasKey('attestation', $options); self::assertNotEmpty($options['attestation']); self::assertSame('phpMyAdmin (test.localhost)', $options['rp']['name']); self::assertSame('test.localhost', $options['rp']['id']); @@ -70,7 +75,12 @@ class WebauthnLibServerTest extends TestCase ]; $server = new WebauthnLibServer($twoFactor); - $options = $server->getCredentialRequestOptions('user_name', 'userHandle1', 'test.localhost', []); + $options = $server->getCredentialRequestOptions( + 'user_name', + 'userHandle1', + 'test.localhost', + [['type' => 'public-key', 'id' => 'cHVibGljS2V5Q3JlZGVudGlhbElkMQ==']], + ); self::assertNotEmpty($options['challenge']); self::assertSame('test.localhost', $options['rpId']); self::assertSame( @@ -78,4 +88,119 @@ class WebauthnLibServerTest extends TestCase $options['allowCredentials'], ); } + + /** + * @see https://github.com/web-auth/webauthn-framework/blob/v3.3.12/tests/library/Functional/AssertionTest.php#L46 + * + * @requires extension bcmath + */ + public function testParseAndValidateAssertionResponse(): void + { + $twoFactor = self::createStub(TwoFactor::class); + $twoFactor->user = 'foo'; + $twoFactor->config = [ + 'backend' => 'WebAuthn', + 'settings' => [ + 'userHandle' => 'Zm9v', + 'credentials' => [ + 'eHouz/Zi7+BmByHjJ/tx9h4a1WZsK4IzUmgGjkhyOodPGAyUqUp/B9yUkflXY3yHWsNtsrgCXQ3HjAIFUeZB+w==' => [ + // phpcs:ignore Generic.Files.LineLength.TooLong + 'publicKeyCredentialId' => 'eHouz_Zi7-BmByHjJ_tx9h4a1WZsK4IzUmgGjkhyOodPGAyUqUp_B9yUkflXY3yHWsNtsrgCXQ3HjAIFUeZB-w', + 'type' => 'public-key', + 'transports' => [], + 'attestationType' => 'none', + 'aaguid' => '00000000-0000-0000-0000-000000000000', + // phpcs:ignore Generic.Files.LineLength.TooLong + 'credentialPublicKey' => 'pQECAyYgASFYIJV56vRrFusoDf9hm3iDmllcxxXzzKyO9WruKw4kWx7zIlgg_nq63l8IMJcIdKDJcXRh9hoz0L-nVwP1Oxil3_oNQYs', + 'userHandle' => 'Zm9v', + 'counter' => 100, + 'otherUI' => null, + ], + ], + ], + ]; + + $server = new WebauthnLibServer($twoFactor); + + $uriStub = self::createStub(UriInterface::class); + $uriStub->method('getHost')->willReturn('localhost'); + $request = self::createStub(ServerRequest::class); + $request->method('getUri')->willReturn($uriStub); + + // phpcs:ignore Generic.Files.LineLength.TooLong + $authenticatorResponse = '{"id":"eHouz_Zi7-BmByHjJ_tx9h4a1WZsK4IzUmgGjkhyOodPGAyUqUp_B9yUkflXY3yHWsNtsrgCXQ3HjAIFUeZB-w","type":"public-key","rawId":"eHouz/Zi7+BmByHjJ/tx9h4a1WZsK4IzUmgGjkhyOodPGAyUqUp/B9yUkflXY3yHWsNtsrgCXQ3HjAIFUeZB+w==","response":{"authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4/krrmihjLHmVzzuoMdl2MBAAAAew==","clientDataJSON":"eyJjaGFsbGVuZ2UiOiJHMEpiTExuZGVmM2EwSXkzUzJzU1FBOHVPNFNPX3plNkZaTUF1UEk2LXhJIiwiY2xpZW50RXh0ZW5zaW9ucyI6e30sImhhc2hBbGdvcml0aG0iOiJTSEEtMjU2Iiwib3JpZ2luIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6ODQ0MyIsInR5cGUiOiJ3ZWJhdXRobi5nZXQifQ==","signature":"MEUCIEY/vcNkbo/LdMTfLa24ZYLlMMVMRd8zXguHBvqud9AJAiEAwCwpZpvcMaqCrwv85w/8RGiZzE+gOM61ffxmgEDeyhM=","userHandle":null}}'; + $challenge = 'G0JbLLndef3a0Iy3S2sSQA8uO4SO/ze6FZMAuPI6+xI='; + + $allowedCredentials = [ + [ + 'type' => 'public-key', + 'id' => 'eHouz_Zi7-BmByHjJ_tx9h4a1WZsK4IzUmgGjkhyOodPGAyUqUp_B9yUkflXY3yHWsNtsrgCXQ3HjAIFUeZB-w', + ], + ]; + + $server->parseAndValidateAssertionResponse($authenticatorResponse, $allowedCredentials, $challenge, $request); + + /** + * @psalm-suppress TypeDoesNotContainType + * @phpstan-ignore-next-line + */ + self::assertSame( + [ + 'eHouz/Zi7+BmByHjJ/tx9h4a1WZsK4IzUmgGjkhyOodPGAyUqUp/B9yUkflXY3yHWsNtsrgCXQ3HjAIFUeZB+w==' => [ + // phpcs:ignore Generic.Files.LineLength.TooLong + 'publicKeyCredentialId' => 'eHouz_Zi7-BmByHjJ_tx9h4a1WZsK4IzUmgGjkhyOodPGAyUqUp_B9yUkflXY3yHWsNtsrgCXQ3HjAIFUeZB-w', + 'type' => 'public-key', + 'transports' => [], + 'attestationType' => 'none', + 'trustPath' => ['type' => 'Webauthn\\TrustPath\\EmptyTrustPath'], + 'aaguid' => '00000000-0000-0000-0000-000000000000', + // phpcs:ignore Generic.Files.LineLength.TooLong + 'credentialPublicKey' => 'pQECAyYgASFYIJV56vRrFusoDf9hm3iDmllcxxXzzKyO9WruKw4kWx7zIlgg_nq63l8IMJcIdKDJcXRh9hoz0L-nVwP1Oxil3_oNQYs', + 'userHandle' => 'Zm9v', + 'counter' => 123, + 'otherUI' => null, + ], + ], + $twoFactor->config['settings']['credentials'], + ); + } + + /** @see https://github.com/web-auth/webauthn-framework/blob/v3.3.12/tests/library/Functional/NoneAttestationStatementTest.php#L45 */ + public function testParseAndValidateAttestationResponse(): void + { + $twoFactor = self::createStub(TwoFactor::class); + $twoFactor->user = ''; + $twoFactor->config = ['backend' => 'WebAuthn', 'settings' => ['userHandle' => '', 'credentials' => []]]; + + $uriStub = self::createStub(UriInterface::class); + $uriStub->method('getHost')->willReturn('localhost'); + $request = self::createStub(ServerRequest::class); + $request->method('getUri')->willReturn($uriStub); + + // phpcs:ignore Generic.Files.LineLength.TooLong + $options = '{"rp":{"name":"My Application"},"pubKeyCredParams":[{"type":"public-key","alg":-7}],"challenge":"9WqgpRIYvGMCUYiFT20o1U7hSD193k11zu4tKP7wRcrE26zs1zc4LHyPinvPGS86wu6bDvpwbt8Xp2bQ3VBRSQ==","attestation":"none","user":{"name":"test@foo.com","id":"MJr5sD0WitVwZM0eoSO6kWhyseT67vc3oQdk\/k1VdZQ=","displayName":"Test PublicKeyCredentialUserEntity"},"authenticatorSelection":{"requireResidentKey":false,"userVerification":"preferred"}}'; + // phpcs:ignore Generic.Files.LineLength.TooLong + $response = '{"id":"mMihuIx9LukswxBOMjMHDf6EAONOy7qdWhaQQ7dOtViR2cVB_MNbZxURi2cvgSvKSILb3mISe9lPNG9sYgojuY5iNinYOg6hRVxmm0VssuNG2pm1-RIuTF9DUtEJZEEK","type":"public-key","rawId":"mMihuIx9LukswxBOMjMHDf6EAONOy7qdWhaQQ7dOtViR2cVB/MNbZxURi2cvgSvKSILb3mISe9lPNG9sYgojuY5iNinYOg6hRVxmm0VssuNG2pm1+RIuTF9DUtEJZEEK","response":{"clientDataJSON":"eyJjaGFsbGVuZ2UiOiI5V3FncFJJWXZHTUNVWWlGVDIwbzFVN2hTRDE5M2sxMXp1NHRLUDd3UmNyRTI2enMxemM0TEh5UGludlBHUzg2d3U2YkR2cHdidDhYcDJiUTNWQlJTUSIsImNsaWVudEV4dGVuc2lvbnMiOnt9LCJoYXNoQWxnb3JpdGhtIjoiU0hBLTI1NiIsIm9yaWdpbiI6Imh0dHBzOi8vbG9jYWxob3N0Ojg0NDMiLCJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIn0=","attestationObject":"o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjkSZYN5YgOjGh0NBcPZHZgW4/krrmihjLHmVzzuoMdl2NBAAAAAAAAAAAAAAAAAAAAAAAAAAAAYJjIobiMfS7pLMMQTjIzBw3+hADjTsu6nVoWkEO3TrVYkdnFQfzDW2cVEYtnL4ErykiC295iEnvZTzRvbGIKI7mOYjYp2DoOoUVcZptFbLLjRtqZtfkSLkxfQ1LRCWRBCqUBAgMmIAEhWCAcPxwKyHADVjTgTsat4R/Jax6PWte50A8ZasMm4w6RxCJYILt0FCiGwC6rBrh3ySNy0yiUjZpNGAhW+aM9YYyYnUTJ"}}'; + + $server = new WebauthnLibServer($twoFactor); + $credential = $server->parseAndValidateAttestationResponse($response, $options, $request); + + self::assertSame( + [ + // phpcs:ignore Generic.Files.LineLength.TooLong + 'publicKeyCredentialId' => 'mMihuIx9LukswxBOMjMHDf6EAONOy7qdWhaQQ7dOtViR2cVB_MNbZxURi2cvgSvKSILb3mISe9lPNG9sYgojuY5iNinYOg6hRVxmm0VssuNG2pm1-RIuTF9DUtEJZEEK', + 'type' => 'public-key', + 'transports' => [], + 'attestationType' => 'none', + 'trustPath' => ['type' => 'Webauthn\\TrustPath\\EmptyTrustPath'], + 'aaguid' => '00000000-0000-0000-0000-000000000000', + // phpcs:ignore Generic.Files.LineLength.TooLong + 'credentialPublicKey' => 'pQECAyYgASFYIBw_HArIcANWNOBOxq3hH8lrHo9a17nQDxlqwybjDpHEIlggu3QUKIbALqsGuHfJI3LTKJSNmk0YCFb5oz1hjJidRMk', + 'userHandle' => 'MJr5sD0WitVwZM0eoSO6kWhyseT67vc3oQdk_k1VdZQ', + 'counter' => 0, + 'otherUI' => null, + ], + $credential, + ); + } }