/
VerifyHandler.php
216 lines (189 loc) · 7.62 KB
/
VerifyHandler.php
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
<?php
declare(strict_types=1);
namespace SilverStripe\WebAuthn;
use CBOR\Decoder;
use Cose\Algorithm\Manager;
use Exception;
use GuzzleHttp\Psr7\ServerRequest;
use Psr\Log\LoggerInterface;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\MFA\Exception\AuthenticationFailedException;
use SilverStripe\MFA\Method\Handler\VerifyHandlerInterface;
use SilverStripe\MFA\Model\RegisteredMethod;
use SilverStripe\MFA\State\Result;
use SilverStripe\MFA\Store\StoreInterface;
use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler;
use Webauthn\AuthenticatorAssertionResponse;
use Webauthn\AuthenticatorAssertionResponseValidator;
use Webauthn\PublicKeyCredentialRequestOptions;
use Webauthn\PublicKeyCredentialSource;
use Webauthn\TokenBinding\TokenBindingNotSupportedHandler;
use Cose\Algorithm\Signature\ECDSA\ES256;
class VerifyHandler implements VerifyHandlerInterface
{
use BaseHandlerTrait;
use CredentialRepositoryProviderTrait;
/**
* Dependency injection configuration
*
* @config
* @var array
*/
private static $dependencies = [
'Logger' => '%$' . LoggerInterface::class . '.mfa',
];
/**
* @var LoggerInterface
*/
protected $logger;
/**
* Sets the {@see $logger} member variable
*
* @param LoggerInterface|null $logger
* @return self
*/
public function setLogger(?LoggerInterface $logger): self
{
$this->logger = $logger;
return $this;
}
/**
* Stores any data required to handle a log in process with a method, and returns relevant state to be applied to
* the front-end application managing the process.
*
* @param StoreInterface $store An object that hold session data (and the Member) that can be mutated
* @param RegisteredMethod $method The RegisteredMethod instance that is being verified
* @return array Props to be passed to a front-end component
*/
public function start(StoreInterface $store, RegisteredMethod $method): array
{
return [
'publicKey' => $this->getCredentialRequestOptions($store, $method, true),
];
}
/**
* Verify the request has provided the right information to verify the member that aligns with any sessions state
* that may have been set prior
*
* @param HTTPRequest $request
* @param StoreInterface $store
* @param RegisteredMethod $registeredMethod The RegisteredMethod instance that is being verified
* @return Result
*/
public function verify(HTTPRequest $request, StoreInterface $store, RegisteredMethod $registeredMethod): Result
{
$data = json_decode((string) $request->getBody(), true);
try {
if (empty($data['credentials'])) {
throw new ResponseDataException('Incomplete data, required information missing');
}
$data = $this->makeAuthenticatorDataBase64UrlSafe($data);
$attestationStatementSupportManager = $this->getAttestationStatementSupportManager();
$attestationObjectLoader = $this->getAttestationObjectLoader($attestationStatementSupportManager);
$publicKeyCredential = $this
->getPublicKeyCredentialLoader($attestationObjectLoader)
->load(base64_decode($data['credentials'] ?? ''));
$response = $publicKeyCredential->getResponse();
if (!$response instanceof AuthenticatorAssertionResponse) {
throw new ResponseTypeException('Unexpected response type found');
}
// Create a PSR-7 request
$psrRequest = ServerRequest::fromGlobals();
$this->getAuthenticatorAssertionResponseValidator($store)
->check(
$publicKeyCredential->getRawId(),
$response,
$this->getCredentialRequestOptions($store, $registeredMethod),
$psrRequest,
(string) $store->getMember()->ID
);
} catch (Exception $e) {
$this->logger->error($e->getMessage());
return Result::create(false, 'Verification failed: ' . $e->getMessage());
}
return Result::create();
}
/**
* Make authenticatorData Base64urlSafe which is expected by PublicKeyCredentialLoader::createResponse()
*/
private function makeAuthenticatorDataBase64UrlSafe(array $data): array
{
try {
$decodedCredentials = base64_decode($data['credentials']);
$jsonCredientials = json_decode($decodedCredentials, true);
$jsonCredientials['response']['authenticatorData'] = str_replace(
['+', '/', '='],
['-', '_', ''],
$jsonCredientials['response']['authenticatorData']
);
$decodedCredentials = json_encode($jsonCredientials, JSON_UNESCAPED_SLASHES);
$data['credentials'] = base64_encode($decodedCredentials);
return $data;
} catch (Exception) {
throw new ResponseDataException('Incomplete data, required information missing');
}
}
/**
* Get the key that a React UI component is registered under (with @silverstripe/react-injector on the front-end)
*
* @return string
*/
public function getComponent(): string
{
return 'WebAuthnVerify';
}
/**
* @param StoreInterface $store
* @param RegisteredMethod|null $registeredMethod
* @param bool $reset
* @return PublicKeyCredentialRequestOptions
* @throws AuthenticationFailedException
* @throws Exception
*/
protected function getCredentialRequestOptions(
StoreInterface $store,
RegisteredMethod $registeredMethod = null,
$reset = false
): PublicKeyCredentialRequestOptions {
$state = $store->getState();
if (
!$reset &&
isset($state['credentialOptions']) &&
$state['credentialOptions'] instanceof PublicKeyCredentialRequestOptions
) {
return $state['credentialOptions'];
}
// Use the interface methods (despite the fact the "repository" is per-member in this module)
$validCredentials = $this->getCredentialRepository($store, $registeredMethod)
->findAllForUserEntity($this->getUserEntity($store->getMember()));
if (!count($validCredentials ?? [])) {
throw new AuthenticationFailedException('User does not appear to have any credentials loaded for webauthn');
}
$descriptors = array_map(function (PublicKeyCredentialSource $source) {
return $source->getPublicKeyCredentialDescriptor();
}, $validCredentials ?? []);
$options = new PublicKeyCredentialRequestOptions((string) random_bytes(32));
$options->setTimeout(40000);
$options->allowCredentials(...$descriptors);
$options->setUserVerification(PublicKeyCredentialRequestOptions::USER_VERIFICATION_REQUIREMENT_PREFERRED);
// Persist the options for later
$store->addState(['credentialOptions' => $options]);
return $options;
}
/**
* @param StoreInterface $store
* @return AuthenticatorAssertionResponseValidator
*/
protected function getAuthenticatorAssertionResponseValidator(
StoreInterface $store
): AuthenticatorAssertionResponseValidator {
$manager = new Manager();
$manager->add(new ES256());
return new AuthenticatorAssertionResponseValidator(
$this->getCredentialRepository($store),
new TokenBindingNotSupportedHandler(),
new ExtensionOutputCheckerHandler(),
$manager
);
}
}