1 | <?php |
||
41 | class SecondFactorController extends Controller |
||
42 | { |
||
43 | public function selectSecondFactorForVerificationAction() |
||
44 | { |
||
45 | $context = $this->getResponseContext(); |
||
46 | $originalRequestId = $context->getInResponseTo(); |
||
47 | |||
48 | /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */ |
||
49 | $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId); |
||
50 | $logger->notice('Determining which second factor to use...'); |
||
51 | |||
52 | $requiredLoa = $this |
||
53 | ->getStepupService() |
||
54 | ->resolveHighestRequiredLoa( |
||
55 | $context->getRequiredLoa(), |
||
56 | $context->getServiceProvider(), |
||
57 | $context->getAuthenticatingIdp() |
||
58 | ); |
||
59 | |||
60 | if ($requiredLoa === null) { |
||
61 | $logger->notice( |
||
62 | 'No valid required Loa can be determined, no authentication is possible, Loa cannot be given' |
||
63 | ); |
||
64 | |||
65 | return $this->forward('SurfnetStepupGatewayGatewayBundle:Gateway:sendLoaCannotBeGiven'); |
||
66 | } else { |
||
67 | $logger->notice(sprintf('Determined that the required Loa is "%s"', $requiredLoa)); |
||
68 | } |
||
69 | |||
70 | if ($this->getStepupService()->isIntrinsicLoa($requiredLoa)) { |
||
71 | $this->get('gateway.authentication_logger')->logIntrinsicLoaAuthentication($originalRequestId); |
||
72 | |||
73 | return $this->forward($context->getResponseAction()); |
||
74 | } |
||
75 | |||
76 | $secondFactorCollection = $this |
||
77 | ->getStepupService() |
||
78 | ->determineViableSecondFactors($context->getIdentityNameId(), $requiredLoa); |
||
79 | |||
80 | if (count($secondFactorCollection) === 0) { |
||
81 | $logger->notice('No second factors can give the determined Loa'); |
||
82 | |||
83 | return $this->forward('SurfnetStepupGatewayGatewayBundle:Gateway:sendLoaCannotBeGiven'); |
||
84 | } |
||
85 | |||
86 | // will be replaced by a second factor selection screen once we support multiple |
||
87 | /** @var \Surfnet\StepupGateway\GatewayBundle\Entity\SecondFactor $secondFactor */ |
||
88 | $secondFactor = $secondFactorCollection->first(); |
||
89 | // when multiple second factors are supported this should be moved into the |
||
90 | // StepUpAuthenticationService::determineViableSecondFactors and handled in a performant way |
||
91 | // currently keeping this here for visibility |
||
92 | if (!$this->get('gateway.service.whitelist')->contains($secondFactor->institution)) { |
||
93 | $logger->notice(sprintf( |
||
94 | 'Second factor "%s" is listed for institution "%s" which is not on the whitelist, sending Loa ' |
||
95 | . 'cannot be given response', |
||
96 | $secondFactor->secondFactorId, |
||
97 | $secondFactor->institution |
||
98 | )); |
||
99 | |||
100 | return $this->forward('SurfnetStepupGatewayGatewayBundle:Gateway:sendLoaCannotBeGiven'); |
||
101 | } |
||
102 | |||
103 | $logger->notice(sprintf( |
||
104 | 'Found "%d" second factors, using second factor of type "%s"', |
||
105 | count($secondFactorCollection), |
||
106 | $secondFactor->secondFactorType |
||
107 | )); |
||
108 | |||
109 | $context->saveSelectedSecondFactor($secondFactor->secondFactorId); |
||
110 | |||
111 | $this->getStepupService()->clearSmsVerificationState(); |
||
112 | |||
113 | $route = 'gateway_verify_second_factor_' . strtolower($secondFactor->secondFactorType); |
||
114 | return $this->redirect($this->generateUrl($route)); |
||
115 | } |
||
116 | |||
117 | public function verifyGssfAction() |
||
154 | |||
155 | public function gssfVerifiedAction() |
||
187 | |||
188 | /** |
||
189 | * @Template |
||
190 | * @param Request $request |
||
191 | * @return array|Response |
||
192 | */ |
||
193 | public function verifyYubiKeySecondFactorAction(Request $request) |
||
245 | |||
246 | /** |
||
247 | * @Template |
||
248 | * @param Request $request |
||
249 | * @return array|Response |
||
250 | */ |
||
251 | public function verifySmsSecondFactorAction(Request $request) |
||
252 | { |
||
253 | $context = $this->getResponseContext(); |
||
254 | $originalRequestId = $context->getInResponseTo(); |
||
255 | |||
256 | /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */ |
||
257 | $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId); |
||
258 | |||
259 | $selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger); |
||
260 | |||
261 | $logger->notice('Verifying possession of SMS second factor, preparing to send'); |
||
262 | |||
263 | $command = new SendSmsChallengeCommand(); |
||
264 | $command->secondFactorId = $selectedSecondFactor; |
||
265 | |||
266 | $form = $this->createForm('gateway_send_sms_challenge', $command)->handleRequest($request); |
||
267 | |||
268 | $stepupService = $this->getStepupService(); |
||
269 | $phoneNumber = InternationalPhoneNumber::fromStringFormat( |
||
270 | $stepupService->getSecondFactorIdentifier($selectedSecondFactor) |
||
271 | ); |
||
272 | |||
273 | $otpRequestsRemaining = $stepupService->getSmsOtpRequestsRemainingCount(); |
||
274 | $maximumOtpRequests = $stepupService->getSmsMaximumOtpRequestsCount(); |
||
275 | $viewVariables = ['otpRequestsRemaining' => $otpRequestsRemaining, 'maximumOtpRequests' => $maximumOtpRequests]; |
||
276 | |||
277 | if ($form->get('cancel')->isClicked()) { |
||
278 | return $this->forward('SurfnetStepupGatewayGatewayBundle:Gateway:sendAuthenticationCancelledByUser'); |
||
279 | } |
||
280 | |||
281 | if (!$form->isValid()) { |
||
282 | return array_merge($viewVariables, ['phoneNumber' => $phoneNumber, 'form' => $form->createView()]); |
||
283 | } |
||
284 | |||
285 | $logger->notice('Verifying possession of SMS second factor, sending challenge per SMS'); |
||
286 | |||
287 | if (!$stepupService->sendSmsChallenge($command)) { |
||
288 | $form->addError(new FormError('gateway.form.send_sms_challenge.sms_sending_failed')); |
||
289 | |||
290 | return array_merge($viewVariables, ['phoneNumber' => $phoneNumber, 'form' => $form->createView()]); |
||
291 | } |
||
292 | |||
293 | return $this->redirect($this->generateUrl('gateway_verify_second_factor_sms_verify_challenge')); |
||
294 | } |
||
295 | |||
296 | /** |
||
297 | * @Template |
||
298 | * @param Request $request |
||
299 | * @return array|Response |
||
300 | */ |
||
301 | public function verifySmsSecondFactorChallengeAction(Request $request) |
||
302 | { |
||
303 | $context = $this->getResponseContext(); |
||
304 | $originalRequestId = $context->getInResponseTo(); |
||
305 | |||
306 | /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */ |
||
307 | $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId); |
||
308 | |||
309 | $selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger); |
||
310 | |||
311 | $command = new VerifyPossessionOfPhoneCommand(); |
||
312 | $form = $this->createForm('gateway_verify_sms_challenge', $command)->handleRequest($request); |
||
313 | |||
314 | if ($form->get('cancel')->isClicked()) { |
||
315 | return $this->forward('SurfnetStepupGatewayGatewayBundle:Gateway:sendAuthenticationCancelledByUser'); |
||
316 | } |
||
317 | |||
318 | if (!$form->isValid()) { |
||
319 | return ['form' => $form->createView()]; |
||
320 | } |
||
321 | |||
322 | $logger->notice('Verifying input SMS challenge matches'); |
||
323 | |||
324 | $verification = $this->getStepupService()->verifySmsChallenge($command); |
||
325 | |||
326 | if ($verification->wasSuccessful()) { |
||
327 | $this->getStepupService()->clearSmsVerificationState(); |
||
328 | |||
329 | $this->getResponseContext()->markSecondFactorVerified(); |
||
330 | $this->getAuthenticationLogger()->logSecondFactorAuthentication($originalRequestId); |
||
331 | |||
332 | $logger->info( |
||
333 | sprintf( |
||
334 | 'Marked Sms Second Factor "%s" as verified, forwarding to Saml Proxy to respond', |
||
335 | $selectedSecondFactor |
||
336 | ) |
||
337 | ); |
||
338 | |||
339 | return $this->forward($context->getResponseAction()); |
||
340 | } elseif ($verification->didOtpExpire()) { |
||
341 | $logger->notice('SMS challenge expired'); |
||
342 | $form->addError(new FormError('gateway.form.send_sms_challenge.challenge_expired')); |
||
343 | } elseif ($verification->wasAttemptedTooManyTimes()) { |
||
344 | $logger->notice('SMS challenge verification was attempted too many times'); |
||
345 | $form->addError(new FormError('gateway.form.send_sms_challenge.too_many_attempts')); |
||
346 | } else { |
||
347 | $logger->notice('SMS challenge did not match'); |
||
348 | $form->addError(new FormError('gateway.form.send_sms_challenge.sms_challenge_incorrect')); |
||
349 | } |
||
350 | |||
351 | return ['form' => $form->createView()]; |
||
352 | } |
||
353 | |||
354 | /** |
||
355 | * @Template |
||
356 | */ |
||
357 | public function initiateU2fAuthenticationAction() |
||
405 | |||
406 | /** |
||
407 | * @Template("SurfnetStepupGatewayGatewayBundle:SecondFactor:initiateU2fAuthentication.html.twig") |
||
408 | * |
||
409 | * @param Request $request |
||
410 | * @return array|Response |
||
411 | */ |
||
412 | public function verifyU2fAuthenticationAction(Request $request) |
||
474 | |||
475 | public function cancelU2fAuthenticationAction() |
||
479 | |||
480 | /** |
||
481 | * @return \Surfnet\StepupGateway\GatewayBundle\Service\StepupAuthenticationService |
||
482 | */ |
||
483 | private function getStepupService() |
||
487 | |||
488 | /** |
||
489 | * @return ResponseContext |
||
490 | */ |
||
491 | private function getResponseContext() |
||
495 | |||
496 | /** |
||
497 | * @return \Surfnet\StepupGateway\GatewayBundle\Monolog\Logger\AuthenticationLogger |
||
498 | */ |
||
499 | private function getAuthenticationLogger() |
||
503 | |||
504 | /** |
||
505 | * @param ResponseContext $context |
||
506 | * @param LoggerInterface $logger |
||
507 | * @return string |
||
508 | */ |
||
509 | private function getSelectedSecondFactor(ResponseContext $context, LoggerInterface $logger) |
||
521 | } |
||
522 |
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: