Passed
Pull Request — develop (#302)
by Michiel
26:45 queued 21:39
created

CookieService::shouldAddCookie()   B

Complexity

Conditions 9
Paths 64

Size

Total Lines 34
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

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