$credentials */
+ $credentials = $this->twoFactor->config['settings']['credentials'];
+ foreach ($credentials as &$credential) {
+ if (isset($credential['trustPath'])) {
+ continue;
+ }
+
+ $credential['trustPath'] = ['type' => EmptyTrustPath::class];
+ }
+
+ return $credentials;
+ }
+
+ /**
+ * @param mixed[] $data
+ */
+ private function write(array $data): void
+ {
+ $this->twoFactor->config['settings']['credentials'] = $data;
+ }
+ };
+ }
+}
diff --git a/psalm.xml b/psalm.xml
index 05f5923b9d..b24ee723e8 100644
--- a/psalm.xml
+++ b/psalm.xml
@@ -283,6 +283,7 @@
proc_priv: bool,
querytime: float|int,
read_limit: int,
+ request: PhpMyAdmin\Http\ServerRequest,
save_on_server: bool,
server: int,
SESSION_KEY: string,
diff --git a/scripts/create-release.sh b/scripts/create-release.sh
index 48b075bacd..e6a50e412d 100755
--- a/scripts/create-release.sh
+++ b/scripts/create-release.sh
@@ -222,7 +222,18 @@ cleanup_composer_vendors() {
vendor/phpmyadmin/shapefile/CONTRIBUTING.md \
vendor/phpmyadmin/shapefile/CODE_OF_CONDUCT.md \
vendor/phpmyadmin/sql-parser/CODE_OF_CONDUCT.md \
- vendor/phpmyadmin/sql-parser/CONTRIBUTING.md
+ vendor/phpmyadmin/sql-parser/CONTRIBUTING.md \
+ vendor/beberlei/assert/.github/ \
+ vendor/brick/math/SECURITY.md \
+ vendor/brick/math/psalm-baseline.xml \
+ vendor/brick/math/psalm.xml \
+ vendor/ramsey/collection/SECURITY.md \
+ vendor/spomky-labs/base64url/.github/ \
+ vendor/spomky-labs/cbor-php/.php_cs.dist \
+ vendor/spomky-labs/cbor-php/CODE_OF_CONDUCT.md \
+ vendor/spomky-labs/cbor-php/infection.json.dist \
+ vendor/spomky-labs/cbor-php/phpstan.neon \
+ vendor/thecodingmachine/safe/generated/Exceptions/.gitkeep
find vendor/tecnickcom/tcpdf/fonts/ -maxdepth 1 -type f \
-not -name 'dejavusans.*' \
-not -name 'dejavusansb.*' \
@@ -270,6 +281,10 @@ security_checkup() {
echo 'TCPDF should be installed, detection failed !'
exit 1;
fi
+ if [ ! -f vendor/web-auth/webauthn-lib/src/Server.php ]; then
+ echo 'Webauthn-lib should be installed, detection failed !'
+ exit 1;
+ fi
if [ ! -f vendor/code-lts/u2f-php-server/src/U2FServer.php ]; then
echo 'U2F-server should be installed, detection failed !'
exit 1;
@@ -478,7 +493,7 @@ composer update --no-interaction --no-dev
# Parse the required versions from composer.json
PACKAGES_VERSIONS=''
-PACKAGE_LIST='tecnickcom/tcpdf pragmarx/google2fa-qrcode bacon/bacon-qr-code code-lts/u2f-php-server'
+PACKAGE_LIST='tecnickcom/tcpdf pragmarx/google2fa-qrcode bacon/bacon-qr-code code-lts/u2f-php-server web-auth/webauthn-lib'
for PACKAGES in $PACKAGE_LIST
do
diff --git a/templates/config/form_display/input.twig b/templates/config/form_display/input.twig
index 95fd2f777a..4b95a2df47 100644
--- a/templates/config/form_display/input.twig
+++ b/templates/config/form_display/input.twig
@@ -26,7 +26,7 @@
{% if type == 'text' %}
{% elseif type == 'password' %}
-
+
{% elseif type == 'short_text' and value is not iterable %}
{# https://github.com/phpmyadmin/phpmyadmin/issues/11505 #}
diff --git a/templates/login/form.twig b/templates/login/form.twig
index 5fa665e508..74886976d0 100644
--- a/templates/login/form.twig
+++ b/templates/login/form.twig
@@ -76,7 +76,7 @@
{% trans 'Username:' %}
-
+
@@ -85,7 +85,7 @@
{% trans 'Password:' %}
-
+
diff --git a/templates/login/twofactor/webauthn_creation.twig b/templates/login/twofactor/webauthn_creation.twig
new file mode 100644
index 0000000000..552a966976
--- /dev/null
+++ b/templates/login/twofactor/webauthn_creation.twig
@@ -0,0 +1,3 @@
+{{ 'Please connect your WebAuthn/FIDO2 device. Then confirm registration on the device.'|trans }}
+
+
diff --git a/templates/login/twofactor/webauthn_request.twig b/templates/login/twofactor/webauthn_request.twig
new file mode 100644
index 0000000000..5b13e7c246
--- /dev/null
+++ b/templates/login/twofactor/webauthn_request.twig
@@ -0,0 +1,3 @@
+{{ 'Please connect your WebAuthn/FIDO2 device. Then confirm login on the device.'|trans }}
+
+
diff --git a/templates/preferences/two_factor/main.twig b/templates/preferences/two_factor/main.twig
index 17ada02e25..9feba8833d 100644
--- a/templates/preferences/two_factor/main.twig
+++ b/templates/preferences/two_factor/main.twig
@@ -21,6 +21,17 @@
{% else %}
{% trans "Two-factor authentication is available, but not configured for this account." %}
{% endif %}
+ {% if missing|length > 0 %}
+
+ {{ 'Please install optional dependencies to enable more authentication backends.'|trans }}
+ {{ 'Following composer packages are missing:'|trans }}
+
+
+ {% for item in missing %}
+ {{ item.dep }} ({{ item.class }})
+ {% endfor %}
+
+ {% endif %}
{% endif %}
{% else %}
{% trans "Two-factor authentication is not available, enable phpMyAdmin configuration storage to use it." %}
diff --git a/templates/server/privileges/change_password.twig b/templates/server/privileges/change_password.twig
index 1a7c1c8faf..8952d7a91e 100644
--- a/templates/server/privileges/change_password.twig
+++ b/templates/server/privileges/change_password.twig
@@ -26,11 +26,11 @@
{% trans 'Enter:' %}
-
+
{% trans 'Re-type:' %}
-
+
|
diff --git a/templates/server/privileges/login_information_fields.twig b/templates/server/privileges/login_information_fields.twig
index 1f141956d5..32ac5d6193 100644
--- a/templates/server/privileges/login_information_fields.twig
+++ b/templates/server/privileges/login_information_fields.twig
@@ -9,7 +9,7 @@
-
@@ -51,7 +51,7 @@
-
+
{% trans %}Strength:{% context %}Password strength{% endtrans %}
@@ -60,7 +60,7 @@
-
+
diff --git a/templates/server/replication/change_primary.twig b/templates/server/replication/change_primary.twig
index 0d0c2debeb..38304171d8 100644
--- a/templates/server/replication/change_primary.twig
+++ b/templates/server/replication/change_primary.twig
@@ -16,7 +16,7 @@
-
+
diff --git a/templates/server/replication/primary_add_replica_user.twig b/templates/server/replication/primary_add_replica_user.twig
index bd82403e10..6140bcde4e 100644
--- a/templates/server/replication/primary_add_replica_user.twig
+++ b/templates/server/replication/primary_add_replica_user.twig
@@ -21,7 +21,7 @@
-
+
@@ -54,7 +54,7 @@
-
+
@@ -62,7 +62,7 @@
{% trans 'Re-type:' %}
-
+
diff --git a/test/classes/Plugins/Auth/AuthenticationCookieTest.php b/test/classes/Plugins/Auth/AuthenticationCookieTest.php
index 382903294b..beb9a1f21b 100644
--- a/test/classes/Plugins/Auth/AuthenticationCookieTest.php
+++ b/test/classes/Plugins/Auth/AuthenticationCookieTest.php
@@ -211,13 +211,13 @@ class AuthenticationCookieTest extends AbstractNetworkTestCase
$this->assertStringContainsString(
'',
+ 'value="pmauser" class="form-control" autocomplete="username" spellcheck="false" autofocus>',
$result
);
$this->assertStringContainsString(
'',
+ 'value="" class="form-control" autocomplete="current-password" spellcheck="false">',
$result
);
diff --git a/test/classes/Plugins/TwoFactor/WebAuthnTest.php b/test/classes/Plugins/TwoFactor/WebAuthnTest.php
new file mode 100644
index 0000000000..8c1b52ff97
--- /dev/null
+++ b/test/classes/Plugins/TwoFactor/WebAuthnTest.php
@@ -0,0 +1,299 @@
+assertSame('WebAuthn', WebAuthn::$id);
+ $this->assertSame('Hardware Security Key (WebAuthn/FIDO2)', WebAuthn::getName());
+ $this->assertSame(
+ 'Provides authentication using hardware security tokens supporting the WebAuthn/FIDO2 protocol,'
+ . ' such as a YubiKey.',
+ WebAuthn::getDescription()
+ );
+ }
+
+ public function testRender(): void
+ {
+ $GLOBALS['lang'] = 'en';
+ $GLOBALS['server'] = 1;
+ $GLOBALS['text_dir'] = 'ltr';
+ $GLOBALS['PMA_PHP_SELF'] = 'index.php';
+
+ $uri = $this->createStub(UriInterface::class);
+ $uri->method('getHost')->willReturn('test.localhost');
+ $request = $this->createStub(ServerRequest::class);
+ $request->method('getUri')->willReturn($uri);
+ $GLOBALS['request'] = $request;
+
+ $twoFactor = $this->createStub(TwoFactor::class);
+ $twoFactor->user = 'test_user';
+ $twoFactor->config = [
+ 'backend' => 'WebAuthn',
+ 'settings' => [
+ 'credentials' => [
+ // base64 of publicKeyCredentialId1
+ 'cHVibGljS2V5Q3JlZGVudGlhbElkMQ==' => [
+ // base64url of publicKeyCredentialId1
+ 'publicKeyCredentialId' => 'cHVibGljS2V5Q3JlZGVudGlhbElkMQ',
+ 'type' => 'public-key',
+ ],
+ // base64 of publicKeyCredentialId2
+ 'cHVibGljS2V5Q3JlZGVudGlhbElkMg==' => ['publicKeyCredentialId' => '', 'type' => ''],
+ ],
+ ],
+ ];
+
+ $expectedRequestOptions = [
+ 'challenge' => 'challenge',
+ 'allowCredentials' => [['type' => 'public-key', 'id' => 'cHVibGljS2V5Q3JlZGVudGlhbElkMQ']],
+ 'timeout' => 60000,
+ ];
+ $server = $this->createMock(Server::class);
+ $server->expects($this->once())->method('getCredentialRequestOptions')->with(
+ $this->equalTo('test_user'),
+ $this->anything(),
+ $this->equalTo('test.localhost'),
+ $this->equalTo([['type' => 'public-key', 'id' => 'cHVibGljS2V5Q3JlZGVudGlhbElkMQ']])
+ )->willReturn($expectedRequestOptions);
+
+ $webAuthn = new WebAuthn($twoFactor);
+ $webAuthn->setServer($server);
+ $actual = $webAuthn->render();
+
+ $optionsFromSession = $_SESSION['WebAuthnCredentialRequestOptions'] ?? null;
+ $this->assertIsString($optionsFromSession);
+ $this->assertJson($optionsFromSession);
+ $this->assertEquals($expectedRequestOptions, json_decode($optionsFromSession, true));
+
+ $this->assertStringContainsString('id="webauthn_request_response"', $actual);
+ $this->assertStringContainsString('name="webauthn_request_response"', $actual);
+ $this->assertStringContainsString('value=""', $actual);
+ $this->assertStringContainsString('data-request-options="', $actual);
+ $this->assertSame('', $webAuthn->getError());
+
+ $files = ResponseRenderer::getInstance()->getHeader()->getScripts()->getFiles();
+ $this->assertContains('webauthn.js', array_column($files, 'name'));
+ }
+
+ public function testSetup(): void
+ {
+ $GLOBALS['lang'] = 'en';
+ $GLOBALS['server'] = 1;
+ $GLOBALS['text_dir'] = 'ltr';
+ $GLOBALS['PMA_PHP_SELF'] = 'index.php';
+
+ $uri = $this->createStub(UriInterface::class);
+ $uri->method('getHost')->willReturn('test.localhost');
+ $request = $this->createStub(ServerRequest::class);
+ $request->method('getUri')->willReturn($uri);
+ $GLOBALS['request'] = $request;
+
+ $twoFactor = $this->createStub(TwoFactor::class);
+ $twoFactor->user = 'test_user';
+
+ $expectedCreationOptions = [
+ 'challenge' => 'challenge',
+ 'rp' => ['name' => 'phpMyAdmin (test.localhost)', 'id' => 'test.localhost'],
+ 'user' => ['id' => 'user_id', 'name' => 'test_user', 'displayName' => 'test_user'],
+ 'pubKeyCredParams' => [['alg' => -8, 'type' => 'public-key']],
+ 'authenticatorSelection' => ['authenticatorAttachment' => 'cross-platform'],
+ 'timeout' => 60000,
+ 'attestation' => 'none',
+ ];
+ $server = $this->createMock(Server::class);
+ $server->expects($this->once())->method('getCredentialCreationOptions')->with(
+ $this->equalTo('test_user'),
+ $this->anything(),
+ $this->equalTo('test.localhost')
+ )->willReturn($expectedCreationOptions);
+
+ $webAuthn = new WebAuthn($twoFactor);
+ $webAuthn->setServer($server);
+ $actual = $webAuthn->setup();
+
+ $optionsFromSession = $_SESSION['WebAuthnCredentialCreationOptions'] ?? null;
+ $this->assertIsString($optionsFromSession);
+ $this->assertJson($optionsFromSession);
+ $this->assertEquals($expectedCreationOptions, json_decode($optionsFromSession, true));
+
+ $this->assertStringContainsString('id="webauthn_creation_response"', $actual);
+ $this->assertStringContainsString('name="webauthn_creation_response"', $actual);
+ $this->assertStringContainsString('value=""', $actual);
+ $this->assertStringContainsString('data-creation-options="', $actual);
+ $this->assertSame('', $webAuthn->getError());
+
+ $files = ResponseRenderer::getInstance()->getHeader()->getScripts()->getFiles();
+ $this->assertContains('webauthn.js', array_column($files, 'name'));
+ }
+
+ public function testConfigure(): void
+ {
+ $_SESSION = [];
+ $request = $this->createStub(ServerRequest::class);
+ $request->method('getParsedBodyParam')->willReturnMap([['webauthn_creation_response', '', '']]);
+ $GLOBALS['request'] = $request;
+ $webAuthn = new WebAuthn($this->createStub(TwoFactor::class));
+ $this->assertFalse($webAuthn->configure());
+ $this->assertSame('', $webAuthn->getError());
+ }
+
+ public function testConfigure2(): void
+ {
+ $_SESSION['WebAuthnCredentialCreationOptions'] = '';
+ $request = $this->createStub(ServerRequest::class);
+ $request->method('getParsedBodyParam')->willReturnMap([['webauthn_creation_response', '', '{}']]);
+ $GLOBALS['request'] = $request;
+ $webAuthn = new WebAuthn($this->createStub(TwoFactor::class));
+ $this->assertFalse($webAuthn->configure());
+ $this->assertStringContainsString('Two-factor authentication failed:', $webAuthn->getError());
+ }
+
+ public function testConfigure3(): void
+ {
+ $_SESSION['WebAuthnCredentialCreationOptions'] = '{}';
+ $request = $this->createStub(ServerRequest::class);
+ $request->method('getParsedBodyParam')->willReturnMap([['webauthn_creation_response', '', '{}']]);
+ $GLOBALS['request'] = $request;
+
+ $server = $this->createMock(Server::class);
+ $server->expects($this->once())->method('parseAndValidateAttestationResponse')
+ ->willThrowException(new WebAuthnException());
+
+ $webAuthn = new WebAuthn($this->createStub(TwoFactor::class));
+ $webAuthn->setServer($server);
+ $this->assertFalse($webAuthn->configure());
+ $this->assertStringContainsString('Two-factor authentication failed.', $webAuthn->getError());
+ }
+
+ public function testConfigure4(): void
+ {
+ $_SESSION['WebAuthnCredentialCreationOptions'] = '{}';
+ $request = $this->createStub(ServerRequest::class);
+ $request->method('getParsedBodyParam')->willReturnMap([['webauthn_creation_response', '', '{}']]);
+ $GLOBALS['request'] = $request;
+
+ $twoFactor = $this->createStub(TwoFactor::class);
+ $twoFactor->config = ['backend' => '', 'settings' => []];
+
+ // base64url of publicKeyCredentialId1
+ $credential = ['publicKeyCredentialId' => 'cHVibGljS2V5Q3JlZGVudGlhbElkMQ', 'userHandle' => 'userHandle'];
+ $server = $this->createMock(Server::class);
+ $server->expects($this->once())->method('parseAndValidateAttestationResponse')->with(
+ $this->equalTo('{}'),
+ $this->equalTo('{}'),
+ $this->equalTo($request)
+ )->willReturn($credential);
+
+ $webAuthn = new WebAuthn($twoFactor);
+ $webAuthn->setServer($server);
+ $this->assertTrue($webAuthn->configure());
+ /** @psalm-var array{backend: string, settings: mixed[]} $config */
+ $config = $twoFactor->config;
+ $this->assertSame(
+ [
+ 'backend' => '',
+ 'settings' => [
+ 'userHandle' => 'userHandle',
+ 'credentials' => ['cHVibGljS2V5Q3JlZGVudGlhbElkMQ==' => $credential],
+ ],
+ ],
+ $config
+ );
+ }
+
+ public function testCheck(): void
+ {
+ $_SESSION = [];
+ $request = $this->createStub(ServerRequest::class);
+ $request->method('getParsedBodyParam')->willReturnMap([['webauthn_request_response', '', '']]);
+ $GLOBALS['request'] = $request;
+ $webAuthn = new WebAuthn($this->createStub(TwoFactor::class));
+ $this->assertFalse($webAuthn->check());
+ $this->assertSame('', $webAuthn->getError());
+ }
+
+ public function testCheck2(): void
+ {
+ $_SESSION['WebAuthnCredentialRequestOptions'] = '';
+ $request = $this->createStub(ServerRequest::class);
+ $request->method('getParsedBodyParam')->willReturnMap([['webauthn_request_response', '', '{}']]);
+ $GLOBALS['request'] = $request;
+ $webAuthn = new WebAuthn($this->createStub(TwoFactor::class));
+ $this->assertFalse($webAuthn->check());
+ $this->assertStringContainsString('Two-factor authentication failed:', $webAuthn->getError());
+ }
+
+ public function testCheck3(): void
+ {
+ $_SESSION['WebAuthnCredentialRequestOptions'] = '{"challenge":"challenge"}';
+ $request = $this->createStub(ServerRequest::class);
+ $request->method('getParsedBodyParam')->willReturnMap([['webauthn_request_response', '', '{}']]);
+ $GLOBALS['request'] = $request;
+
+ $server = $this->createMock(Server::class);
+ $server->expects($this->once())->method('parseAndValidateAssertionResponse')
+ ->willThrowException(new WebAuthnException());
+
+ $webAuthn = new WebAuthn($this->createStub(TwoFactor::class));
+ $webAuthn->setServer($server);
+ $this->assertFalse($webAuthn->check());
+ $this->assertStringContainsString('Two-factor authentication failed.', $webAuthn->getError());
+ }
+
+ public function testCheck4(): void
+ {
+ $_SESSION['WebAuthnCredentialRequestOptions'] = '{"challenge":"challenge"}';
+ $request = $this->createStub(ServerRequest::class);
+ $request->method('getParsedBodyParam')->willReturnMap([['webauthn_request_response', '', '{}']]);
+ $GLOBALS['request'] = $request;
+
+ $twoFactor = $this->createStub(TwoFactor::class);
+ $twoFactor->config = [
+ 'backend' => 'WebAuthn',
+ 'settings' => [
+ 'credentials' => [
+ // base64 of publicKeyCredentialId1
+ 'cHVibGljS2V5Q3JlZGVudGlhbElkMQ==' => [
+ // base64url of publicKeyCredentialId1
+ 'publicKeyCredentialId' => 'cHVibGljS2V5Q3JlZGVudGlhbElkMQ',
+ 'type' => 'public-key',
+ ],
+ ],
+ ],
+ ];
+
+ $server = $this->createMock(Server::class);
+ $server->expects($this->once())->method('parseAndValidateAssertionResponse')->with(
+ $this->equalTo('{}'),
+ $this->equalTo([['type' => 'public-key', 'id' => 'cHVibGljS2V5Q3JlZGVudGlhbElkMQ']]),
+ $this->equalTo('challenge'),
+ $this->equalTo($request)
+ );
+
+ $webAuthn = new WebAuthn($twoFactor);
+ $webAuthn->setServer($server);
+ $this->assertTrue($webAuthn->check());
+ }
+}
diff --git a/test/classes/WebAuthn/CBORDecoderTest.php b/test/classes/WebAuthn/CBORDecoderTest.php
new file mode 100644
index 0000000000..f43e43a25e
--- /dev/null
+++ b/test/classes/WebAuthn/CBORDecoderTest.php
@@ -0,0 +1,198 @@
+assertNotFalse($data);
+ $this->assertSame($expected, $decoder->decode(new DataStream($data)));
+ }
+
+ /**
+ * @psalm-return iterable
+ */
+ public function dataProviderForTestDecode(): iterable
+ {
+ return [
+ ['00', 0],
+ ['01', 1],
+ ['0a', 10],
+ ['17', 23],
+ ['1818', 24],
+ ['1819', 25],
+ ['1864', 100],
+ ['1903e8', 1000],
+ ['1a000f4240', 1000000],
+ //['1b000000e8d4a51000', 1000000000000],
+ //['1bffffffffffffffff', 18446744073709551615],
+ //['c249010000000000000000', 18446744073709551616],
+ //['3bffffffffffffffff', -18446744073709551616],
+ //['c349010000000000000000', -18446744073709551617],
+ ['20', -1],
+ ['29', -10],
+ ['3863', -100],
+ ['3903e7', -1000],
+ ['f90000', 0.0],
+ ['f98000', -0.0],
+ ['f93c00', 1.0],
+ ['fb3ff199999999999a', 1.1],
+ ['f93e00', 1.5],
+ ['f97bff', 65504.0],
+ ['fa47c35000', 100000.0],
+ ['fa7f7fffff', 3.4028234663852886e+38],
+ ['fb7e37e43c8800759c', 1.0e+300],
+ ['f90001', 5.960464477539063e-8],
+ ['f90400', 0.00006103515625],
+ ['f9c400', -4.0],
+ ['fbc010666666666666', -4.1],
+ ['f97c00', INF],
+ ['f9fc00', -INF],
+ ['fa7f800000', INF],
+ ['faff800000', -INF],
+ ['fb7ff0000000000000', INF],
+ ['fbfff0000000000000', -INF],
+ ['f4', true],
+ ['f5', false],
+ ['f6', null],
+ //['f7', 'undefined'],
+ ['f0', 16],
+ ['f818', 24],
+ ['f8ff', 255],
+ ['c074323031332d30332d32315432303a30343a30305a', '2013-03-21T20:04:00Z'],
+ ['c11a514b67b0', 1363896240],
+ ['c1fb41d452d9ec200000', 1363896240.5],
+ ['d74401020304', hex2bin('01020304')],
+ ['d818456449455446', hex2bin('6449455446')],
+ ['d82076687474703a2f2f7777772e6578616d706c652e636f6d', 'http://www.example.com'],
+ ['40', hex2bin('')],
+ ['4401020304', hex2bin('01020304')],
+ ['60', ''],
+ ['6161', 'a'],
+ ['6449455446', 'IETF'],
+ ['62225c', '"\\'],
+ ['62c3bc', "\u{00fc}"],
+ ['63e6b0b4', "\u{6c34}"],
+ ['64f0908591', "\u{10151}"], // "\u{d800}\u{dd51}"
+ ['80', []],
+ ['83010203', [1, 2, 3]],
+ ['8301820203820405', [1, [2, 3], [4, 5]]],
+ [
+ '98190102030405060708090a0b0c0d0e0f101112131415161718181819',
+ [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25],
+ ],
+ ['a0', []],
+ ['a201020304', [1 => 2, 3 => 4]],
+ ['a26161016162820203', ['a' => 1, 'b' => [2, 3]]],
+ ['826161a161626163', ['a', ['b' => 'c']]],
+ [
+ 'a56161614161626142616361436164614461656145',
+ ['a' => 'A', 'b' => 'B', 'c' => 'C', 'd' => 'D', 'e' => 'E'],
+ ],
+ ['a1646e616d656441636d65', ['name' => 'Acme']],
+ [
+ 'a462696458203082019330820138a0030201023082019330820138a003020102'
+ . '3082019330826469636f6e782b68747470733a2f2f706963732e6578616d706c'
+ . '652e636f6d2f30302f702f61426a6a6a707150622e706e67646e616d65766a6f'
+ . '686e70736d697468406578616d706c652e636f6d6b646973706c61794e616d65'
+ . '6d4a6f686e20502e20536d697468a462696458203082019330820138a0030201'
+ . '023082019330820138a0030201023082019330826469636f6e782b6874747073'
+ . '3a2f2f706963732e6578616d706c652e636f6d2f30302f702f61426a6a6a7071'
+ . '50622e706e67646e616d65766a6f686e70736d697468406578616d706c652e63'
+ . '6f6d6b646973706c61794e616d656d4a6f686e20502e20536d697468',
+ [
+ 'id' => base64_decode('MIIBkzCCATigAwIBAjCCAZMwggE4oAMCAQIwggGTMII='),
+ 'icon' => 'https://pics.example.com/00/p/aBjjjpqPb.png',
+ 'name' => 'johnpsmith@example.com',
+ 'displayName' => 'John P. Smith',
+ ],
+ ],
+ [
+ '82a263616c672664747970656a7075626C69632D6B6579a263616c6739010064747970656a7075626C69632D6B6579',
+ [
+ ['alg' => -7, 'type' => 'public-key'],
+ ['alg' => -257, 'type' => 'public-key'],
+ ],
+ ],
+ [
+ 'A501020326200121582065eda5a12577c2bae829437fe338701a10aaa375e1bb5b5de108de439c08551d'
+ . '2258201e52ed75701163f7f9e40ddf9f341b3dc9ba860af7e0ca7ca7e9eecd0084d19c',
+ [
+ 1 => 2,
+ 3 => -7,
+ -1 => 1,
+ -2 => hex2bin('65eda5a12577c2bae829437fe338701a10aaa375e1bb5b5de108de439c08551d'),
+ -3 => hex2bin('1e52ed75701163f7f9e40ddf9f341b3dc9ba860af7e0ca7ca7e9eecd0084d19c'),
+ ],
+ ],
+ ];
+ }
+
+ public function testDecodeForNaNValues(): void
+ {
+ $decoder = new CBORDecoder();
+ $nanValues = ['f97e00', 'fa7fc00000', 'fb7ff8000000000000'];
+ foreach ($nanValues as $value) {
+ $data = hex2bin($value);
+ $this->assertNotFalse($data);
+ $this->assertNan($decoder->decode(new DataStream($data)));
+ }
+ }
+
+ /**
+ * @dataProvider indefiniteLengthValuesProvider
+ */
+ public function testDecodeForNotSupportedValues(string $encoded): void
+ {
+ $decoder = new CBORDecoder();
+ $data = hex2bin($encoded);
+ $this->assertNotFalse($data);
+ $this->expectException(WebAuthnException::class);
+ $decoder->decode(new DataStream($data));
+ }
+
+ /**
+ * @psalm-return iterable
+ */
+ public function indefiniteLengthValuesProvider(): iterable
+ {
+ return [
+ ['5f42010243030405ff'], // (_ h'0102', h'030405')
+ ['7f657374726561646d696e67ff'], // (_ "strea", "ming")
+ ['9fff'], // [_ ]
+ ['9f018202039f0405ffff'], // [_ 1, [2, 3], [_ 4, 5]]
+ ['9f01820203820405ff'], // [_ 1, [2, 3], [4, 5]]
+ ['83018202039f0405ff'], // [1, [2, 3], [_ 4, 5]]
+ ['83019f0203ff820405'], // [1, [_ 2, 3], [4, 5]]
+ // [_ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25]
+ ['9f0102030405060708090a0b0c0d0e0f101112131415161718181819ff'],
+ ['bf61610161629f0203ffff'], // {_ "a": 1, "b": [_ 2, 3]}
+ ['826161bf61626163ff'], // ["a", {_ "b": "c"}]
+ ['bf6346756ef563416d7421ff'], // {_ "Fun": true, "Amt": -2}
+ ];
+ }
+}
diff --git a/test/classes/WebAuthn/WebauthnLibServerTest.php b/test/classes/WebAuthn/WebauthnLibServerTest.php
new file mode 100644
index 0000000000..5ff7f315bf
--- /dev/null
+++ b/test/classes/WebAuthn/WebauthnLibServerTest.php
@@ -0,0 +1,83 @@
+markTestSkipped('Package "web-auth/webauthn-lib" is required.');
+ }
+
+ public function testGetCredentialCreationOptions(): void
+ {
+ $server = new WebauthnLibServer($this->createStub(TwoFactor::class));
+ $options = $server->getCredentialCreationOptions('user_name', 'user_id', 'test.localhost');
+ $this->assertArrayHasKey('challenge', $options);
+ $this->assertNotEmpty($options['challenge']);
+ $this->assertArrayHasKey('pubKeyCredParams', $options);
+ $this->assertNotEmpty($options['pubKeyCredParams']);
+ $this->assertArrayHasKey('attestation', $options);
+ $this->assertNotEmpty($options['attestation']);
+ $this->assertSame('phpMyAdmin (test.localhost)', $options['rp']['name']);
+ $this->assertSame('test.localhost', $options['rp']['id']);
+ $this->assertSame('user_name', $options['user']['name']);
+ $this->assertSame('user_name', $options['user']['displayName']);
+ $this->assertSame(base64_encode('user_id'), $options['user']['id']);
+ $this->assertArrayHasKey('authenticatorAttachment', $options['authenticatorSelection']);
+ $this->assertSame('cross-platform', $options['authenticatorSelection']['authenticatorAttachment']);
+ }
+
+ public function testGetCredentialRequestOptions(): void
+ {
+ $twoFactor = $this->createStub(TwoFactor::class);
+ $twoFactor->config = [
+ 'backend' => 'WebAuthn',
+ 'settings' => [
+ 'credentials' => [
+ // base64 of publicKeyCredentialId1
+ 'cHVibGljS2V5Q3JlZGVudGlhbElkMQ==' => [
+ // base64url for publicKeyCredentialId1
+ 'publicKeyCredentialId' => 'cHVibGljS2V5Q3JlZGVudGlhbElkMQ',
+ 'type' => 'public-key',
+ 'transports' => [],
+ 'attestationType' => 'none',
+ 'trustPath' => ['type' => 'Webauthn\\TrustPath\\EmptyTrustPath'],
+ 'aaguid' => '00000000-0000-0000-0000-000000000000',
+ 'credentialPublicKey' => 'Y3JlZGVudGlhbFB1YmxpY0tleTE', // base64url for credentialPublicKey1
+ 'userHandle' => 'dXNlckhhbmRsZTE=', // base64 for userHandle1
+ 'counter' => 0,
+ 'otherUI' => null,
+ ],
+ ],
+ ],
+ ];
+
+ $server = new WebauthnLibServer($twoFactor);
+ $options = $server->getCredentialRequestOptions('user_name', 'userHandle1', 'test.localhost', []);
+ $this->assertNotEmpty($options['challenge']);
+ $this->assertSame('test.localhost', $options['rpId']);
+ $this->assertEquals(
+ [['type' => 'public-key', 'id' => 'cHVibGljS2V5Q3JlZGVudGlhbElkMQ==']],
+ $options['allowCredentials']
+ );
+ }
+}
|