Skip to content

Commit

Permalink
Merge pull request #38 from creative-commoners/pulls/4.0/moaaar-tests
Browse files Browse the repository at this point in the history
Minor refactoring in VerifyHandler for test mocking and add tests for it
  • Loading branch information
Garion Herman committed Jun 25, 2019
2 parents 81d4578 + 8f0e300 commit c620f45
Show file tree
Hide file tree
Showing 2 changed files with 250 additions and 35 deletions.
85 changes: 50 additions & 35 deletions src/VerifyHandler.php
Expand Up @@ -2,6 +2,7 @@

namespace SilverStripe\WebAuthn;

use CBOR\Decoder;
use Exception;
use GuzzleHttp\Psr7\ServerRequest;
use Psr\Log\LoggerInterface;
Expand Down Expand Up @@ -34,7 +35,7 @@ class VerifyHandler implements VerifyHandlerInterface
/**
* @var LoggerInterface
*/
protected $logger = null;
protected $logger;

/**
* Sets the {@see $logger} member variable
Expand Down Expand Up @@ -71,52 +72,45 @@ public function start(StoreInterface $store, RegisteredMethod $method): array
* @param StoreInterface $store
* @param RegisteredMethod $registeredMethod The RegisteredMethod instance that is being verified
* @return Result
* @throws Exception
*/
public function verify(HTTPRequest $request, StoreInterface $store, RegisteredMethod $registeredMethod): Result
{
$options = $this->getCredentialRequestOptions($store, $registeredMethod);

$data = json_decode($request->getBody(), true);

$decoder = $this->getDecoder();
$attestationStatementSupportManager = $this->getAttestationStatementSupportManager($decoder);
$attestationObjectLoader = $this->getAttestationObjectLoader($attestationStatementSupportManager, $decoder);
$publicKeyCredentialLoader = $this->getPublicKeyCredentialLoader($attestationObjectLoader, $decoder);

$credentialRepository = new CredentialRepository($store->getMember(), $registeredMethod);
$data = json_decode((string) $request->getBody(), true);

$authenticatorAssertionResponseValidator = new AuthenticatorAssertionResponseValidator(
$credentialRepository,
$decoder,
new TokenBindingNotSupportedHandler(),
new ExtensionOutputCheckerHandler()
);
try {
if (empty($data['credentials'])) {
throw new ResponseDataException('Incomplete data, required information missing');
}

// Create a PSR-7 request
$psrRequest = ServerRequest::fromGlobals();
$decoder = $this->getDecoder();
$attestationStatementSupportManager = $this->getAttestationStatementSupportManager($decoder);
$attestationObjectLoader = $this->getAttestationObjectLoader($attestationStatementSupportManager, $decoder);
$publicKeyCredential = $this
->getPublicKeyCredentialLoader($attestationObjectLoader, $decoder)
->load(base64_decode($data['credentials']));

try {
$publicKeyCredential = $publicKeyCredentialLoader->load(base64_decode($data['credentials']));
$response = $publicKeyCredential->getResponse();

if (!$response instanceof AuthenticatorAssertionResponse) {
throw new ResponseTypeException('Unexpected response type found');
}

$authenticatorAssertionResponseValidator->check(
$publicKeyCredential->getRawId(),
$publicKeyCredential->getResponse(),
$options,
$psrRequest,
(string) $store->getMember()->ID
);

return Result::create();
// Create a PSR-7 request
$psrRequest = ServerRequest::fromGlobals();

$this->getAuthenticatorAssertionResponseValidator($decoder, $store, $registeredMethod)
->check(
$publicKeyCredential->getRawId(),
$response,
$this->getCredentialRequestOptions($store, $registeredMethod),
$psrRequest,
(string) $store->getMember()->ID
);
} catch (Exception $e) {
$this->logger->error($e->getMessage());
throw $e;
return Result::create(false, 'Verification failed: ' . $e->getMessage());
}

return Result::create();
}

/**
Expand Down Expand Up @@ -159,8 +153,8 @@ protected function getCredentialRequestOptions(
return PublicKeyCredentialRequestOptions::createFromArray($state['credentialOptions']);
}

$data = json_decode($registeredMethod->Data, true);
$descriptor = PublicKeyCredentialDescriptor::createFromArray($data['descriptor']);
$data = json_decode((string) $registeredMethod->Data, true) ?? [];
$descriptor = PublicKeyCredentialDescriptor::createFromArray($data['descriptor'] ?? []);

$options = new PublicKeyCredentialRequestOptions(
random_bytes(32),
Expand All @@ -175,4 +169,25 @@ protected function getCredentialRequestOptions(

return $options;
}

/**
* @param Decoder $decoder
* @param StoreInterface $store
* @param RegisteredMethod $registeredMethod
* @return AuthenticatorAssertionResponseValidator
*/
protected function getAuthenticatorAssertionResponseValidator(
Decoder $decoder,
StoreInterface $store,
RegisteredMethod $registeredMethod
): AuthenticatorAssertionResponseValidator {
$credentialRepository = new CredentialRepository($store->getMember(), $registeredMethod);

return new AuthenticatorAssertionResponseValidator(
$credentialRepository,
$decoder,
new TokenBindingNotSupportedHandler(),
new ExtensionOutputCheckerHandler()
);
}
}
200 changes: 200 additions & 0 deletions tests/VerifyHandlerTest.php
@@ -0,0 +1,200 @@
<?php

