handleSmsProofOfPossession()   B
last analyzed

Complexity

Conditions 11
Paths 29

Size

Total Lines 57
Code Lines 36

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 11
eloc 36
c 2
b 0
f 0
nc 29
nop 4
dl 0
loc 57
rs 7.3166

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 Surfnet\StepupMiddlewareClientBundle\Identity\Dto\Identity;
22
use Surfnet\StepupSelfService\SelfServiceBundle\Command\SendRecoveryTokenSmsChallengeCommand;
23
use Surfnet\StepupSelfService\SelfServiceBundle\Command\VerifySmsRecoveryTokenChallengeCommand;
24
use Surfnet\StepupSelfService\SelfServiceBundle\Exception\LogicException;
25
use Surfnet\StepupSelfService\SelfServiceBundle\Form\Type\SendSmsChallengeType;
26
use Surfnet\StepupSelfService\SelfServiceBundle\Form\Type\VerifySmsChallengeType;
27
use Surfnet\StepupSelfService\SelfServiceBundle\Service\SmsRecoveryTokenService;
28
use Surfnet\StepupSelfService\SelfServiceBundle\Service\SmsSecondFactorServiceInterface;
29
use Symfony\Component\HttpFoundation\Request;
30
use Symfony\Component\HttpFoundation\Response;
31
32
/**
33
 * @SuppressWarnings(PHPMD.CyclomaticComplexity)
34
 */
