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 = [ |
||||
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())); |
||||
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
Loading history...
|
|||||
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 |