1 | <?php |
||||
2 | |||||
3 | declare(strict_types=1); |
||||
4 | |||||
5 | namespace SilverStripe\WebAuthn; |
||||
6 | |||||
7 | use Cose\Algorithms; |
||||
8 | use Exception; |
||||
9 | use GuzzleHttp\Psr7\ServerRequest; |
||||
10 | use Psr\Log\LoggerInterface; |
||||
11 | use SilverStripe\Control\Director; |
||||
12 | use SilverStripe\Control\HTTPRequest; |
||||
13 | use SilverStripe\Core\Config\Configurable; |
||||
14 | use SilverStripe\Core\Extensible; |
||||
15 | use SilverStripe\MFA\Method\Handler\RegisterHandlerInterface; |
||||
16 | use SilverStripe\MFA\State\Result; |
||||
17 | use SilverStripe\MFA\Store\StoreInterface; |
||||
18 | use SilverStripe\SiteConfig\SiteConfig; |
||||
19 | use Webauthn\AttestationStatement\AttestationStatementSupportManager; |
||||
20 | use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs; |
||||
21 | use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler; |
||||
22 | use Webauthn\AuthenticatorAttestationResponse; |
||||
23 | use Webauthn\AuthenticatorAttestationResponseValidator; |
||||
24 | use Webauthn\AuthenticatorSelectionCriteria; |
||||
25 | use Webauthn\PublicKeyCredentialCreationOptions; |
||||
26 | use Webauthn\PublicKeyCredentialParameters; |
||||
27 | use Webauthn\PublicKeyCredentialRpEntity; |
||||
28 | use Webauthn\PublicKeyCredentialSource; |
||||
29 | use Webauthn\TokenBinding\TokenBindingNotSupportedHandler; |
||||
30 | |||||
31 | class RegisterHandler implements RegisterHandlerInterface |
||||
32 | { |
||||
33 | use BaseHandlerTrait; |
||||
34 | use Extensible; |
||||
35 | use Configurable; |
||||
36 | use CredentialRepositoryProviderTrait; |
||||
0 ignored issues
–
show
introduced
by
Loading history...
|
|||||
37 | |||||
38 | /** |
||||
39 | * Provide a user help link that will be available when registering backup codes |
||||
40 | * |
||||
41 | * @config |
||||
42 | * @var string |
||||
43 | */ |
||||
44 | private static $user_help_link = 'https://userhelp.silverstripe.org/en/4/optional_features/multi-factor_authentication/user_manual/using_security_keys/'; // phpcs:ignore |
||||
0 ignored issues
–
show
|
|||||
45 | |||||
46 | /** |
||||
47 | * The default attachment mode to use for Authentication Selection Criteria. |
||||
48 | * |
||||
49 | * See {@link getAuthenticatorSelectionCriteria()} for more information. |
||||
50 | * |
||||
51 | * @config |
||||
52 | * @var string |
||||
53 | */ |
||||
54 | private static $authenticator_attachment = AuthenticatorSelectionCriteria::AUTHENTICATOR_ATTACHMENT_CROSS_PLATFORM; |
||||
0 ignored issues
–
show
|
|||||
55 | |||||
56 | /** |
||||
57 | * Dependency injection configuration |
||||
58 | * |
||||
59 | * @config |
||||
60 | * @var array |
||||
61 | */ |
||||
62 | private static $dependencies = [ |
||||
0 ignored issues
–
show
|
|||||
63 | 'Logger' => '%$' . LoggerInterface::class . '.mfa', |
||||
64 | ]; |
||||
65 | |||||
66 | /** |
||||
67 | * @var LoggerInterface |
||||
68 | */ |
||||
69 | protected $logger = null; |
||||
70 | |||||
71 | /** |
||||
72 | * Sets the {@see $logger} member variable |
||||
73 | * |
||||
74 | * @param LoggerInterface|null $logger |
||||
75 | * @return self |
||||
76 | */ |
||||
77 | public function setLogger(?LoggerInterface $logger): self |
||||
78 | { |
||||
79 | $this->logger = $logger; |
||||
80 | return $this; |
||||
81 | } |
||||
82 | |||||
83 | /** |
||||
84 | * Stores any data required to handle a registration process with a method, and returns relevant state to be applied |
||||
85 | * to the front-end application managing the process. |
||||
86 | * |
||||
87 | * @param StoreInterface $store An object that hold session data (and the Member) that can be mutated |
||||
88 | * @return array Props to be passed to a front-end component |
||||
89 | * @throws Exception When there is no valid source of CSPRNG |
||||
90 | */ |
||||
91 | public function start(StoreInterface $store): array |
||||
92 | { |
||||
93 | $options = $this->getCredentialCreationOptions($store, true); |
||||
94 | |||||
95 | return [ |
||||
96 | 'keyData' => $options, |
||||
97 | ]; |
||||
98 | } |
||||
99 | |||||
100 | /** |
||||
101 | * Confirm that the provided details are valid, and create a new RegisteredMethod against the member. |
||||
102 | * |
||||
103 | * @param HTTPRequest $request |
||||
104 | * @param StoreInterface $store |
||||
105 | * @return Result |
||||
106 | * @throws Exception |
||||
107 | */ |
||||
108 | public function register(HTTPRequest $request, StoreInterface $store): Result |
||||
109 | { |
||||
110 | $options = $this->getCredentialCreationOptions($store); |
||||
111 | $data = json_decode((string) $request->getBody(), true); |
||||
112 | |||||
113 | try { |
||||
114 | if (empty($data['credentials'])) { |
||||
115 | throw new ResponseDataException('Incomplete data, required information missing'); |
||||
116 | } |
||||
117 | |||||
118 | $decoder = $this->getDecoder(); |
||||
119 | $attestationStatementSupportManager = $this->getAttestationStatementSupportManager($decoder); |
||||
120 | $attestationObjectLoader = $this->getAttestationObjectLoader($attestationStatementSupportManager, $decoder); |
||||
121 | $publicKeyCredentialLoader = $this->getPublicKeyCredentialLoader($attestationObjectLoader, $decoder); |
||||
122 | $publicKeyCredential = $publicKeyCredentialLoader->load(base64_decode($data['credentials'])); |
||||
123 | $response = $publicKeyCredential->getResponse(); |
||||
124 | |||||
125 | if (!$response instanceof AuthenticatorAttestationResponse) { |
||||
126 | throw new ResponseTypeException('Unexpected response type found'); |
||||
127 | } |
||||
128 | |||||
129 | if (!$response->getAttestationObject()->getAuthData()->hasAttestedCredentialData()) { |
||||
130 | throw new ResponseDataException('Incomplete data, required information missing'); |
||||
131 | } |
||||
132 | |||||
133 | // Create a PSR-7 request |
||||
134 | $psrRequest = ServerRequest::fromGlobals(); |
||||
135 | |||||
136 | // Validate the webauthn response |
||||
137 | $this->getAuthenticatorAttestationResponseValidator($attestationStatementSupportManager, $store) |
||||
138 | ->check($response, $options, $psrRequest); |
||||
139 | } catch (Exception $e) { |
||||
140 | $this->logger->error($e->getMessage()); |
||||
141 | return Result::create(false, 'Registration failed: ' . $e->getMessage()); |
||||
142 | } |
||||
143 | |||||
144 | $credentialRepository = $this->getCredentialRepository($store); |
||||
145 | |||||
146 | $source = PublicKeyCredentialSource::createFromPublicKeyCredential( |
||||
147 | $publicKeyCredential, |
||||
148 | $options->getUser()->getId() |
||||
149 | ); |
||||
150 | |||||
151 | // Clear the repository so only one key is registered at a time |
||||
152 | // NOTE: This can be considered temporary behaviour until the UI supports managing multiple keys |
||||
153 | $credentialRepository->reset(); |
||||
154 | |||||
155 | // Persist the "credential source" |
||||
156 | $credentialRepository->saveCredentialSource($source); |
||||
157 | |||||
158 | return Result::create()->setContext($credentialRepository->toArray()); |
||||
159 | } |
||||
160 | |||||
161 | /** |
||||
162 | * @param AttestationStatementSupportManager $attestationStatementSupportManager |
||||
163 | * @param StoreInterface $store |
||||
164 | * @return AuthenticatorAttestationResponseValidator |
||||
165 | */ |
||||
166 | protected function getAuthenticatorAttestationResponseValidator( |
||||
167 | AttestationStatementSupportManager $attestationStatementSupportManager, |
||||
168 | StoreInterface $store |
||||
169 | ): AuthenticatorAttestationResponseValidator { |
||||
170 | $credentialRepository = $this->getCredentialRepository($store); |
||||
171 | |||||
172 | return new AuthenticatorAttestationResponseValidator( |
||||
173 | $attestationStatementSupportManager, |
||||
174 | $credentialRepository, |
||||
175 | new TokenBindingNotSupportedHandler(), |
||||
176 | new ExtensionOutputCheckerHandler() |
||||
177 | ); |
||||
178 | } |
||||
179 | |||||
180 | /** |
||||
181 | * Provide a localised description of this MFA Method. |
||||
182 | * |
||||
183 | * eg. "Verification codes are created by an app on your phone" |
||||
184 | * |
||||
185 | * @return string |
||||
186 | */ |
||||
187 | public function getDescription(): string |
||||
188 | { |
||||
189 | return _t( |
||||
190 | __CLASS__ . '.DESCRIPTION', |
||||
191 | 'A small USB device which is used for verifying you' |
||||
192 | ); |
||||
193 | } |
||||
194 | |||||
195 | /** |
||||
196 | * Provide a localised URL to a support article about the registration process for this MFA Method. |
||||
197 | * |
||||
198 | * @return string |
||||
199 | */ |
||||
200 | public function getSupportLink(): string |
||||
201 | { |
||||
202 | return $this->config()->get('user_help_link') ?: ''; |
||||
203 | } |
||||
204 | |||||
205 | /** |
||||
206 | * Provide a localised string to describe the support link {@see getSupportLink} about this MFA Method. |
||||
207 | * |
||||
208 | * @return string |
||||
209 | */ |
||||
210 | public function getSupportText(): string |
||||
211 | { |
||||
212 | return _t(__CLASS__ . '.SUPPORT_LINK_DESCRIPTION', 'How to use security keys.'); |
||||
213 | } |
||||
214 | |||||
215 | /** |
||||
216 | * Get the key that a React UI component is registered under (with @silverstripe/react-injector on the front-end) |
||||
217 | * |
||||
218 | * @return string |
||||
219 | */ |
||||
220 | public function getComponent(): string |
||||
221 | { |
||||
222 | return 'WebAuthnRegister'; |
||||
223 | } |
||||
224 | |||||
225 | /** |
||||
226 | * @return PublicKeyCredentialRpEntity |
||||
227 | */ |
||||
228 | protected function getRelyingPartyEntity(): PublicKeyCredentialRpEntity |
||||
229 | { |
||||
230 | // Relying party entity ONLY allows domains or subdomains. Remove ports or anything else that isn't already. |
||||
231 | // See https://github.com/web-auth/webauthn-framework/blob/v1.2.2/doc/webauthn/PublicKeyCredentialCreation.md#relying-party-entity |
||||
232 | $host = parse_url(Director::host(), PHP_URL_HOST); |
||||
233 | |||||
234 | return new PublicKeyCredentialRpEntity( |
||||
235 | (string) SiteConfig::current_site_config()->Title, |
||||
236 | $host, |
||||
237 | static::config()->get('application_logo') |
||||
238 | ); |
||||
239 | } |
||||
240 | |||||
241 | /** |
||||
242 | * @param StoreInterface $store |
||||
243 | * @param bool $reset |
||||
244 | * @return PublicKeyCredentialCreationOptions |
||||
245 | * @throws Exception |
||||
246 | */ |
||||
247 | protected function getCredentialCreationOptions( |
||||
248 | StoreInterface $store, |
||||
249 | bool $reset = false |
||||
250 | ): PublicKeyCredentialCreationOptions { |
||||
251 | $state = $store->getState(); |
||||
252 | |||||
253 | if (!$reset && !empty($state) && !empty($state['credentialOptions'])) { |
||||
254 | return PublicKeyCredentialCreationOptions::createFromArray($state['credentialOptions']); |
||||
255 | } |
||||
256 | |||||
257 | $credentialOptions = new PublicKeyCredentialCreationOptions( |
||||
258 | $this->getRelyingPartyEntity(), |
||||
259 | $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\Re...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...
|
|||||
260 | random_bytes(32), |
||||
261 | [new PublicKeyCredentialParameters('public-key', Algorithms::COSE_ALGORITHM_ES256)], |
||||
262 | 40000, |
||||
263 | [], |
||||
264 | $this->getAuthenticatorSelectionCriteria(), |
||||
265 | PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE, |
||||
266 | new AuthenticationExtensionsClientInputs() |
||||
267 | ); |
||||
268 | |||||
269 | $store->setState(['credentialOptions' => $credentialOptions] + $state); |
||||
270 | |||||
271 | return $credentialOptions; |
||||
272 | } |
||||
273 | |||||
274 | /** |
||||
275 | * Returns an "Authenticator Selection Criteria" object which is intended to select the appropriate authenticators |
||||
276 | * to participate in the creation operation. |
||||
277 | * |
||||
278 | * The default is to allow only "cross platform" authenticators, e.g. disabling "single platform" authenticators |
||||
279 | * such as touch ID. |
||||
280 | * |
||||
281 | * For more information: https://github.com/web-auth/webauthn-framework/blob/v1.2/doc/webauthn/PublicKeyCredentialCreation.md#authenticator-selection-criteria |
||||
282 | * |
||||
283 | * @return AuthenticatorSelectionCriteria |
||||
284 | */ |
||||
285 | protected function getAuthenticatorSelectionCriteria(): AuthenticatorSelectionCriteria |
||||
286 | { |
||||
287 | return new AuthenticatorSelectionCriteria( |
||||
288 | (string) $this->config()->get('authenticator_attachment') |
||||
289 | ); |
||||
290 | } |
||||
291 | } |
||||
292 |