namespace SilverStripe\WebAuthn\Tests;

use Exception;
use PHPUnit_Framework_MockObject_MockObject;
use Psr\Log\LoggerInterface;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\MFA\Model\RegisteredMethod;
use SilverStripe\MFA\State\Result;
use SilverStripe\MFA\Store\SessionStore;
use SilverStripe\Security\Member;
use SilverStripe\WebAuthn\VerifyHandler;
use Webauthn\AuthenticatorAssertionResponse;
use Webauthn\AuthenticatorAssertionResponseValidator;
use Webauthn\AuthenticatorAttestationResponse;
use Webauthn\AuthenticatorResponse;
use Webauthn\PublicKeyCredential;
use Webauthn\PublicKeyCredentialLoader;

class VerifyHandlerTest extends SapphireTest
{
protected $usesDatabase = true;

/**
* @var VerifyHandler
*/
protected $handler;

/**
* @var Member
*/
protected $member;

/**
* @var HTTPRequest
*/
protected $request;

/**
* @var SessionStore
*/
protected $store;

/**
* @var RegisteredMethod
*/
protected $registeredMethod;

/**
* @var array
*/
protected $mockData = [];

protected function setUp()
{
parent::setUp();

$this->request = new HTTPRequest('GET', '/');
$this->handler = Injector::inst()->create(VerifyHandler::class);

$memberID = $this->logInWithPermission();
/** @var Member $member */
$this->member = Member::get()->byID($memberID);

$this->store = new SessionStore($this->member);

$this->registeredMethod = new RegisteredMethod();
$this->registeredMethod->Data = json_encode([
'descriptor' => [
'type' => 'public-key',
'id' => '7lE6zdHESCF3/qSijHVuTwlTNi/yZSD/XP6Nm6HBI8YLA0uzPqyU4U4RxyZyuKXEPiIENEr509TekP2mDKrFoQ=='
],
'data' => [
'aaguid' => 'AAAAAAAAAAAAAAAAAAAAAA==',
'credentialId' => '7lE6zdHESCF3/qSijHVuTwlTNi/yZSD/XP6Nm6HBI8YLPiIENEr509TekP2mDKrFoQ==',
'credentialPublicKey' => 'pU4vyn6OmHbdDyx7nWsJD+/2CycZkGzJ1u3TVj+c='
],
'counter' => null,
]);
}

/**
* @expectedException \Assert\InvalidArgumentException
*/
public function testStartThrowsExceptionWithMissingData()
{
$this->registeredMethod->Data = null;
$this->handler->start($this->store, $this->registeredMethod);
}

public function testStart()
{
$result = $this->handler->start($this->store, $this->registeredMethod);
$this->assertArrayHasKey('publicKey', $result);
}

public function testVerifyReturnsErrorWhenRequiredInformationIsMissing()
{
$this->registeredMethod->Data = null;
$result = $this->handler->verify($this->request, $this->store, $this->registeredMethod);

$this->assertFalse($result->isSuccessful());
$this->assertContains('Incomplete data', $result->getMessage());
}

/**
* @param AuthenticatorResponse $mockResponse
* @param Result $expectedResult
* @param callable $responseValidatorMockCallback
* @dataProvider verifyProvider
*/
public function testVerify(
$mockResponse,
$expectedResult,
callable $responseValidatorMockCallback = null
) {
/** @var VerifyHandler&PHPUnit_Framework_MockObject_MockObject $handlerMock */
$handlerMock = $this->getMockBuilder(VerifyHandler::class)
->setMethods(['getPublicKeyCredentialLoader', 'getAuthenticatorAssertionResponseValidator'])
->getMock();

$responseValidatorMock = $this->createMock(AuthenticatorAssertionResponseValidator::class);
// Allow the data provider to customise the validation check handling
if ($responseValidatorMockCallback) {
$responseValidatorMockCallback($responseValidatorMock);
}
$handlerMock->expects($this->any())->method('getAuthenticatorAssertionResponseValidator')
->willReturn($responseValidatorMock);

$loggerMock = $this->createMock(LoggerInterface::class);
$handlerMock->setLogger($loggerMock);

$loaderMock = $this->createMock(PublicKeyCredentialLoader::class);
$handlerMock->expects($this->once())->method('getPublicKeyCredentialLoader')->willReturn($loaderMock);

$publicKeyCredentialMock = $this->createMock(PublicKeyCredential::class);
$loaderMock->expects($this->once())->method('load')->with('example')->willReturn(
$publicKeyCredentialMock
);

$publicKeyCredentialMock->expects($this->once())->method('getResponse')->willReturn($mockResponse);

$this->request->setBody(json_encode([
'credentials' => base64_encode('example'),
]));
$result = $handlerMock->verify($this->request, $this->store, $this->registeredMethod);

$this->assertSame($expectedResult->isSuccessful(), $result->isSuccessful());
if ($expectedResult->getMessage()) {
$this->assertContains($expectedResult->getMessage(), $result->getMessage());
}
}

/**
* Some centralised or reusable logic for testVerify. Note that some of the mocks are only used in some of the
* provided data scenarios, but any expected call numbers are based on all scenarios being run.
*
* @return array[]
*/
public function verifyProvider()
{
return [
'wrong response return type' => [
// Deliberately the wrong child implementation of \Webauthn\AuthenticatorResponse
$this->createMock(AuthenticatorAttestationResponse::class),
new Result(false, 'Unexpected response type found'),
],
'valid response' => [
$this->createMock(AuthenticatorAssertionResponse::class),
new Result(true),
function (PHPUnit_Framework_MockObject_MockObject $responseValidatorMock) {
// Specifically setting expectations for the result of the response validator's "check" call
$responseValidatorMock->expects($this->once())->method('check')->willReturn(true);
},
],
'invalid response' => [
$this->createMock(AuthenticatorAssertionResponse::class),
new Result(false, 'I am a test'),
function (PHPUnit_Framework_MockObject_MockObject $responseValidatorMock) {
// Specifically setting expectations for the result of the response validator's "check" call
$responseValidatorMock->expects($this->once())->method('check')
->willThrowException(new Exception('I am a test'));
},
],
];
}

public function testGetLeadInLabel()
{
$this->assertContains('security key', $this->handler->getLeadInLabel());
}

public function testGetComponent()
{
$this->assertSame('WebAuthnVerify', $this->handler->getComponent());
}
}

0 comments on commit c620f45

Please sign in to comment.