Passed
Pull Request — develop (#295)
by Peter
04:30
created

CookieService::shouldSkip2faAuthentication()   C

Complexity

Conditions 13
Paths 12

Size

Total Lines 79
Code Lines 48

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 13
eloc 48
nc 12
nop 5
dl 0
loc 79
rs 6.6166
c 0
b 0
f 0

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 declare(strict_types=1);
2
3
/**
4
 * Copyright 2022 SURFnet bv
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
 */
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...
18
19
namespace Surfnet\StepupGateway\GatewayBundle\Sso2fa;
20
21
use Doctrine\Common\Collections\Collection;
22
use Exception;
23
use Psr\Log\LoggerInterface;
24
use Surfnet\StepupBundle\Service\SecondFactorTypeService;
25
use Surfnet\StepupGateway\GatewayBundle\Entity\SecondFactor;
26
use Surfnet\StepupGateway\GatewayBundle\Entity\ServiceProvider;
27
use Surfnet\StepupGateway\GatewayBundle\Exception\RuntimeException;
28
use Surfnet\StepupGateway\GatewayBundle\Saml\ResponseContext;
29
use Surfnet\StepupGateway\GatewayBundle\Service\InstitutionConfigurationService;
30
use Surfnet\StepupGateway\GatewayBundle\Service\SecondFactorService;
31
use Surfnet\StepupGateway\GatewayBundle\Sso2fa\DateTime\ExpirationHelperInterface;
32
use Surfnet\StepupGateway\GatewayBundle\Sso2fa\Exception\CookieNotFoundException;
33
use Surfnet\StepupGateway\GatewayBundle\Sso2fa\Exception\DecryptionFailedException;
34
use Surfnet\StepupGateway\GatewayBundle\Sso2fa\Exception\InvalidAuthenticationTimeException;
35
use Surfnet\StepupGateway\GatewayBundle\Sso2fa\Http\CookieHelperInterface;
36
use Surfnet\StepupGateway\GatewayBundle\Sso2fa\ValueObject\CookieValue;
37
use Surfnet\StepupGateway\GatewayBundle\Sso2fa\ValueObject\CookieValueInterface;
38
use Surfnet\StepupGateway\GatewayBundle\Sso2fa\ValueObject\NullCookieValue;
39
use Symfony\Component\HttpFoundation\Request;
40
use Symfony\Component\HttpFoundation\Response;
41
42
/**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
43
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects) | Coupling is high as we are integrating logic into the infrastructure
44
 */
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...
45
class CookieService implements CookieServiceInterface
46
{
47
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
48
     * @var CookieHelperInterface
49
     */
50
    private $cookieHelper;
0 ignored issues
show
Coding Style introduced by
Private member variable "cookieHelper" must be prefixed with an underscore
Loading history...
51
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
52
     * @var InstitutionConfigurationService
53
     */
54
    private $institutionConfigurationService;
0 ignored issues
show
Coding Style introduced by
Private member variable "institutionConfigurationService" must be prefixed with an underscore
Loading history...
55
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
56
     * @var LoggerInterface
57
     */
58
    private $logger;
0 ignored issues
show
Coding Style introduced by
Private member variable "logger" must be prefixed with an underscore
Loading history...
59
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
60
     * @var SecondFactorService
61
     */
62
    private $secondFactorService;
0 ignored issues
show
Coding Style introduced by
Private member variable "secondFactorService" must be prefixed with an underscore
Loading history...
63
64
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
65
     * @var SecondFactorTypeService
66
     */
67
    private $secondFactorTypeService;
0 ignored issues
show
Coding Style introduced by
Private member variable "secondFactorTypeService" must be prefixed with an underscore
Loading history...
68
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
69
     * @var ExpirationHelperInterface
70
     */
71
    private $expirationHelper;
0 ignored issues
show
Coding Style introduced by
Private member variable "expirationHelper" must be prefixed with an underscore
Loading history...
72
73
    public function __construct(
0 ignored issues
show
Coding Style introduced by
Missing doc comment for function __construct()
Loading history...
74
        CookieHelperInterface $cookieHelper,
75
        InstitutionConfigurationService $institutionConfigurationService,
76
        SecondFactorService $secondFactorService,
77
        SecondFactorTypeService $secondFactorTypeService,
78
        ExpirationHelperInterface $expirationHelper,
79
        LoggerInterface $logger
80
    ) {
81
        $this->cookieHelper = $cookieHelper;
82
        $this->institutionConfigurationService = $institutionConfigurationService;
83
        $this->secondFactorService = $secondFactorService;
84
        $this->secondFactorTypeService = $secondFactorTypeService;
85
        $this->expirationHelper = $expirationHelper;
86
        $this->logger = $logger;
87
    }
88
89
    public function handleSsoOn2faCookieStorage(
0 ignored issues
show
Coding Style introduced by
Missing doc comment for function handleSsoOn2faCookieStorage()
Loading history...
90
        ResponseContext $responseContext,
91
        Request $request,
92
        Response $httpResponse,
93
        string $authenticationMode = 'sso'
94
    ): Response {
95
        // Check if this specific SP is configured to allow setting of a SSO on 2FA cookie (configured in MW config)
96
        $remoteSp = $this->getRemoteSp($responseContext);
97
        if (!$remoteSp->allowedToSetSsoCookieOn2fa()) {
98
            $this->logger->notice(
99
                sprintf(
100
                    'Ignoring SSO on 2FA for SP: %s',
101
                    $remoteSp->getEntityId()
102
                )
103
            );
104
            return $httpResponse;
105
        }
106
        $secondFactorId = $responseContext->getSelectedSecondFactor();
107
108
        // We can only set an SSO on 2FA cookie if a second factor authentication is being handled.
109
        if ($secondFactorId) {
110
            $secondFactor = $this->secondFactorService->findByUuid($secondFactorId);
111
            if (!$secondFactor) {
112
                throw new RuntimeException(sprintf('Second Factor token not found with ID: %s', $secondFactorId));
113
            }
114
            // Test if the institution of the Identity this SF belongs to has SSO on 2FA enabled
115
            $isEnabled = $this->institutionConfigurationService->ssoOn2faEnabled($secondFactor->institution);
116
            $this->logger->notice(
117
                sprintf(
118
                    'SSO on 2FA is %senabled for %s',
119
                    $isEnabled ? '' : 'not ',
120
                    $secondFactor->institution
121
                )
122
            );
123
            if ($isEnabled) {
124
                $this->logger->notice(sprintf('SSO on 2FA is enabled for %s', $secondFactor->institution));
125
                $ssoCookie = $this->read($request);
126
                $loa = $secondFactor->getLoaLevel($this->secondFactorTypeService);
127
                // Did the LoA requirement change? If a higher LoA was requested, update the cookie value accordingly.
128
                if ($this->shouldAddCookie($ssoCookie, $loa)) {
129
                    $identityId = $responseContext->getIdentityNameId();
130
                    $cookie = CookieValue::from($identityId, $secondFactor->secondFactorId, $loa);
131
                    $this->store($httpResponse, $cookie);
132
                }
133
            }
134
        }
135
        $responseContext->finalizeAuthentication();
136
        return $httpResponse;
137
    }
138
139
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $responseContext should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $requiredLoa should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $identityNameId should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $secondFactorCollection should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $request should have a doc-comment as per coding-style.
Loading history...
140
     * Allow high cyclomatic complexity in favour of keeping this method readable
141
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
0 ignored issues
show
Coding Style introduced by
There must be exactly one blank line before the tags in a doc comment
Loading history...
142
     * @SuppressWarnings(PHPMD.NPathComplexity)
143
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
144
    public function shouldSkip2faAuthentication(
145
        ResponseContext $responseContext,
146
        float $requiredLoa,
147
        string $identityNameId,
148
        Collection $secondFactorCollection,
149
        Request $request
150
    ): bool {
151
        if ($responseContext->isForceAuthn()) {
152
            $this->logger->notice('Ignoring SSO on 2FA cookie when ForceAuthN is specified.');
153
            return false;
154
        }
155
        $remoteSp = $this->getRemoteSp($responseContext);
156
        // Test if the SP allows SSO on 2FA to take place (configured in MW config)
157
        if (!$remoteSp->allowSsoOn2fa()) {
158
            $this->logger->notice(
159
                sprintf(
160
                    'Ignoring SSO on 2FA for SP: %s',
161
                    $remoteSp->getEntityId()
162
                )
163
            );
164
            return false;
165
        }
166
        $ssoCookie = $this->read($request);
167
        if ($ssoCookie instanceof NullCookieValue) {
168
            return false;
169
        }
170
        if ($ssoCookie instanceof CookieValue && !$ssoCookie->meetsRequiredLoa($requiredLoa)) {
171
            $this->logger->notice(
172
                sprintf(
173
                    'The required LoA %d did not match the LoA of the SSO cookie %d',
174
                    $requiredLoa,
175
                    $ssoCookie->getLoa()
176
                )
177
            );
178
            return false;
179
        }
180
        if ($ssoCookie instanceof CookieValue && !$ssoCookie->issuedTo($identityNameId)) {
181
            $this->logger->notice(
182
                sprintf(
183
                    'The SSO on 2FA cookie was not issued to %s, but to %s',
184
                    $identityNameId,
185
                    $ssoCookie->getIdentityId()
186
                )
187
            );
188
            return false;
189
        }
190
        try {
191
            $isExpired = $this->expirationHelper->isExpired($ssoCookie);
192
            if ($isExpired) {
193
                $this->logger->notice(
194
                    'The SSO on 2FA cookie has expired. Meaning [authentication time] + [cookie lifetime] is in the past'
195
                );
196
                return false;
197
            }
198
        } catch (InvalidAuthenticationTimeException $e) {
199
            $this->logger->notice('The SSO on 2FA cookie contained an invalid authentication time', [$e->getMessage()]);
200
            return false;
201
        }
202
203
        if (!$this->secondFactorService->findByUuid($ssoCookie->secondFactorId())) {
204
            $this->logger->notice(
205
                'The second factor stored in the SSO cookie was revoked or has otherwise became unknown to Gateway',
206
                [
207
                    'secondFactorIdFromCookie' => $ssoCookie->secondFactorId()
208
                ]
209
            );
210
            return false;
211
        }
212
213
        /** @var SecondFactor $secondFactor */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
Missing short description in doc comment
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
214
        foreach ($secondFactorCollection as $secondFactor) {
215
            $loa = $secondFactor->getLoaLevel($this->secondFactorTypeService);
216
            if ($loa >= $requiredLoa) {
217
                $this->logger->notice('Verified the current 2FA authentication can be given with the SSO on 2FA cookie');
218
                $responseContext->saveSelectedSecondFactor($secondFactor);
219
                return true;
220
            }
221
        }
222
        return false;
223
    }
224
225
    public function getCookieFingerprint(Request $request): string
0 ignored issues
show
Coding Style introduced by
Missing doc comment for function getCookieFingerprint()
Loading history...
226
    {
227
        return $this->cookieHelper->fingerprint($request);
228
    }
229
230
    private function shouldAddCookie(CookieValueInterface $ssoCookie, float $loa)
0 ignored issues
show
Coding Style introduced by
Missing doc comment for function shouldAddCookie()
Loading history...
Coding Style introduced by
Private method name "CookieService::shouldAddCookie" must be prefixed with an underscore
Loading history...
231
    {
232
        // IF the SSO cookie is not found (we've got a NullCookieValue returned from the cookie helper)
233
        $cookieNotSet = $ssoCookie instanceof NullCookieValue;
234
        // OR the existing cookie does exist, but the LoA stored in that cookie does not match the required LoA
235
        $cookieDoesNotMeetLoaRequirement = ($ssoCookie instanceof CookieValue && !$ssoCookie->meetsRequiredLoa($loa));
236
        if ($cookieDoesNotMeetLoaRequirement) {
237
            $this->logger->notice(
238
                sprintf(
239
                    'Storing new SSO on 2FA cookie as LoA requirement (%d changed to %d) changed',
240
                    $ssoCookie->getLoa(),
241
                    $loa
242
                )
243
            );
244
        }
245
        return $cookieNotSet || $cookieDoesNotMeetLoaRequirement;
246
    }
247
248
    private function store(Response $response, CookieValueInterface $cookieValue)
0 ignored issues
show
Coding Style introduced by
Missing doc comment for function store()
Loading history...
Coding Style introduced by
Private method name "CookieService::store" must be prefixed with an underscore
Loading history...
249
    {
250
        $this->cookieHelper->write($response, $cookieValue);
251
    }
252
253
    private function read(Request $request): CookieValueInterface
0 ignored issues
show
Coding Style introduced by
Missing doc comment for function read()
Loading history...
Coding Style introduced by
Private method name "CookieService::read" must be prefixed with an underscore
Loading history...
254
    {
255
        try {
256
            return $this->cookieHelper->read($request);
257
        } catch (CookieNotFoundException $e) {
258
            $this->logger->notice('Attempt to decrypt the cookie failed, the cookie could not be found');
259
            return new NullCookieValue();
260
        } catch (DecryptionFailedException $e) {
261
            $this->logger->notice('Decryption of the SSO on 2FA cookie failed');
262
            return new NullCookieValue();
263
        } catch (Exception $e) {
264
            $this->logger->notice(
265
                'Decryption failed, see original message in context',
266
                ['original-exception-message' => $e->getMessage()]
267
            );
268
            return new NullCookieValue();
269
        }
270
    }
271
272
    private function getRemoteSp(ResponseContext $responseContext): ServiceProvider
0 ignored issues
show
Coding Style introduced by
Missing doc comment for function getRemoteSp()
Loading history...
Coding Style introduced by
Private method name "CookieService::getRemoteSp" must be prefixed with an underscore
Loading history...
273
    {
274
        $remoteSp = $responseContext->getServiceProvider();
275
        if (!$remoteSp) {
0 ignored issues
show
introduced by
$remoteSp is of type Surfnet\StepupGateway\Ga...\Entity\ServiceProvider, thus it always evaluated to true.
Loading history...
276
            throw new RuntimeException('SP not found in the response context, unable to continue with SSO on 2FA');
277
        }
278
        return $remoteSp;
279
    }
280
}
281