RecoveryTokenControllerTrait   A
last analyzed

Complexity

Total Complexity 31

Size/Duplication

Total Lines 203
Duplicated Lines 0 %

Importance

Changes 5
Bugs 0 Features 0
Metric Value
eloc 110
c 5
b 0
f 0
dl 0
loc 203
rs 9.92
wmc 31

7 Methods

Rating   Name   Duplication   Size   Complexity  
A assertSecondFactorInPossession() 0 15 2
A assertNoRecoveryTokens() 0 8 2
A assertRecoveryTokenInPossession() 0 16 4
B handleSmsChallenge() 0 48 8
A assertMayAddRecoveryToken() 0 9 2
A assertNoRecoveryTokenOfType() 0 9 2
B handleSmsProofOfPossession() 0 57 11
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 Surfnet\StepupMiddlewareClientBundle\Identity\Dto\Identity;
24
use Surfnet\StepupSelfService\SelfServiceBundle\Command\SendRecoveryTokenSmsChallengeCommand;
25
use Surfnet\StepupSelfService\SelfServiceBundle\Command\VerifySmsRecoveryTokenChallengeCommand;
26
use Surfnet\StepupSelfService\SelfServiceBundle\Exception\LogicException;
27
use Surfnet\StepupSelfService\SelfServiceBundle\Form\Type\SendSmsChallengeType;
28
use Surfnet\StepupSelfService\SelfServiceBundle\Form\Type\VerifySmsChallengeType;
29
use Surfnet\StepupSelfService\SelfServiceBundle\Service\SmsRecoveryTokenService;
30
use Surfnet\StepupSelfService\SelfServiceBundle\Service\SmsSecondFactorServiceInterface;
31
use Symfony\Component\HttpFoundation\Request;
32
use Symfony\Component\HttpFoundation\Response;
33
34
/**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
35
 * @SuppressWarnings(PHPMD.CyclomaticComplexity)
36
 */
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...
37
trait RecoveryTokenControllerTrait
38
{
39
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $request should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $templateName should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $exitRoute should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $secondFactorId should have a doc-comment as per coding-style.
Loading history...
40
     * Send SMS challenge form handler
41
     * - One is used during SMS recovery token registration within the vetting flow
42
     * - The other is actioned from the recovery token overview on the '/overview' page
43
     *
44
     * Note fourth param: '$secondFactorId' is optional parameter, only used in the vetting flow scenario
45
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
46
    private function handleSmsChallenge(
0 ignored issues
show
Coding Style introduced by
Private method name "RecoveryTokenControllerTrait::handleSmsChallenge" must be prefixed with an underscore
Loading history...
47
        Request $request,
48
        string $templateName,
49
        string $exitRoute,
50
        ?string $secondFactorId = null
51
    ): Response {
52
        $identity = $this->getUser()->getIdentity();
0 ignored issues
show
Bug introduced by
It seems like getUser() 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

52
        $identity = $this->/** @scrutinizer ignore-call */ getUser()->getIdentity();
Loading history...
53
        $this->assertNoRecoveryTokenOfType('sms', $identity);
54
        if ($secondFactorId) {
55
            $this->assertSecondFactorInPossession($secondFactorId, $identity);
56
        }
57
        $command = new SendRecoveryTokenSmsChallengeCommand();
58
        $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

58
        $form = $this->/** @scrutinizer ignore-call */ createForm(SendSmsChallengeType::class, $command)->handleRequest($request);
Loading history...
59
        $otpRequestsRemaining = $this->smsService
60
            ->getOtpRequestsRemainingCount(SmsSecondFactorServiceInterface::REGISTRATION_SECOND_FACTOR_ID);
61
        $maximumOtpRequests = $this->smsService->getMaximumOtpRequestsCount();
62
63
        $viewVariables = [
64
            'otpRequestsRemaining' => $otpRequestsRemaining,
65
            'maximumOtpRequests' => $maximumOtpRequests
66
        ];
67
68
        if (isset($secondFactorId)) {
69
            $viewVariables['secondFactorId'] = $secondFactorId;
70
        }
71
72
        if ($form->isSubmitted() && $form->isValid()) {
73
            $command->identity = $identity->id;
74
            $command->institution = $identity->institution;
75
76
            if ($otpRequestsRemaining === 0) {
77
                $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

77
                $this->/** @scrutinizer ignore-call */ 
78
                       addFlash('error', 'ss.prove_phone_possession.challenge_request_limit_reached');
Loading history...
78
                $parameters = ['form' => $form->createView(), ...$viewVariables];
79
                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

79
                return $this->/** @scrutinizer ignore-call */ render($templateName, $parameters);
Loading history...
80
            }
81
82
            if ($this->smsService->sendChallenge($command)) {
83
                $urlParameter = [];
84
                if (isset($secondFactorId)) {
85
                    $urlParameter = ['secondFactorId' => $secondFactorId];
86
                }
87
                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

87
                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

87
                return $this->redirect($this->/** @scrutinizer ignore-call */ generateUrl($exitRoute, $urlParameter));
Loading history...
88
            }
89
            $this->addFlash('error', 'ss.form.recovery_token.error.challenge_not_sent_error_message');
90
        }
91
        return $this->render(
92
            $templateName,
93
            ['form' => $form->createView(), ...$viewVariables]
94
        );
95
    }
96
97
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $request should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $templateName should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $exitRoute should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $secondFactorId should have a doc-comment as per coding-style.
Loading history...
98
     * Proof of possession of phone form handler
99
     *
100
     * Note fourth param: '$secondFactorId' is optional parameter, only used in the vetting flow scenario
101
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
102
    private function handleSmsProofOfPossession(
0 ignored issues
show
Coding Style introduced by
Private method name "RecoveryTokenControllerTrait::handleSmsProofOfPossession" must be prefixed with an underscore
Loading history...
103
        Request $request,
104
        string $templateName,
105
        string $exitRoute,
106
        ?string $secondFactorId = null
107
    ): Response {
108
        if (!$this->smsService->hasSmsVerificationState(SmsRecoveryTokenService::REGISTRATION_RECOVERY_TOKEN_ID)) {
109
            $this->addFlash('notice', 'ss.registration.sms.alert.no_verification_state');
110
            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

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