Passed
Pull Request — develop (#302)
by Michiel
08:47 queued 04:18
created

CookieService::getCookieFingerprint()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
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
    ): Response {
94
        // Check if this specific SP is configured to allow setting of an SSO on 2FA cookie (configured in MW config)
95
        $remoteSp = $this->getRemoteSp($responseContext);
96
        if (!$remoteSp->allowedToSetSsoCookieOn2fa()) {
97
            $this->logger->notice(
98
                sprintf(
99
                    'SP: %s does not allow writing SSO on 2FA cookies',
100
                    $remoteSp->getEntityId()
101
                )
102
            );
103
            return $httpResponse;
104
        }
105
106
        // The second factor id is read from state. It is the SF Id of the token used during authentication
107
        $secondFactorId = $responseContext->getSelectedSecondFactor();
108
109
        // We can only set an SSO on 2FA cookie if a second factor authentication is being handled.
110
        if ($secondFactorId) {
111
            $secondFactor = $this->secondFactorService->findByUuid($secondFactorId);
112
            if (!$secondFactor) {
113
                throw new RuntimeException(sprintf('Second Factor token not found with ID: %s', $secondFactorId));
114
            }
115
            // Test if the institution of the Identity this SF belongs to has SSO on 2FA enabled
116
            $isEnabled = $this->institutionConfigurationService->ssoOn2faEnabled($secondFactor->institution);
117
            $this->logger->notice(
118
                sprintf(
119
                    'SSO on 2FA is %senabled for %s',
120
                    $isEnabled ? '' : 'not ',
121
                    $secondFactor->institution
122
                )
123
            );
124
125
            if ($isEnabled) {
126
                $identityId = $responseContext->getIdentityNameId();
127
                $loa = $secondFactor->getLoaLevel($this->secondFactorTypeService);
128
                $isVerifiedBySsoOn2faCookie = $responseContext->isVerifiedBySsoOn2faCookie();
129
                // Did the user perform a new second factor authentication?
130
                if (!$isVerifiedBySsoOn2faCookie) {
131
                    $cookie = CookieValue::from($identityId, $secondFactor->secondFactorId, $loa);
132
                    $this->store($httpResponse, $cookie);
133
                }
134
            }
135
        }
136
        $responseContext->finalizeAuthentication();
137
        return $httpResponse;
138
    }
139
140
    /**
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 $request should have a doc-comment as per coding-style.
Loading history...
141
     * Allow high cyclomatic complexity in favour of keeping this method readable
142
     * @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...
143
     * @SuppressWarnings(PHPMD.NPathComplexity)
144
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
145
    public function shouldSkip2faAuthentication(
146
        ResponseContext $responseContext,
147
        float $requiredLoa,
148
        string $identityNameId,
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
167
        $ssoCookie = $this->read($request);
168
        // Perform validation on the cookie and its contents
169
        if (!$this->isCookieValid($ssoCookie, $requiredLoa, $identityNameId)) {
170
            return false;
171
        }
172
173
        $secondFactor = $this->secondFactorService->findByUuid($ssoCookie->secondFactorId());
174
        if (!$this->secondFactorService->findByUuid($ssoCookie->secondFactorId())) {
175
            $this->logger->notice(
176
                'The second factor stored in the SSO cookie was revoked or has otherwise became unknown to Gateway',
177
                [
178
                    'secondFactorIdFromCookie' => $ssoCookie->secondFactorId()
179
                ]
180
            );
181
            return false;
182
        }
183
184
        $this->logger->notice('Verified the current 2FA authentication can be given with the SSO on 2FA cookie');
185
        $responseContext->saveSelectedSecondFactor($secondFactor);
0 ignored issues
show
Bug introduced by
It seems like $secondFactor can also be of type null; however, parameter $secondFactor of Surfnet\StepupGateway\Ga...eSelectedSecondFactor() does only seem to accept Surfnet\StepupGateway\Ga...dle\Entity\SecondFactor, 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

185
        $responseContext->saveSelectedSecondFactor(/** @scrutinizer ignore-type */ $secondFactor);
Loading history...
186
        return true;
187
    }
188
189
    public function getCookieFingerprint(Request $request): string
0 ignored issues
show
Coding Style introduced by
Missing doc comment for function getCookieFingerprint()
Loading history...
190
    {
191
        return $this->cookieHelper->fingerprint($request);
192
    }
193
194
    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...
195
    {
196
        $this->cookieHelper->write($response, $cookieValue);
197
    }
198
199
    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...
200
    {
201
        try {
202
            return $this->cookieHelper->read($request);
203
        } catch (CookieNotFoundException $e) {
204
            $this->logger->notice('The SSO on 2FA cookie is not found in the request header');
205
            return new NullCookieValue();
206
        } catch (DecryptionFailedException $e) {
207
            $this->logger->notice('Decryption of the SSO on 2FA cookie failed');
208
            return new NullCookieValue();
209
        } catch (Exception $e) {
210
            $this->logger->notice(
211
                'Decryption failed, see original message in context',
212
                ['original-exception-message' => $e->getMessage()]
213
            );
214
            return new NullCookieValue();
215
        }
216
    }
217
218
    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...
219
    {
220
        $remoteSp = $responseContext->getServiceProvider();
221
        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...
222
            throw new RuntimeException('SP not found in the response context, unable to continue with SSO on 2FA');
223
        }
224
        return $remoteSp;
225
    }
226
227
    private function isCookieValid(CookieValueInterface $ssoCookie, float $requiredLoa, string $identityNameId): bool
0 ignored issues
show
Coding Style introduced by
Missing doc comment for function isCookieValid()
Loading history...
Coding Style introduced by
Private method name "CookieService::isCookieValid" must be prefixed with an underscore
Loading history...
228
    {
229
        if ($ssoCookie instanceof NullCookieValue) {
230
            return false;
231
        }
232
        if ($ssoCookie instanceof CookieValue && !$ssoCookie->meetsRequiredLoa($requiredLoa)) {
233
            $this->logger->notice(
234
                sprintf(
235
                    'The required LoA %d did not match the LoA of the SSO cookie %d',
236
                    $requiredLoa,
237
                    $ssoCookie->getLoa()
238
                )
239
            );
240
            return false;
241
        }
242
        if ($ssoCookie instanceof CookieValue && !$ssoCookie->issuedTo($identityNameId)) {
243
            $this->logger->notice(
244
                sprintf(
245
                    'The SSO on 2FA cookie was not issued to %s, but to %s',
246
                    $identityNameId,
247
                    $ssoCookie->getIdentityId()
248
                )
249
            );
250
            return false;
251
        }
252
        try {
253
            $isExpired = $this->expirationHelper->isExpired($ssoCookie);
254
            if ($isExpired) {
255
                $this->logger->notice(
256
                    'The SSO on 2FA cookie has expired. Meaning [authentication time] + [cookie lifetime] is in the past'
257
                );
258
                return false;
259
            }
260
        } catch (InvalidAuthenticationTimeException $e) {
261
            $this->logger->notice('The SSO on 2FA cookie contained an invalid authentication time', [$e->getMessage()]);
262
            return false;
263
        }
264
        return true;
265
    }
266
}
267