Passed
Push — master ( c4afc2...9cde23 )
by Pieter van der
27:49 queued 12:42
created

RecoveryTokenController   A

Complexity

Total Complexity 24

Size/Duplication

Total Lines 327
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 138
dl 0
loc 327
rs 10
c 1
b 0
f 0
wmc 24

9 Methods

Rating   Name   Duplication   Size   Complexity  
A deleteAction() 0 21 4
A createSmsAction() 0 12 2
A stepUpAction() 0 30 3
A proveSmsPossessionAction() 0 7 1
A createSafeStoreAction() 0 34 5
A stepUpConsumeAssertionAction() 0 34 4
A __construct() 0 27 1
A newRecoveryTokenAction() 0 19 3
A selectTokenTypeAction() 0 11 1
1
<?php
2
3
/**
4
 * Copyright 2022 SURFnet B.V.
5
 *
6
 * Licensed under the Apache License, Version 2.0 (the "License");
7
 * you may not use this file except in compliance with the License.
8
 * You may obtain a copy of the License at
9
 *
10
 *     http://www.apache.org/licenses/LICENSE-2.0
11
 *
12
 * Unless required by applicable law or agreed to in writing, software
13
 * distributed under the License is distributed on an "AS IS" BASIS,
14
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
 * See the License for the specific language governing permissions and
16
 * limitations under the License.
17
 */
18
19
namespace Surfnet\StepupSelfService\SelfServiceBundle\Controller;
20
21
use Exception;
22
use Psr\Log\LoggerInterface;
23
use Surfnet\SamlBundle\Entity\IdentityProvider;
24
use Surfnet\SamlBundle\Entity\ServiceProvider;
25
use Surfnet\SamlBundle\Http\PostBinding;
26
use Surfnet\SamlBundle\Http\RedirectBinding;
27
use Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger;
28
use Surfnet\SamlBundle\SAML2\Response\Assertion\InResponseTo;
29
use Surfnet\StepupBundle\Service\LoaResolutionService;
30
use Surfnet\StepupBundle\Value\Loa;
31
use Surfnet\StepupMiddlewareClientBundle\Exception\NotFoundException;
32
use Surfnet\StepupSelfService\SelfServiceBundle\Command\PromiseSafeStorePossessionCommand;
33
use Surfnet\StepupSelfService\SelfServiceBundle\Command\RevokeRecoveryTokenCommand;
34
use Surfnet\StepupSelfService\SelfServiceBundle\Exception\LogicException;
35
use Surfnet\StepupSelfService\SelfServiceBundle\Form\Type\PromiseSafeStorePossessionType;
36
use Surfnet\StepupSelfService\SelfServiceBundle\Service\SecondFactorService;
37
use Surfnet\StepupSelfService\SelfServiceBundle\Service\SelfAssertedTokens\AuthenticationRequestFactory;
38
use Surfnet\StepupSelfService\SelfServiceBundle\Service\SelfAssertedTokens\RecoveryTokenService;
39
use Surfnet\StepupSelfService\SelfServiceBundle\Service\SelfAssertedTokens\RecoveryTokenState;
40
use Surfnet\StepupSelfService\SelfServiceBundle\Service\SelfAssertedTokens\SafeStoreService;
41
use Surfnet\StepupSelfService\SelfServiceBundle\Service\SmsRecoveryTokenService;
42
use Symfony\Component\HttpFoundation\Request;
43
use Symfony\Component\HttpFoundation\Response;
44
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
45
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
46
use Symfony\Component\Security\Core\Exception\AuthenticationException;
47
48
/**
49
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
50
51
 */
