Passed
Push — feature/symfony6-upgrade ( 933605...693c71 )
by Paul
09:09 queued 02:29
created

RecoveryTokenController::newRecoveryToken()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 19
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 13
nc 2
nop 1
dl 0
loc 19
rs 9.8333
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types = 1);
4
5
/**
6
 * Copyright 2022 SURFnet B.V.
7
 *
8
 * Licensed under the Apache License, Version 2.0 (the "License");
9
 * you may not use this file except in compliance with the License.
10
 * You may obtain a copy of the License at
11
 *
12
 *     http://www.apache.org/licenses/LICENSE-2.0
13
 *
14
 * Unless required by applicable law or agreed to in writing, software
15
 * distributed under the License is distributed on an "AS IS" BASIS,
16
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
 * See the License for the specific language governing permissions and
18
 * limitations under the License.
19
 */
0 ignored issues
show
Coding Style introduced by
PHP version not specified
Loading history...
Coding Style introduced by
Missing @category tag in file comment
Loading history...
Coding Style introduced by
Missing @package tag in file comment
Loading history...
Coding Style introduced by
Missing @author tag in file comment
Loading history...
Coding Style introduced by
Missing @license tag in file comment
Loading history...
Coding Style introduced by
Missing @link tag in file comment
Loading history...
20
21
namespace Surfnet\StepupSelfService\SelfServiceBundle\Controller;
22
23
use Exception;
24
use Psr\Log\LoggerInterface;
25
use Surfnet\SamlBundle\Entity\IdentityProvider;
26
use Surfnet\SamlBundle\Entity\ServiceProvider;
27
use Surfnet\SamlBundle\Http\PostBinding;
28
use Surfnet\SamlBundle\Http\RedirectBinding;
29
use Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger;
30
use Surfnet\SamlBundle\SAML2\Response\Assertion\InResponseTo;
31
use Surfnet\StepupBundle\Service\LoaResolutionService;
32
use Surfnet\StepupBundle\Value\Loa;
33
use Surfnet\StepupMiddlewareClientBundle\Exception\NotFoundException;
34
use Surfnet\StepupSelfService\SelfServiceBundle\Command\PromiseSafeStorePossessionCommand;
35
use Surfnet\StepupSelfService\SelfServiceBundle\Command\RevokeRecoveryTokenCommand;
36
use Surfnet\StepupSelfService\SelfServiceBundle\Exception\LogicException;
37
use Surfnet\StepupSelfService\SelfServiceBundle\Form\Type\PromiseSafeStorePossessionType;
38
use Surfnet\StepupSelfService\SelfServiceBundle\Service\SecondFactorService;
39
use Surfnet\StepupSelfService\SelfServiceBundle\Service\SelfAssertedTokens\AuthenticationRequestFactory;
40
use Surfnet\StepupSelfService\SelfServiceBundle\Service\SelfAssertedTokens\RecoveryTokenService;
41
use Surfnet\StepupSelfService\SelfServiceBundle\Service\SelfAssertedTokens\RecoveryTokenState;
42
use Surfnet\StepupSelfService\SelfServiceBundle\Service\SelfAssertedTokens\SafeStoreService;
43
use Surfnet\StepupSelfService\SelfServiceBundle\Service\SmsRecoveryTokenService;
44
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
45
use Symfony\Component\HttpFoundation\Request;
46
use Symfony\Component\HttpFoundation\Response;
47
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
48
use Symfony\Component\Routing\Attribute\Route;
49
use Symfony\Component\Security\Core\Exception\AuthenticationException;
50
51
/**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
52
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
53
 */
