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(), ); } #[BackupStaticProperties(true)] public function testRender(): void { (new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, null); $GLOBALS['lang'] = 'en'; $GLOBALS['server'] = 1; $GLOBALS['text_dir'] = 'ltr'; DatabaseInterface::$instance = $this->createDatabaseInterface(); $uri = $this->createStub(UriInterface::class); $uri->method('getHost')->willReturn('test.localhost'); $request = $this->createStub(ServerRequest::class); $request->method('getUri')->willReturn($uri); $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($request); $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')); } #[BackupStaticProperties(true)] public function testSetup(): void { (new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, null); $GLOBALS['lang'] = 'en'; $GLOBALS['server'] = 1; $GLOBALS['text_dir'] = 'ltr'; DatabaseInterface::$instance = $this->createDatabaseInterface(); $uri = $this->createStub(UriInterface::class); $uri->method('getHost')->willReturn('test.localhost'); $request = $this->createStub(ServerRequest::class); $request->method('getUri')->willReturn($uri); $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($request); $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', '', '']]); $webAuthn = new WebAuthn($this->createStub(TwoFactor::class)); $this->assertFalse($webAuthn->configure($request)); $this->assertSame('', $webAuthn->getError()); } public function testConfigure2(): void { $_SESSION['WebAuthnCredentialCreationOptions'] = ''; $request = $this->createStub(ServerRequest::class); $request->method('getParsedBodyParam')->willReturnMap([['webauthn_creation_response', '', '{}']]); $webAuthn = new WebAuthn($this->createStub(TwoFactor::class)); $this->assertFalse($webAuthn->configure($request)); $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', '', '{}']]); $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($request)); $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', '', '{}']]); $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($request)); /** @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', '', '']]); $webAuthn = new WebAuthn($this->createStub(TwoFactor::class)); $this->assertFalse($webAuthn->check($request)); $this->assertSame('', $webAuthn->getError()); } public function testCheck2(): void { $_SESSION['WebAuthnCredentialRequestOptions'] = ''; $request = $this->createStub(ServerRequest::class); $request->method('getParsedBodyParam')->willReturnMap([['webauthn_request_response', '', '{}']]); $webAuthn = new WebAuthn($this->createStub(TwoFactor::class)); $this->assertFalse($webAuthn->check($request)); $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', '', '{}']]); $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($request)); $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', '', '{}']]); $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($request)); } }