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