0 ignored issues
show
Coding Style introduced by
Missing @category tag in class comment
Loading history...
Coding Style introduced by
Missing @package tag in class comment
Loading history...
Coding Style introduced by
Missing @author tag in class comment
Loading history...
Coding Style introduced by
Missing @license tag in class comment
Loading history...
Coding Style introduced by
Missing @link tag in class comment
Loading history...
54
class RecoveryTokenController extends AbstractController
55
{
56
    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...
57
58
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
Coding Style introduced by
Parameter $recoveryTokenService should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $safeStoreService should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $secondFactorService should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $smsService should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $loaResolutionService should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $authnRequestFactory should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $redirectBinding should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $postBinding should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $serviceProvider should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $identityProvider should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $samlLogger should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $logger should have a doc-comment as per coding-style.
Loading history...
59
     * @SuppressWarnings(PHPMD.ExcessiveParameterList)
60
     */
61
    public function __construct(
62
        private readonly RecoveryTokenService         $recoveryTokenService,
63
        private readonly SafeStoreService             $safeStoreService,
64
        private readonly SecondFactorService          $secondFactorService,
65
        private readonly SmsRecoveryTokenService      $smsService,
66
        private readonly LoaResolutionService         $loaResolutionService,
67
        private readonly AuthenticationRequestFactory $authnRequestFactory,
68
        private readonly RedirectBinding              $redirectBinding,
69
        private readonly PostBinding                  $postBinding,
70
        private readonly ServiceProvider              $serviceProvider,
71
        private readonly IdentityProvider             $identityProvider,
72
        private readonly SamlAuthenticationLogger     $samlLogger,
73
        private readonly LoggerInterface              $logger
74
    ) {
75
    }
76
77
    /**
78
     * Recovery Tokens: Select the token type to add
79
     * Shows an overview of the available token types for this Identity
80
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
81
    #[Route(
82
        path: '/recovery-token/select-recovery-token',
83
        name: 'ss_recovery_token_display_types',
84
        methods: ['GET'],
85
    )]
86
    public function selectTokenType(): Response
87
    {
88
        $this->logger->info('Determining which recovery token are available');
89
        $identity = $this->getUser()->getIdentity();
0 ignored issues
show
Bug introduced by
The method getIdentity() does not exist on Symfony\Component\Security\Core\User\UserInterface. It seems like you code against a sub-type of Symfony\Component\Security\Core\User\UserInterface such as Surfnet\StepupSelfServic...n\AuthenticatedIdentity. ( Ignorable by Annotation )

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

89
        $identity = $this->getUser()->/** @scrutinizer ignore-call */ getIdentity();
Loading history...
90
        $this->assertMayAddRecoveryToken($identity);
91
92
        $availableRecoveryTokens = $this->recoveryTokenService->getRemainingTokenTypes($identity);
93
94
        return $this->render(
95
            'registration/self_asserted_tokens/select_recovery_token.html.twig',
96
            ['availableRecoveryTokens' => $availableRecoveryTokens]
97
        );
98
    }
99
100
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $request should have a doc-comment as per coding-style.
Loading history...
101
     * Reovery Tokens: create a token of safe-store type
102
     *
103
     * Shows the one-time secret and asks the Identity to store the
104
     * password in a safe location.
105
     *
106
     * Note: A stepup authentication is required to perform this action.
107
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
108
    #[Route(
109
        path: '/recovery-token/create-safe-store',
110
        name: 'ss_recovery_token_safe_store',
111
        methods: ['GET', 'POST'],
112
    )]
113
    public function createSafeStore(Request $request): Response
114
    {
115
        if (!$this->recoveryTokenService->wasStepUpGiven()) {
116
            $this->recoveryTokenService->setReturnTo(RecoveryTokenState::RECOVERY_TOKEN_RETURN_TO_CREATE_SAFE_STORE);
117
            return $this->forward("Surfnet\StepupSelfService\SelfServiceBundle\Controller\RecoveryTokenController::stepUp");
118
        }
119
        $this->recoveryTokenService->resetReturnTo();
120
121
        $identity = $this->getUser()->getIdentity();
122
        $this->assertNoRecoveryTokenOfType('safe-store', $identity);
123
        $secret = $this->safeStoreService->produceSecret();
124
        $command = new PromiseSafeStorePossessionCommand();
125
126
        $form = $this->createForm(PromiseSafeStorePossessionType::class, $command)->handleRequest($request);
127
128
        if ($form->isSubmitted() && $form->isValid()) {
129
            $command->secret = $secret;
130
            $command->identity = $identity;
131
132
            $executionResult = $this->safeStoreService->promisePossession($command);
133
            if (!$executionResult->getErrors()) {
134
                $this->recoveryTokenService->resetStepUpGiven();
135
                return $this->redirect(
136
                    $this->generateUrl('ss_second_factor_list')
137
                );
138
            }
139
            $this->addFlash('error', 'ss.form.recovery_token.error.error_message');
140
        }
141
142
        return $this->render(
143
            'registration/self_asserted_tokens/create_safe_store.html.twig',
144
            [
145
                'form' => $form->createView(),
146
                'secret' => $secret,
147
            ]
148
        );
149
    }
150
151
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $request should have a doc-comment as per coding-style.
Loading history...
152
     * Recovery Tokens: Create the SMS recovery token
153
     * Step 1: Send an OTP to phone of Identity
154
     *
155
     * Note: Shares logic with the registration SMS recovery token send challenge action
156
     * Note: A stepup authentication is required to perform this action.
157
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
158
    #[Route(
159
        path: '/recovery-token/create-sms',
160
        name: 'ss_recovery_token_sms',
161
        methods: ['GET', 'POST'],
162
    )]
163
    public function createSms(Request $request): Response
164
    {
165
        if (!$this->recoveryTokenService->wasStepUpGiven()) {
166
            $this->recoveryTokenService->setReturnTo(RecoveryTokenState::RECOVERY_TOKEN_RETURN_TO_CREATE_SMS);
167
            return $this->forward("Surfnet\StepupSelfService\SelfServiceBundle\Controller\RecoveryTokenController::stepUp");
168
        }
169
        $this->recoveryTokenService->resetReturnTo();
170
171
        return $this->handleSmsChallenge(
172
            $request,
173
            'registration/self_asserted_tokens/create_sms.html.twig',
174
            'ss_recovery_token_prove_sms_possession'
175
        );
176
    }
177
178
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $request should have a doc-comment as per coding-style.
Loading history...
179
     * Recovery Tokens: Create the SMS recovery token
180
     * Step 2: Process proof of phone possession of Identity
181
     *
182
     * Note: Shares logic with the registration SMS recovery token send challenge action
183
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
184
    #[Route(
185
        path: '/recovery-token/prove-sms-possession',
186
        name: 'ss_recovery_token_prove_sms_possession',
187
        methods: ['GET', 'POST'],
188
    )]
189
    public function proveSmsPossession(Request $request): Response
190
    {
191
        $this->recoveryTokenService->resetStepUpGiven();
192
        return $this->handleSmsProofOfPossession(
193
            $request,
194
            'registration/self_asserted_tokens/sms_prove_possession.html.twig',
195
            'ss_second_factor_list'
196
        );
197
    }
198
199
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $recoveryTokenId should have a doc-comment as per coding-style.
Loading history...
200
     * Recovery Tokens: delete a recovery token
201
     *
202
     * Regardless of token type, the recovery token in possession of an Identity
203
     * is revoked.
204
     *
205
     * Note: A stepup authentication is required to perform this action.
206
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
207
    #[Route(
208
        path: '/recovery-token/delete/{recoveryTokenId}',
209
        name: 'ss_recovery_token_delete',
210
        methods: ['GET'],
211
    )]
212
    public function delete(string $recoveryTokenId): Response
213
    {
214
        $this->assertRecoveryTokenInPossession($recoveryTokenId, $this->getUser()->getIdentity());
215
        try {
216
            $recoveryToken = $this->recoveryTokenService->getRecoveryToken($recoveryTokenId);
217
            $command = new RevokeRecoveryTokenCommand();
218
            $command->identity = $this->getUser()->getIdentity();
219
            $command->recoveryToken = $recoveryToken;
220
            $executionResult = $this->safeStoreService->revokeRecoveryToken($command);
221
            if ($executionResult->getErrors() !== []) {
222
                $this->addFlash('error', 'ss.form.recovery_token.delete.success');
223
                foreach ($executionResult->getErrors() as $error) {
224
                    $this->logger->error(sprintf('Recovery Token revocation failed with message: "%s"', $error));
225
                }
226
                return $this->redirect($this->generateUrl('ss_second_factor_list'));
227
            }
228
        } catch (NotFoundException) {
229
            throw new LogicException('Identity %s tried to remove an unpossessed recovery token');
230
        }
231
        $this->addFlash('success', 'ss.form.recovery_token.delete.success');
232
        return $this->redirect($this->generateUrl('ss_second_factor_list'));
233
    }
