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