1 | <?php |
||
42 | class SecondFactorController extends Controller |
||
43 | { |
||
44 | public function selectSecondFactorForVerificationAction() |
||
45 | { |
||
46 | $context = $this->getResponseContext(); |
||
47 | $originalRequestId = $context->getInResponseTo(); |
||
48 | |||
49 | /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */ |
||
50 | $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId); |
||
51 | $logger->notice('Determining which second factor to use...'); |
||
52 | |||
53 | $requiredLoa = $this |
||
54 | ->getStepupService() |
||
55 | ->resolveHighestRequiredLoa( |
||
56 | $context->getRequiredLoa(), |
||
57 | $context->getServiceProvider(), |
||
58 | $context->getAuthenticatingIdp() |
||
59 | ); |
||
60 | |||
61 | if ($requiredLoa === null) { |
||
62 | $logger->notice( |
||
63 | 'No valid required Loa can be determined, no authentication is possible, Loa cannot be given' |
||
64 | ); |
||
65 | |||
66 | return $this->forward('SurfnetStepupGatewayGatewayBundle:Gateway:sendLoaCannotBeGiven'); |
||
67 | } else { |
||
68 | $logger->notice(sprintf('Determined that the required Loa is "%s"', $requiredLoa)); |
||
69 | } |
||
70 | |||
71 | if ($this->getStepupService()->isIntrinsicLoa($requiredLoa)) { |
||
72 | $this->get('gateway.authentication_logger')->logIntrinsicLoaAuthentication($originalRequestId); |
||
73 | |||
74 | return $this->forward('SurfnetStepupGatewayGatewayBundle:Gateway:respond'); |
||
75 | } |
||
76 | |||
77 | $secondFactorCollection = $this |
||
78 | ->getStepupService() |
||
79 | ->determineViableSecondFactors($context->getIdentityNameId(), $requiredLoa); |
||
80 | |||
81 | if (count($secondFactorCollection) === 0) { |
||
82 | $logger->notice('No second factors can give the determined Loa'); |
||
83 | |||
84 | return $this->forward('SurfnetStepupGatewayGatewayBundle:Gateway:sendLoaCannotBeGiven'); |
||
85 | } |
||
86 | |||
87 | // will be replaced by a second factor selection screen once we support multiple |
||
88 | /** @var \Surfnet\StepupGateway\GatewayBundle\Entity\SecondFactor $secondFactor */ |
||
89 | $secondFactor = $secondFactorCollection->first(); |
||
90 | // when multiple second factors are supported this should be moved into the |
||
91 | // StepUpAuthenticationService::determineViableSecondFactors and handled in a performant way |
||
92 | // currently keeping this here for visibility |
||
93 | if (!$this->get('gateway.service.whitelist')->contains($secondFactor->institution)) { |
||
94 | $logger->notice(sprintf( |
||
95 | 'Second factor "%s" is listed for institution "%s" which is not on the whitelist, sending Loa ' |
||
96 | . 'cannot be given response', |
||
97 | $secondFactor->secondFactorId, |
||
98 | $secondFactor->institution |
||
99 | )); |
||
100 | |||
101 | return $this->forward('SurfnetStepupGatewayGatewayBundle:Gateway:sendLoaCannotBeGiven'); |
||
102 | } |
||
103 | |||
104 | $logger->notice(sprintf( |
||
105 | 'Found "%d" second factors, using second factor of type "%s"', |
||
106 | count($secondFactorCollection), |
||
107 | $secondFactor->secondFactorType |
||
108 | )); |
||
109 | |||
110 | $context->saveSelectedSecondFactor($secondFactor->secondFactorId); |
||
111 | |||
112 | $this->getStepupService()->clearSmsVerificationState(); |
||
113 | |||
114 | $route = 'gateway_verify_second_factor_' . strtolower($secondFactor->secondFactorType); |
||
115 | return $this->redirect($this->generateUrl($route)); |
||
116 | } |
||
117 | |||
118 | public function verifyGssfAction() |
||
119 | { |
||
120 | $context = $this->getResponseContext(); |
||
121 | $originalRequestId = $context->getInResponseTo(); |
||
122 | |||
123 | /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */ |
||
124 | $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId); |
||
125 | $logger->info('Received request to verify GSSF'); |
||
126 | |||
127 | $selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger); |
||
128 | |||
129 | $logger->info(sprintf( |
||
130 | 'Selected GSSF "%s" for verfication, forwarding to Saml handling', |
||
131 | $selectedSecondFactor |
||
132 | )); |
||
133 | |||
134 | /** @var \Surfnet\StepupGateway\GatewayBundle\Service\SecondFactorService $secondFactorService */ |
||
135 | $secondFactorService = $this->get('gateway.service.second_factor_service'); |
||
136 | /** @var \Surfnet\StepupGateway\GatewayBundle\Entity\SecondFactor $secondFactor */ |
||
137 | $secondFactor = $secondFactorService->findByUuid($selectedSecondFactor); |
||
138 | if (!$secondFactor) { |
||
139 | $logger->critical(sprintf( |
||
140 | 'Requested verification of GSSF "%s", however that Second Factor no longer exists', |
||
141 | $selectedSecondFactor |
||
142 | )); |
||
143 | |||
144 | throw new RuntimeException('Verification of selected second factor that no longer exists'); |
||
145 | } |
||
146 | |||
147 | return $this->forward( |
||
148 | 'SurfnetStepupGatewaySamlStepupProviderBundle:SamlProxy:sendSecondFactorVerificationAuthnRequest', |
||
149 | [ |
||
150 | 'provider' => $secondFactor->secondFactorType, |
||
151 | 'subjectNameId' => $secondFactor->secondFactorIdentifier |
||
152 | ] |
||
153 | ); |
||
154 | } |
||
155 | |||
156 | public function gssfVerifiedAction() |
||
157 | { |
||
158 | $context = $this->getResponseContext(); |
||
159 | $originalRequestId = $context->getInResponseTo(); |
||
160 | |||
161 | /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */ |
||
162 | $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId); |
||
163 | $logger->info('Attempting to mark GSSF as verified'); |
||
164 | |||
165 | $selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger); |
||
166 | |||
167 | /** @var \Surfnet\StepupGateway\GatewayBundle\Entity\SecondFactor $secondFactor */ |
||
168 | $secondFactor = $this->get('gateway.service.second_factor_service')->findByUuid($selectedSecondFactor); |
||
169 | if (!$secondFactor) { |
||
170 | $logger->critical(sprintf( |
||
171 | 'Verification of GSSF "%s" succeeded, however that Second Factor no longer exists', |
||
172 | $selectedSecondFactor |
||
173 | )); |
||
174 | |||
175 | throw new RuntimeException('Verification of selected second factor that no longer exists'); |
||
176 | } |
||
177 | |||
178 | $context->markSecondFactorVerified(); |
||
179 | $this->getAuthenticationLogger()->logSecondFactorAuthentication($originalRequestId); |
||
180 | |||
181 | $logger->info(sprintf( |
||
182 | 'Marked GSSF "%s" as verified, forwarding to Gateway controller to respond', |
||
183 | $selectedSecondFactor |
||
184 | )); |
||
185 | |||
186 | return $this->forward('SurfnetStepupGatewayGatewayBundle:Gateway:respond'); |
||
187 | } |
||
188 | |||
189 | /** |
||
190 | * @Template |
||
191 | * @param Request $request |
||
192 | * @return array|Response |
||
193 | */ |
||
194 | public function verifyYubiKeySecondFactorAction(Request $request) |
||
195 | { |
||
196 | $context = $this->getResponseContext(); |
||
197 | $originalRequestId = $context->getInResponseTo(); |
||
198 | |||
199 | /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */ |
||
200 | $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId); |
||
201 | |||
202 | $selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger); |
||
203 | |||
204 | $logger->notice('Verifying possession of Yubikey second factor'); |
||
205 | |||
206 | $command = new VerifyYubikeyOtpCommand(); |
||
207 | $command->secondFactorId = $selectedSecondFactor; |
||
208 | |||
209 | $form = $this->createForm('gateway_verify_yubikey_otp', $command)->handleRequest($request); |
||
210 | |||
211 | if ($form->get('cancel')->isClicked()) { |
||
|
|||
212 | return $this->forward('SurfnetStepupGatewayGatewayBundle:Gateway:sendAuthenticationCancelledByUser'); |
||
213 | } |
||
214 | |||
215 | if (!$form->isValid()) { |
||
216 | // OTP field is rendered empty in the template. |
||
217 | return ['form' => $form->createView()]; |
||
218 | } |
||
219 | |||
220 | $result = $this->getStepupService()->verifyYubikeyOtp($command); |
||
221 | |||
222 | if ($result->didOtpVerificationFail()) { |
||
223 | $form->addError(new FormError('gateway.form.verify_yubikey.otp_verification_failed')); |
||
224 | |||
225 | // OTP field is rendered empty in the template. |
||
226 | return ['form' => $form->createView()]; |
||
227 | } elseif (!$result->didPublicIdMatch()) { |
||
228 | $form->addError(new FormError('gateway.form.verify_yubikey.public_id_mismatch')); |
||
229 | |||
230 | // OTP field is rendered empty in the template. |
||
231 | return ['form' => $form->createView()]; |
||
232 | } |
||
233 | |||
234 | $this->getResponseContext()->markSecondFactorVerified(); |
||
235 | $this->getAuthenticationLogger()->logSecondFactorAuthentication($originalRequestId); |
||
236 | |||
237 | $logger->info( |
||
238 | sprintf( |
||
239 | 'Marked Yubikey Second Factor "%s" as verified, forwarding to Saml Proxy to respond', |
||
240 | $selectedSecondFactor |
||
241 | ) |
||
242 | ); |
||
243 | |||
244 | return $this->forward('SurfnetStepupGatewayGatewayBundle:Gateway:respond'); |
||
245 | } |
||
246 | |||
247 | /** |
||
248 | * @Template |
||
249 | * @param Request $request |
||
250 | * @return array|Response |
||
251 | */ |
||
252 | public function verifySmsSecondFactorAction(Request $request) |
||
253 | { |
||
254 | $context = $this->getResponseContext(); |
||
255 | $originalRequestId = $context->getInResponseTo(); |
||
256 | |||
257 | /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */ |
||
258 | $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId); |
||
259 | |||
260 | $selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger); |
||
261 | |||
262 | $logger->notice('Verifying possession of SMS second factor, preparing to send'); |
||
263 | |||
264 | $command = new SendSmsChallengeCommand(); |
||
265 | $command->secondFactorId = $selectedSecondFactor; |
||
266 | |||
267 | $form = $this->createForm('gateway_send_sms_challenge', $command)->handleRequest($request); |
||
268 | |||
269 | $stepupService = $this->getStepupService(); |
||
270 | $phoneNumber = InternationalPhoneNumber::fromStringFormat( |
||
271 | $stepupService->getSecondFactorIdentifier($selectedSecondFactor) |
||
272 | ); |
||
273 | |||
274 | $otpRequestsRemaining = $stepupService->getSmsOtpRequestsRemainingCount(); |
||
275 | $maximumOtpRequests = $stepupService->getSmsMaximumOtpRequestsCount(); |
||
276 | $viewVariables = ['otpRequestsRemaining' => $otpRequestsRemaining, 'maximumOtpRequests' => $maximumOtpRequests]; |
||
277 | |||
278 | if ($form->get('cancel')->isClicked()) { |
||
279 | return $this->forward('SurfnetStepupGatewayGatewayBundle:Gateway:sendAuthenticationCancelledByUser'); |
||
280 | } |
||
281 | |||
282 | if (!$form->isValid()) { |
||
283 | return array_merge($viewVariables, ['phoneNumber' => $phoneNumber, 'form' => $form->createView()]); |
||
284 | } |
||
285 | |||
286 | $logger->notice('Verifying possession of SMS second factor, sending challenge per SMS'); |
||
287 | |||
288 | if (!$stepupService->sendSmsChallenge($command)) { |
||
289 | $form->addError(new FormError('gateway.form.send_sms_challenge.sms_sending_failed')); |
||
290 | |||
291 | return array_merge($viewVariables, ['phoneNumber' => $phoneNumber, 'form' => $form->createView()]); |
||
292 | } |
||
293 | |||
294 | return $this->redirect($this->generateUrl('gateway_verify_second_factor_sms_verify_challenge')); |
||
295 | } |
||
296 | |||
297 | /** |
||
298 | * @Template |
||
299 | * @param Request $request |
||
300 | * @return array|Response |
||
301 | */ |
||
302 | public function verifySmsSecondFactorChallengeAction(Request $request) |
||
303 | { |
||
304 | $context = $this->getResponseContext(); |
||
305 | $originalRequestId = $context->getInResponseTo(); |
||
306 | |||
307 | /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */ |
||
308 | $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId); |
||
309 | |||
310 | $selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger); |
||
311 | |||
312 | $command = new VerifyPossessionOfPhoneCommand(); |
||
313 | $form = $this->createForm('gateway_verify_sms_challenge', $command)->handleRequest($request); |
||
314 | |||
315 | if ($form->get('cancel')->isClicked()) { |
||
316 | return $this->forward('SurfnetStepupGatewayGatewayBundle:Gateway:sendAuthenticationCancelledByUser'); |
||
317 | } |
||
318 | |||
319 | if (!$form->isValid()) { |
||
320 | return ['form' => $form->createView()]; |
||
321 | } |
||
322 | |||
323 | $logger->notice('Verifying input SMS challenge matches'); |
||
324 | |||
325 | $verification = $this->getStepupService()->verifySmsChallenge($command); |
||
326 | |||
327 | if ($verification->wasSuccessful()) { |
||
328 | $this->getStepupService()->clearSmsVerificationState(); |
||
329 | |||
330 | $this->getResponseContext()->markSecondFactorVerified(); |
||
331 | $this->getAuthenticationLogger()->logSecondFactorAuthentication($originalRequestId); |
||
332 | |||
333 | $logger->info( |
||
334 | sprintf( |
||
335 | 'Marked Sms Second Factor "%s" as verified, forwarding to Saml Proxy to respond', |
||
336 | $selectedSecondFactor |
||
337 | ) |
||
338 | ); |
||
339 | |||
340 | return $this->forward('SurfnetStepupGatewayGatewayBundle:Gateway:respond'); |
||
341 | } elseif ($verification->didOtpExpire()) { |
||
342 | $logger->notice('SMS challenge expired'); |
||
343 | $form->addError(new FormError('gateway.form.send_sms_challenge.challenge_expired')); |
||
344 | } elseif ($verification->wasAttemptedTooManyTimes()) { |
||
345 | $logger->notice('SMS challenge verification was attempted too many times'); |
||
346 | $form->addError(new FormError('gateway.form.send_sms_challenge.too_many_attempts')); |
||
347 | } else { |
||
348 | $logger->notice('SMS challenge did not match'); |
||
349 | $form->addError(new FormError('gateway.form.send_sms_challenge.sms_challenge_incorrect')); |
||
350 | } |
||
351 | |||
352 | return ['form' => $form->createView()]; |
||
353 | } |
||
354 | |||
355 | /** |
||
356 | * @Template |
||
357 | */ |
||
358 | public function initiateU2fAuthenticationAction() |
||
406 | |||
407 | /** |
||
408 | * @Template("SurfnetStepupGatewayGatewayBundle:SecondFactor:initiateU2fAuthentication.html.twig") |
||
409 | * |
||
410 | * @param Request $request |
||
411 | * @return array|Response |
||
412 | */ |
||
413 | public function verifyU2fAuthenticationAction(Request $request) |
||
414 | { |
||
415 | $context = $this->getResponseContext(); |
||
416 | $originalRequestId = $context->getInResponseTo(); |
||
417 | |||
418 | /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */ |
||
419 | $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId); |
||
420 | |||
421 | $selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger); |
||
422 | |||
423 | $logger->notice('Received sign response from device'); |
||
424 | |||
425 | /** @var AttributeBagInterface $session */ |
||
426 | $session = $this->get('gateway.session.u2f'); |
||
427 | $signRequest = $session->get('request'); |
||
428 | $signResponse = new SignResponse(); |
||
429 | |||
430 | $formAction = $this->generateUrl('gateway_verify_second_factor_u2f_verify_authentication'); |
||
431 | $form = $this |
||
432 | ->createForm( |
||
433 | 'surfnet_stepup_u2f_verify_device_authentication', |
||
434 | $signResponse, |
||
435 | ['sign_request' => $signRequest, 'action' => $formAction] |
||
436 | ) |
||
437 | ->handleRequest($request); |
||
438 | |||
439 | $cancelFormAction = $this->generateUrl('gateway_verify_second_factor_u2f_cancel_authentication'); |
||
440 | $cancelForm = |
||
441 | $this->createForm('gateway_cancel_second_factor_verification', null, ['action' => $cancelFormAction]); |
||
442 | |||
443 | if (!$form->isValid()) { |
||
444 | $logger->error('U2F authentication verification could not be started because device send illegal data'); |
||
445 | $this->addFlash('error', 'gateway.u2f.alert.error'); |
||
446 | |||
447 | return ['authenticationFailed' => true, 'cancelForm' => $cancelForm->createView()]; |
||
448 | } |
||
449 | |||
450 | $service = $this->get('surfnet_stepup_u2f_verification.service.u2f_verification'); |
||
451 | $result = $service->verifyAuthentication($signRequest, $signResponse); |
||
452 | |||
453 | if ($result->wasSuccessful()) { |
||
454 | $context->markSecondFactorVerified(); |
||
455 | $this->getAuthenticationLogger()->logSecondFactorAuthentication($originalRequestId); |
||
456 | |||
457 | $logger->info( |
||
458 | sprintf( |
||
459 | 'Marked U2F second factor "%s" as verified, forwarding to Saml Proxy to respond', |
||
460 | $selectedSecondFactor |
||
461 | ) |
||
462 | ); |
||
463 | |||
464 | return $this->forward('SurfnetStepupGatewayGatewayBundle:Gateway:respond'); |
||
465 | } elseif ($result->didDeviceReportError()) { |
||
466 | $logger->error('U2F device reported error during authentication'); |
||
467 | $this->addFlash('error', 'gateway.u2f.alert.device_reported_an_error'); |
||
468 | } else { |
||
469 | $logger->error('U2F authentication verification failed'); |
||
470 | $this->addFlash('error', 'gateway.u2f.alert.error'); |
||
471 | } |
||
472 | |||
473 | return ['authenticationFailed' => true, 'cancelForm' => $cancelForm->createView()]; |
||
474 | } |
||
475 | |||
476 | public function cancelU2fAuthenticationAction() |
||
480 | |||
481 | /** |
||
482 | * @return \Surfnet\StepupGateway\GatewayBundle\Service\StepupAuthenticationService |
||
483 | */ |
||
484 | private function getStepupService() |
||
488 | |||
489 | /** |
||
490 | * @return ResponseContext |
||
491 | */ |
||
492 | private function getResponseContext() |
||
496 | |||
497 | /** |
||
498 | * @return \Surfnet\StepupGateway\GatewayBundle\Monolog\Logger\AuthenticationLogger |
||
499 | */ |
||
500 | private function getAuthenticationLogger() |
||
504 | |||
505 | /** |
||
506 | * @param ResponseContext $context |
||
507 | * @param LoggerInterface $logger |
||
508 | * @return string |
||
509 | */ |
||
510 | private function getSelectedSecondFactor(ResponseContext $context, LoggerInterface $logger) |
||
522 | } |
||
523 |
Let’s take a look at an example:
In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.
Available Fixes
Change the type-hint for the parameter:
Add an additional type-check:
Add the method to the interface: