1 | <?php |
||
2 | |||
3 | namespace SilverStripe\WebAuthn\Tests; |
||
4 | |||
5 | use Exception; |
||
6 | use PHPUnit_Framework_MockObject_MockObject; |
||
7 | use Psr\Log\LoggerInterface; |
||
8 | use SilverStripe\Control\HTTPRequest; |
||
9 | use SilverStripe\Core\Injector\Injector; |
||
10 | use SilverStripe\Dev\SapphireTest; |
||
11 | use SilverStripe\MFA\Model\RegisteredMethod; |
||
12 | use SilverStripe\MFA\State\Result; |
||
13 | use SilverStripe\MFA\Store\SessionStore; |
||
14 | use SilverStripe\Security\Member; |
||
15 | use SilverStripe\WebAuthn\VerifyHandler; |
||
16 | use Webauthn\AuthenticatorAssertionResponse; |
||
17 | use Webauthn\AuthenticatorAssertionResponseValidator; |
||
18 | use Webauthn\AuthenticatorAttestationResponse; |
||
19 | use Webauthn\AuthenticatorResponse; |
||
20 | use Webauthn\PublicKeyCredential; |
||
21 | use Webauthn\PublicKeyCredentialLoader; |
||
22 | |||
23 | class VerifyHandlerTest extends SapphireTest |
||
24 | { |
||
25 | protected $usesDatabase = true; |
||
26 | |||
27 | /** |
||
28 | * @var VerifyHandler |
||
29 | */ |
||
30 | protected $handler; |
||
31 | |||
32 | /** |
||
33 | * @var Member |
||
34 | */ |
||
35 | protected $member; |
||
36 | |||
37 | /** |
||
38 | * @var HTTPRequest |
||
39 | */ |
||
40 | protected $request; |
||
41 | |||
42 | /** |
||
43 | * @var SessionStore |
||
44 | */ |
||
45 | protected $store; |
||
46 | |||
47 | /** |
||
48 | * @var RegisteredMethod |
||
49 | */ |
||
50 | protected $registeredMethod; |
||
51 | |||
52 | /** |
||
53 | * @var array |
||
54 | */ |
||
55 | protected $mockData = []; |
||
56 | |||
57 | protected function setUp() |
||
58 | { |
||
59 | parent::setUp(); |
||
60 | |||
61 | $this->request = new HTTPRequest('GET', '/'); |
||
62 | $this->handler = Injector::inst()->create(VerifyHandler::class); |
||
63 | |||
64 | $memberID = $this->logInWithPermission(); |
||
65 | /** @var Member $member */ |
||
66 | $this->member = Member::get()->byID($memberID); |
||
67 | |||
68 | $this->store = new SessionStore($this->member); |
||
69 | |||
70 | $this->registeredMethod = new RegisteredMethod(); |
||
71 | |||
72 | // phpcs:disable |
||
73 | $this->registeredMethod->Data = json_encode([ |
||
74 | 'g8e1UH4B1gUYl\/7AiDXHTp8SE3cxYnpC6jF3Fo0KMm79FNN\/e34hDE1Mnd4FSOoNW6B+p7xB2tqj28svkJQh1Q==' => [ |
||
75 | 'source' => [ |
||
76 | 'publicKeyCredentialId' => 'g8e1UH4B1gUYl_7AiDXHTp8SE3cxYnpC6jF3Fo0KMm79FNN_e34hDE1Mnd4FSOoNW6B-p7xB2tqj28svkJQh1Q', |
||
77 | 'type' => 'public-key', |
||
78 | 'transports' => |
||
79 | array ( |
||
80 | ), |
||
81 | 'attestationType' => 'none', |
||
82 | 'trustPath' => |
||
83 | array ( |
||
84 | 'type' => 'empty', |
||
85 | ), |
||
86 | 'aaguid' => 'AAAAAAAAAAAAAAAAAAAAAA', |
||
87 | 'credentialPublicKey' => 'pQECAyYgASFYII3gDdvOBje5JfjNO0VhxE2RrV5XoKqWmCZAmR0f9nFaIlggZOUvkovGH9cfeyfXEpJAVOzR1d-rVRZJvwWJf444aLo', |
||
88 | 'userHandle' => 'MQ', |
||
89 | 'counter' => 268, |
||
90 | ], |
||
91 | 'counter' => 0, |
||
92 | ] |
||
93 | ]); |
||
94 | // phpcs:enable |
||
95 | } |
||
96 | |||
97 | /** |
||
98 | * @expectedException \SilverStripe\MFA\Exception\AuthenticationFailedException |
||
99 | */ |
||
100 | public function testStartThrowsExceptionWithMissingData() |
||
101 | { |
||
102 | $this->registeredMethod->Data = ''; |
||
103 | $this->handler->start($this->store, $this->registeredMethod); |
||
104 | } |
||
105 | |||
106 | public function testStart() |
||
107 | { |
||
108 | $result = $this->handler->start($this->store, $this->registeredMethod); |
||
109 | $this->assertArrayHasKey('publicKey', $result); |
||
110 | } |
||
111 | |||
112 | public function testVerifyReturnsErrorWhenRequiredInformationIsMissing() |
||
113 | { |
||
114 | $this->registeredMethod->Data = null; |
||
115 | $result = $this->handler->verify($this->request, $this->store, $this->registeredMethod); |
||
116 | |||
117 | $this->assertFalse($result->isSuccessful()); |
||
118 | $this->assertContains('Incomplete data', $result->getMessage()); |
||
119 | } |
||
120 | |||
121 | /** |
||
122 | * @param AuthenticatorResponse $mockResponse |
||
123 | * @param Result $expectedResult |
||
124 | * @param callable $responseValidatorMockCallback |
||
125 | * @dataProvider verifyProvider |
||
126 | */ |
||
127 | public function testVerify( |
||
128 | $mockResponse, |
||
129 | $expectedResult, |
||
130 | callable $responseValidatorMockCallback = null |
||
131 | ) { |
||
132 | /** @var VerifyHandler&PHPUnit_Framework_MockObject_MockObject $handlerMock */ |
||
133 | $handlerMock = $this->getMockBuilder(VerifyHandler::class) |
||
134 | ->setMethods(['getPublicKeyCredentialLoader', 'getAuthenticatorAssertionResponseValidator']) |
||
135 | ->getMock(); |
||
136 | |||
137 | $responseValidatorMock = $this->createMock(AuthenticatorAssertionResponseValidator::class); |
||
138 | // Allow the data provider to customise the validation check handling |
||
139 | if ($responseValidatorMockCallback) { |
||
140 | $responseValidatorMockCallback($responseValidatorMock); |
||
141 | } |
||
142 | $handlerMock->expects($this->any())->method('getAuthenticatorAssertionResponseValidator') |
||
143 | ->willReturn($responseValidatorMock); |
||
144 | |||
145 | $loggerMock = $this->createMock(LoggerInterface::class); |
||
146 | $handlerMock->setLogger($loggerMock); |
||
147 | |||
148 | $loaderMock = $this->createMock(PublicKeyCredentialLoader::class); |
||
149 | $handlerMock->expects($this->once())->method('getPublicKeyCredentialLoader')->willReturn($loaderMock); |
||
150 | |||
151 | $publicKeyCredentialMock = $this->createMock(PublicKeyCredential::class); |
||
152 | $loaderMock->expects($this->once())->method('load')->with('example')->willReturn( |
||
153 | $publicKeyCredentialMock |
||
154 | ); |
||
155 | |||
156 | $publicKeyCredentialMock->expects($this->once())->method('getResponse')->willReturn($mockResponse); |
||
157 | |||
158 | $this->request->setBody(json_encode([ |
||
159 | 'credentials' => base64_encode('example'), |
||
160 | ])); |
||
161 | $result = $handlerMock->verify($this->request, $this->store, $this->registeredMethod); |
||
0 ignored issues
–
show
|
|||
162 | |||
163 | $this->assertSame($expectedResult->isSuccessful(), $result->isSuccessful()); |
||
164 | if ($expectedResult->getMessage()) { |
||
165 | $this->assertContains($expectedResult->getMessage(), $result->getMessage()); |
||
166 | } |
||
167 | } |
||
168 | |||
169 | /** |
||
170 | * Some centralised or reusable logic for testVerify. Note that some of the mocks are only used in some of the |
||
171 | * provided data scenarios, but any expected call numbers are based on all scenarios being run. |
||
172 | * |
||
173 | * @return array[] |
||
174 | */ |
||
175 | public function verifyProvider() |
||
176 | { |
||
177 | return [ |
||
178 | 'wrong response return type' => [ |
||
179 | // Deliberately the wrong child implementation of \Webauthn\AuthenticatorResponse |
||
180 | $this->createMock(AuthenticatorAttestationResponse::class), |
||
181 | new Result(false, 'Unexpected response type found'), |
||
182 | ], |
||
183 | 'valid response' => [ |
||
184 | $this->createMock(AuthenticatorAssertionResponse::class), |
||
185 | new Result(true), |
||
186 | function (PHPUnit_Framework_MockObject_MockObject $responseValidatorMock) { |
||
187 | // Specifically setting expectations for the result of the response validator's "check" call |
||
188 | $responseValidatorMock->expects($this->once())->method('check')->willReturn(true); |
||
189 | }, |
||
190 | ], |
||
191 | 'invalid response' => [ |
||
192 | $this->createMock(AuthenticatorAssertionResponse::class), |
||
193 | new Result(false, 'I am a test'), |
||
194 | function (PHPUnit_Framework_MockObject_MockObject $responseValidatorMock) { |
||
195 | // Specifically setting expectations for the result of the response validator's "check" call |
||
196 | $responseValidatorMock->expects($this->once())->method('check') |
||
197 | ->willThrowException(new Exception('I am a test')); |
||
198 | }, |
||
199 | ], |
||
200 | ]; |
||
201 | } |
||
202 | } |
||
203 |
This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.
This is most likely a typographical error or the method has been renamed.