52
class RecoveryTokenController extends Controller
53
{
54
    use RecoveryTokenControllerTrait;
0 ignored issues
show
introduced by
The trait Surfnet\StepupSelfServic...eryTokenControllerTrait requires some properties which are not provided by Surfnet\StepupSelfServic...RecoveryTokenController: $id, $recoveryTokenId
Loading history...
55
    /**
56
     * @var RecoveryTokenService
57
     */
58
    private $recoveryTokenService;
59
60
    /**
61
     * @var SecondFactorService
62
     */
63
    private $secondFactorService;
64
65
    /**
66
     * @var LoggerInterface
67
     */
68
    private $logger;
69
70
    /**
71
     * @var SafeStoreService
72
     */
73
    private $safeStoreService;
74
75
    /**
76
     * @var SmsRecoveryTokenService
77
     */
78
    private $smsService;
79
80
    /**
81
     * @var LoaResolutionService
82
     */
83
    private $loaResolutionService;
84
85
    /**
86
     * @var AuthenticationRequestFactory
87
     */
88
    private $authnRequestFactory;
89
90
    /**
91
     * @var SamlAuthenticationLogger
92
     */
93
    private $samlLogger;
94
95
    /**
96
     * @var RedirectBinding
97
     */
98
    private $redirectBinding;
99
100
    /**
101
     * @var PostBinding
102
     */
103
    private $postBinding;
104
105
    /**
106
     * @var ServiceProvider
107
     */
108
    private $serviceProvider;
109
110
    /**
111
     * @var IdentityProvider
112
     */
113
    private $identityProvider;
114
115
    /**
116
     * @SuppressWarnings(PHPMD.ExcessiveParameterList)
117
     */
118
    public function __construct(
119
        RecoveryTokenService $recoveryTokenService,
120
        SafeStoreService $safeStoreService,
121
        SecondFactorService $secondFactorService,
122
        SmsRecoveryTokenService $smsService,
123
        LoaResolutionService $loaResolutionService,
124
        AuthenticationRequestFactory $authenticationRequestFactory,
125
        RedirectBinding $redirectBinding,
126
        PostBinding $postBinding,
127
        ServiceProvider $serviceProvider,
128
        IdentityProvider $identityProvider,
129
        SamlAuthenticationLogger $samlLogger,
130
        LoggerInterface $logger
131
    ) {
132
        $this->recoveryTokenService = $recoveryTokenService;
133
        $this->safeStoreService = $safeStoreService;
134
        $this->secondFactorService = $secondFactorService;
135
        $this->loaResolutionService = $loaResolutionService;
136
        $this->authnRequestFactory = $authenticationRequestFactory;
137
        $this->redirectBinding = $redirectBinding;
138
        $this->postBinding = $postBinding;
139
        $this->serviceProvider = $serviceProvider;
140
        $this->identityProvider = $identityProvider;
141
        $this->samlLogger = $samlLogger;
142
        $this->logger = $logger;
143
        // Looks like an unused service, is used in RecoveryTokenControllerTrait
144
        $this->smsService = $smsService;
145
    }
146
147
    /**
148
     * Recovery Tokens: Select the token type to add
149
     * Shows an overview of the available token types for this Identity
150
     */
151
    public function selectTokenTypeAction(): Response
152
    {
153
        $this->logger->info('Determining which recovery token are available');
154
        $identity = $this->getIdentity();
155
        $this->assertMayAddRecoveryToken($identity);
156
157
        $availableRecoveryTokens = $this->recoveryTokenService->getRemainingTokenTypes($identity);
158
159
        return $this->render(
160
            '@SurfnetStepupSelfServiceSelfService/registration/self_asserted_tokens/select_recovery_token.html.twig',
161
            ['availableRecoveryTokens' => $availableRecoveryTokens]
162
        );
163
    }
164
165
    public function newRecoveryTokenAction($secondFactorId): Response
166
    {
167
        $this->logger->info('Determining which recovery token are available');
168
        $identity = $this->getIdentity();
169
        $this->assertSecondFactorInPossession($secondFactorId, $identity);
170
        $this->assertNoRecoveryTokens($identity);
171
172
        $secondFactor = $this->secondFactorService->findOneVerified($secondFactorId);
173
        $availableRecoveryTokens = $this->recoveryTokenService->getRemainingTokenTypes($identity);
174
        if ($secondFactor && $secondFactor->type === 'sms') {
175
            $this->logger->notice('SMS recovery token type is not allowed as we are vetting a SMS second factor');
176
            unset($availableRecoveryTokens['sms']);
177
        }
178
179
        return $this->render(
180
            '@SurfnetStepupSelfServiceSelfService/registration/self_asserted_tokens/new_recovery_token.html.twig',
181
            [
182
                'secondFactorId' => $secondFactorId,
183
                'availableRecoveryTokens' => $availableRecoveryTokens
184
            ]
185
        );
186
    }
187
188
    /**
189
     * Reovery Tokens: create a token of safe-store type
190
     *
191
     * Shows the one-time secret and asks the Identity to store the
192
     * password in a safe location.
193
     *
194
     * Note: A stepup authentication is required to perform this action.
195
     */
196
    public function createSafeStoreAction(Request $request): Response
197
    {
198
        if (!$this->recoveryTokenService->wasStepUpGiven()) {
199
            $this->recoveryTokenService->setReturnTo(RecoveryTokenState::RECOVERY_TOKEN_RETURN_TO_CREATE_SAFE_STORE);
200
            return $this->forward("Surfnet\StepupSelfService\SelfServiceBundle\Controller\RecoveryTokenController::stepUpAction");
201
        }
202
        $this->recoveryTokenService->resetReturnTo();
203
204
        $identity = $this->getIdentity();
205
        $this->assertNoRecoveryTokenOfType('safe-store', $identity);
206
        $secret = $this->safeStoreService->produceSecret();
207
        $command = new PromiseSafeStorePossessionCommand();
208
209
        $form = $this->createForm(PromiseSafeStorePossessionType::class, $command)->handleRequest($request);
210
211
        if ($form->isSubmitted() && $form->isValid()) {
212
            $command->secret = $secret;
213
            $command->identity = $identity;
214
215
            $executionResult = $this->safeStoreService->promisePossession($command);
216
            if (!$executionResult->getErrors()) {
217
                $this->recoveryTokenService->resetStepUpGiven();
218
                return $this->redirect(
219
                    $this->generateUrl('ss_second_factor_list')
220
                );
221
            }
222
            $this->addFlash('error', 'ss.form.recovery_token.error.error_message');
223
        }
224
225
        return $this->render(
226
            '@SurfnetStepupSelfServiceSelfService/registration/self_asserted_tokens/create_safe_store.html.twig',
227
            [
228
                'form' => $form->createView(),
229
                'secret' => $secret,
230
            ]
231
        );
232
    }
233
234
    /**
235
     * Recovery Tokens: Create the SMS recovery token
236
     * Step 1: Send an OTP to phone of Identity
237
     *
238
     * Note: Shares logic with the registration SMS recovery token send challenge action
239
     * Note: A stepup authentication is required to perform this action.
240
     */
241
    public function createSmsAction(Request $request): Response
242
    {
243
        if (!$this->recoveryTokenService->wasStepUpGiven()) {
244
            $this->recoveryTokenService->setReturnTo(RecoveryTokenState::RECOVERY_TOKEN_RETURN_TO_CREATE_SMS);
245
            return $this->forward("Surfnet\StepupSelfService\SelfServiceBundle\Controller\RecoveryTokenController::stepUpAction");
246
        }
247
        $this->recoveryTokenService->resetReturnTo();
248
249
        return $this->handleSmsChallenge(
250
            $request,
251
            '@SurfnetStepupSelfServiceSelfService/registration/self_asserted_tokens/create_sms.html.twig',
252
            'ss_recovery_token_prove_sms_possession'
253
        );
254
    }
255
256
    /**
257
     * Recovery Tokens: Create the SMS recovery token
258
     * Step 2: Process proof of phone possession of Identity
259
     *
260
     * Note: Shares logic with the registration SMS recovery token send challenge action
261
     */
262
    public function proveSmsPossessionAction(Request $request): Response
263
    {
264
        $this->recoveryTokenService->resetStepUpGiven();
265
        return $this->handleSmsProofOfPossession(
266
            $request,
267
            '@SurfnetStepupSelfServiceSelfService/registration/self_asserted_tokens/sms_prove_possession.html.twig',
268
            'ss_second_factor_list'
269
        );
270
    }
271
272
    /**
273
     * Recovery Tokens: delete a recovery token
274
     *
275
     * Regardless of token type, the recovery token in possession of an Identity
276
     * is revoked.
277
     *
278
     * Note: A stepup authentication is required to perform this action.
279
     */
280
    public function deleteAction(string $recoveryTokenId): Response
281
    {
282
        $this->assertRecoveryTokenInPossession($recoveryTokenId, $this->getIdentity());
283
        try {
284
            $recoveryToken = $this->recoveryTokenService->getRecoveryToken($recoveryTokenId);
285
            $command = new RevokeRecoveryTokenCommand();
286
            $command->identity = $this->getIdentity();
287
            $command->recoveryToken = $recoveryToken;
288
            $executionResult = $this->safeStoreService->revokeRecoveryToken($command);
289
            if (!empty($executionResult->getErrors())) {
290
                $this->addFlash('error', 'ss.form.recovery_token.delete.success');
291
                foreach ($executionResult->getErrors() as $error) {
292
                    $this->logger->error(sprintf('Recovery Token revocation failed with message: "%s"', $error));
293
                }
294
                return $this->redirect($this->generateUrl('ss_second_factor_list'));
295
            }
296
        } catch (NotFoundException $e) {
297
            throw new LogicException('Identity %s tried to remove an unpossessed recovery token');
298
        }
299
        $this->addFlash('success', 'ss.form.recovery_token.delete.success');
300
        return $this->redirect($this->generateUrl('ss_second_factor_list'));
301
    }
302
303
    /**
304
     * Create a step-up AuthNRequest
305
     *
306
     * This request is sent to the Gateway (using the SF test endpoint)
307
     * LoA 1.5 is requested, allowing use of self-asserted tokens.
308
     */
309
    public function stepUpAction(): Response
310
    {
311
        $this->logger->notice('Starting step up authentication for a recovery token action');
312
313
        $identity = $this->getIdentity();
314
315
        $vettedSecondFactors = $this->secondFactorService->findVettedByIdentity($identity->id);
316
        if (!$vettedSecondFactors || $vettedSecondFactors->getTotalItems() === 0) {
0 ignored issues
show
introduced by
$vettedSecondFactors is of type Surfnet\StepupMiddleware...dSecondFactorCollection, thus it always evaluated to true.
Loading history...
317
            $this->logger->error(
318
                sprintf(
319
                    'Identity "%s" tried to test a second factor, but does not own a suitable vetted token.',
320
                    $identity->id
321
                )
322
            );
323
            $this->addFlash('error', 'ss.recovery_token.step_up.no_tokens_available.failed');
324
            return $this->redirect($this->generateUrl('ss_second_factor_list'));
325
        }
326
327
        // By requesting LoA 1.5 any relevant token can be tested (LoA self asserted, 2 and 3)
328
        $authenticationRequest = $this->authnRequestFactory->createSecondFactorRequest(
329
            $identity->nameId,
330
            $this->loaResolutionService->getLoaByLevel(Loa::LOA_SELF_VETTED)
0 ignored issues
show
Bug introduced by
It seems like $this->loaResolutionServ...e\Loa::LOA_SELF_VETTED) can also be of type null; however, parameter $loa of Surfnet\StepupSelfServic...teSecondFactorRequest() does only seem to accept Surfnet\StepupBundle\Value\Loa, 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 ignore-type  annotation

330
            /** @scrutinizer ignore-type */ $this->loaResolutionService->getLoaByLevel(Loa::LOA_SELF_VETTED)
Loading history...
Bug introduced by
Surfnet\StepupBundle\Value\Loa::LOA_SELF_VETTED of type double is incompatible with the type integer expected by parameter $loaLevel of Surfnet\StepupBundle\Ser...ervice::getLoaByLevel(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

330
            $this->loaResolutionService->getLoaByLevel(/** @scrutinizer ignore-type */ Loa::LOA_SELF_VETTED)
Loading history...
331
        );
332
333
        $this->recoveryTokenService->startStepUpRequest($authenticationRequest->getRequestId());
334
335
        $samlLogger = $this->samlLogger->forAuthentication($authenticationRequest->getRequestId());
336
        $samlLogger->notice('Sending authentication request to the second factor test IDP');
337
338
        return $this->redirectBinding->createRedirectResponseFor($authenticationRequest);
0 ignored issues
show
Deprecated Code introduced by
The function Surfnet\SamlBundle\Http\...teRedirectResponseFor() has been deprecated: Please use the `createResponseFor` method instead ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

338
        return /** @scrutinizer ignore-deprecated */ $this->redirectBinding->createRedirectResponseFor($authenticationRequest);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
339
    }
340
341
    /**
342
     * Consume the Saml Response from the Step Up authentication
343
     * We need this step-up auth for adding and deleting recovery tokens.
344
     */
345
    public function stepUpConsumeAssertionAction(Request $httpRequest): Response
346
    {
347
        if (!$this->recoveryTokenService->hasStepUpRequest()) {
348
            $this->logger->error(
349
                'Received an authentication response modifying a recovery token, no matching request was found'
350
            );
351
            throw new AccessDeniedHttpException('Did not expect an authentication response');
352
        }
353
        $this->logger->notice('Received an authentication response for  a second factor');
354
        $initiatedRequestId = $this->recoveryTokenService->getStepUpRequest();
355
        $samlLogger = $this->samlLogger->forAuthentication($initiatedRequestId);
356
        $this->recoveryTokenService->deleteStepUpRequest();
357
        try {
358
            $assertion = $this->postBinding->processResponse(
359
                $httpRequest,
360
                $this->identityProvider,
361
                $this->serviceProvider
362
            );
363
            if (!InResponseTo::assertEquals($assertion, $initiatedRequestId)) {
364
                $samlLogger->error(
365
                    sprintf(
366
                        'Expected a response to the request with ID "%s", but the SAMLResponse was a response to a different request',
367
                        $initiatedRequestId
368
                    )
369
                );
370
                throw new AuthenticationException('Unexpected InResponseTo in SAMLResponse');
371
            }
372
        } catch (Exception $exception) {
373
            $this->addFlash('error', 'ss.recovery_token.step_up.failed');
374
        }
375
        // Store step-up was given in state
376
        $this->recoveryTokenService->stepUpGiven();
377
        $returnTo = $this->recoveryTokenService->returnTo();
378
        return $this->redirectToRoute($returnTo->getRoute(), $returnTo->getParameters());
379
    }
380
}
381