234
235
    /**
236
     * Create a step-up AuthNRequest
237
     *
238
     * This request is sent to the Gateway (using the SF test endpoint)
239
     * LoA 1.5 is requested, allowing use of self-asserted tokens.
240
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
241
    public function stepUp(): Response
242
    {
243
        $this->logger->notice('Starting step up authentication for a recovery token action');
244
245
        $identity = $this->getUser()->getIdentity();
246
247
        $vettedSecondFactors = $this->secondFactorService->findVettedByIdentity($identity->id);
248
        if (!$vettedSecondFactors || $vettedSecondFactors->getTotalItems() === 0) {
249
            $this->logger->error(
250
                sprintf(
251
                    'Identity "%s" tried to test a second factor, but does not own a suitable vetted token.',
252
                    $identity->id
253
                )
254
            );
255
            $this->addFlash('error', 'ss.recovery_token.step_up.no_tokens_available.failed');
256
            return $this->redirect($this->generateUrl('ss_second_factor_list'));
257
        }
258
259
        // By requesting LoA 1.5 any relevant token can be tested (LoA self asserted, 2 and 3)
260
        $authenticationRequest = $this->authnRequestFactory->createSecondFactorRequest(
261
            $identity->nameId,
262
            $this->loaResolutionService->getLoaByLevel(Loa::LOA_SELF_VETTED)
0 ignored issues
show
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

262
            $this->loaResolutionService->getLoaByLevel(/** @scrutinizer ignore-type */ Loa::LOA_SELF_VETTED)
Loading history...
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

262
            /** @scrutinizer ignore-type */ $this->loaResolutionService->getLoaByLevel(Loa::LOA_SELF_VETTED)
Loading history...
263
        );
264
265
        $this->recoveryTokenService->startStepUpRequest($authenticationRequest->getRequestId());
266
267
        $samlLogger = $this->samlLogger->forAuthentication($authenticationRequest->getRequestId());
268
        $samlLogger->notice('Sending authentication request to the second factor test IDP');
269
270
        return $this->redirectBinding->createResponseFor($authenticationRequest);
271
    }
272
273
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $httpRequest should have a doc-comment as per coding-style.
Loading history...
274
     * Consume the Saml Response from the Step Up authentication
275
     * We need this step-up auth for adding and deleting recovery tokens.
276
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
277
    public function stepUpConsumeAssertion(Request $httpRequest): Response
278
    {
279
        if (!$this->recoveryTokenService->hasStepUpRequest()) {
280
            $this->logger->error(
281
                'Received an authentication response modifying a recovery token, no matching request was found'
282
            );
283
            throw new AccessDeniedHttpException('Did not expect an authentication response');
284
        }
285
        $this->logger->notice('Received an authentication response for  a second factor');
286
        $initiatedRequestId = $this->recoveryTokenService->getStepUpRequest();
287
        $samlLogger = $this->samlLogger->forAuthentication($initiatedRequestId);
288
        $this->recoveryTokenService->deleteStepUpRequest();
289
        try {
290
            $assertion = $this->postBinding->processResponse(
291
                $httpRequest,
292
                $this->identityProvider,
293
                $this->serviceProvider
294
            );
295
            if (!InResponseTo::assertEquals($assertion, $initiatedRequestId)) {
296
                $samlLogger->error(
297
                    sprintf(
298
                        'Expected a response to the request with ID "%s", but the SAMLResponse was a response to a different request',
299
                        $initiatedRequestId
300
                    )
301
                );
302
                throw new AuthenticationException('Unexpected InResponseTo in SAMLResponse');
303
            }
304
        } catch (Exception) {
305
            $this->addFlash('error', 'ss.recovery_token.step_up.failed');
306
        }
307
        // Store step-up was given in state
308
        $this->recoveryTokenService->stepUpGiven();
309
        $returnTo = $this->recoveryTokenService->returnTo();
310
        return $this->redirectToRoute($returnTo->getRoute(), $returnTo->getParameters());
311
    }
312
}
313