35
trait RecoveryTokenControllerTrait
36
{
37
    /**
38
     * Send SMS challenge form handler
39
     * - One is used during SMS recovery token registration within the vetting flow
40
     * - The other is actioned from the recovery token overview on the '/overview' page
41
     *
42
     * Note fourth param: '$secondFactorId' is optional parameter, only used in the vetting flow scenario
43
     */
44
    private function handleSmsChallenge(
45
        Request $request,
46
        string $templateName,
47
        string $exitRoute,
48
        ?string $secondFactorId = null
49
    ): Response {
50
        $identity = $this->getIdentity();
0 ignored issues
show
Bug introduced by
It seems like getIdentity() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

50
        /** @scrutinizer ignore-call */ 
51
        $identity = $this->getIdentity();
Loading history...
51
        $this->assertNoRecoveryTokenOfType('sms', $identity);
52
        if ($secondFactorId) {
53
            $this->assertSecondFactorInPossession($secondFactorId, $identity);
54
        }
55
        $command = new SendRecoveryTokenSmsChallengeCommand();
56
        $form = $this->createForm(SendSmsChallengeType::class, $command)->handleRequest($request);
0 ignored issues
show
Bug introduced by
It seems like createForm() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

56
        $form = $this->/** @scrutinizer ignore-call */ createForm(SendSmsChallengeType::class, $command)->handleRequest($request);
Loading history...
57
        $otpRequestsRemaining = $this->smsService
58
            ->getOtpRequestsRemainingCount(SmsSecondFactorServiceInterface::REGISTRATION_SECOND_FACTOR_ID);
59
        $maximumOtpRequests = $this->smsService->getMaximumOtpRequestsCount();
60
61
        $viewVariables = [
62
            'otpRequestsRemaining' => $otpRequestsRemaining,
63
            'maximumOtpRequests' => $maximumOtpRequests
64
        ];
65
66
        if (isset($secondFactorId)) {
67
            $viewVariables['secondFactorId'] = $secondFactorId;
68
        }
69
70
        if ($form->isSubmitted() && $form->isValid()) {
71
            $command->identity = $identity->id;
72
            $command->institution = $identity->institution;
73
74
            if ($otpRequestsRemaining === 0) {
75
                $this->addFlash('error', 'ss.prove_phone_possession.challenge_request_limit_reached');
0 ignored issues
show
Bug introduced by
It seems like addFlash() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

75
                $this->/** @scrutinizer ignore-call */ 
76
                       addFlash('error', 'ss.prove_phone_possession.challenge_request_limit_reached');
Loading history...
76
                $parameters = array_merge(['form' => $form->createView()], $viewVariables);
77
                return $this->render($templateName, $parameters);
0 ignored issues
show
Bug introduced by
It seems like render() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

77
                return $this->/** @scrutinizer ignore-call */ render($templateName, $parameters);
Loading history...
78
            }
79
80
            if ($this->smsService->sendChallenge($command)) {
81
                $urlParameter = [];
82
                if (isset($secondFactorId)) {
83
                    $urlParameter = ['secondFactorId' => $secondFactorId];
84
                }
85
                return $this->redirect($this->generateUrl($exitRoute, $urlParameter));
0 ignored issues
show
Bug introduced by
It seems like redirect() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

85
                return $this->/** @scrutinizer ignore-call */ redirect($this->generateUrl($exitRoute, $urlParameter));
Loading history...
Bug introduced by
It seems like generateUrl() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

85
                return $this->redirect($this->/** @scrutinizer ignore-call */ generateUrl($exitRoute, $urlParameter));
Loading history...
86
            }
87
            $this->addFlash('error', 'ss.form.recovery_token.error.challenge_not_sent_error_message');
88
        }
89
        return $this->render(
90
            $templateName,
91
            array_merge(
92
                [
93
                    'form' => $form->createView(),
94
                ],
95
                $viewVariables
96
            )
97
        );
98
    }
99
100
    /**
101
     * Proof of possession of phone form handler
102
     *
103
     * Note fourth param: '$secondFactorId' is optional parameter, only used in the vetting flow scenario
104
     */
105
    private function handleSmsProofOfPossession(
106
        Request $request,
107
        string $templateName,
108
        string $exitRoute,
109
        ?string $secondFactorId = null
110
    ) {
111
        if (!$this->smsService->hasSmsVerificationState(SmsRecoveryTokenService::REGISTRATION_RECOVERY_TOKEN_ID)) {
112
            $this->get('session')->getFlashBag()->add('notice', 'ss.registration.sms.alert.no_verification_state');
0 ignored issues
show
Bug introduced by
It seems like get() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

112
            $this->/** @scrutinizer ignore-call */ 
113
                   get('session')->getFlashBag()->add('notice', 'ss.registration.sms.alert.no_verification_state');
Loading history...
113
            return $this->redirectToRoute('ss_recovery_token_sms');
0 ignored issues
show
Bug introduced by
It seems like redirectToRoute() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

113
            return $this->/** @scrutinizer ignore-call */ redirectToRoute('ss_recovery_token_sms');
Loading history...
114
        }
115
        $identity = $this->getIdentity();
116
        $this->assertNoRecoveryTokenOfType('sms', $identity);
117
118
        if ($secondFactorId) {
119
            $this->assertSecondFactorInPossession($secondFactorId, $identity);
120
        }
121
122
        $command = new VerifySmsRecoveryTokenChallengeCommand();
123
        $command->identity = $identity->id;
124
125
        $command->resendRoute = 'ss_registration_recovery_token_sms';
126
        $command->resendRouteParameters = ['secondFactorId' => $secondFactorId, 'recoveryTokenId' => null];
127
128
        if (!$secondFactorId) {
129
            $command->resendRoute = 'ss_recovery_token_sms';
130
            $command->resendRouteParameters = [];
131
        }
132
133
        $form = $this->createForm(VerifySmsChallengeType::class, $command)->handleRequest($request);
134
135
        if ($form->isSubmitted() && $form->isValid()) {
136
            $result = $this->smsService->provePossession($command);
137
            if ($result->isSuccessful()) {
138
                $this->smsService->forgetRecoveryTokenState();
139
                $this->smsService->tokenCreatedDuringSecondFactorRegistration();
140
141
                $this->smsService->clearSmsVerificationState(SmsRecoveryTokenService::REGISTRATION_RECOVERY_TOKEN_ID);
142
                $urlParameter = [];
143
                if (isset($secondFactorId)) {
144
                    $urlParameter = ['secondFactorId' => $secondFactorId];
145
                }
146
                return $this->redirect($this->generateUrl($exitRoute, $urlParameter));
147
            } elseif ($result->wasIncorrectChallengeResponseGiven()) {
148
                $this->addFlash('error', 'ss.prove_phone_possession.incorrect_challenge_response');
149
            } elseif ($result->hasChallengeExpired()) {
150
                $this->addFlash('error', 'ss.prove_phone_possession.challenge_expired');
151
            } elseif ($result->wereTooManyAttemptsMade()) {
152
                $this->addFlash('error', 'ss.prove_phone_possession.too_many_attempts');
153
            } else {
154
                $this->addFlash('error', 'ss.prove_phone_possession.proof_of_possession_failed');
155
            }
156
        }
157
158
        return $this->render(
159
            $templateName,
160
            [
161
                'form' => $form->createView(),
162
            ]
163
        );
164
    }
165
166
    private function assertRecoveryTokenInPossession(string $recoveryTokenId, Identity $identity)
167
    {
168
        $recoveryTokens = $this->recoveryTokenService->getRecoveryTokensForIdentity($identity);
169
        $found = false;
170
        foreach ($recoveryTokens as $recoveryToken) {
171
            if ($recoveryToken->recoveryTokenId === $recoveryTokenId) {
172
                $found = true;
173
            }
174
        }
175
        if (!$found) {
176
            throw new LogicException(
177
                sprintf(
178
                    'Identity "%s" tried to perform a self-asserted token registration with a ' .
179
                    'recovery token ("%s)", but does not own that recovery token',
180
                    $identity->id,
181
                    $recoveryTokenId
182
                )
183
            );
184
        }
185
    }
186
187
    private function assertNoRecoveryTokens(Identity $identity)
188
    {
189
        if ($this->recoveryTokenService->hasRecoveryToken($identity)) {
190
            throw new LogicException(
191
                sprintf(
192
                    'Identity "%s" tried to register a recovery token, but one was already in possession. ' .
193
                    'This is not allowed during self-asserted token registration.',
194
                    $identity->id
195
                )
196
            );
197
        }
198
    }
199
200
    private function assertNoRecoveryTokenOfType(string $type, Identity $identity)
201
    {
202
        $tokens = $this->recoveryTokenService->getRecoveryTokensForIdentity($identity);
203
        if (array_key_exists($type, $tokens)) {
204
            throw new LogicException(
205
                sprintf(
206
                    'Identity "%s" tried to register a recovery token, but one was already in possession. ' .
207
                    'This is not allowed during token registration.',
208
                    $identity->id
209
                )
210
            );
211
        }
212
    }
213
214
    private function assertMayAddRecoveryToken(Identity $identity)
215
    {
216
        $availableTypes = $this->recoveryTokenService->getRemainingTokenTypes($identity);
217
        if (count($availableTypes) === 0) {
218
            throw new LogicException(
219
                sprintf(
220
                    'Identity %s tried to register a token type, but all available token types have ' .
221
                    'already been registered',
222
                    $identity
223
                )
224
            );
225
        }
226
    }
227
228
    private function assertSecondFactorInPossession(string $secondFactorId, Identity $identity)
229
    {
230
        $identityOwnsSecondFactor = $this->secondFactorService->identityHasSecondFactorOfStateWithId(
231
            $identity->id,
232
            'verified',
233
            $secondFactorId
234
        );
235
236
        if (!$identityOwnsSecondFactor) {
237
            throw new LogicException(
238
                sprintf(
239
                    'Identity "%s" tried to register recovery token during registration ' .
240
                    'of second factor token "%s", but does not own that second factor',
241
                    $identity->id,
242
                    $secondFactorId
243
                )
244
            );
245
        }
246
    }
247
}
248