ResponseContext   A
last analyzed

Complexity

Total Complexity 40

Size/Duplication

Total Lines 330
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 77
c 1
b 0
f 0
dl 0
loc 330
rs 9.2
wmc 40

31 Methods

Rating   Name   Duplication   Size   Complexity  
A getIssueInstant() 0 3 1
A getDestinationForAdfs() 0 5 1
A saveAssertion() 0 17 3
A getExpectedInResponseTo() 0 3 1
A getNormalizedSchacHomeOrganization() 0 7 2
A saveSelectedSecondFactor() 0 6 1
A getServiceProvider() 0 9 2
A reconstituteAssertion() 0 7 1
A getSelectedLocale() 0 3 1
A getIssuer() 0 5 1
A getIdentityProvider() 0 3 1
A getRequiredLoa() 0 3 1
A getDestination() 0 5 1
A __construct() 0 12 2
A getInResponseTo() 0 3 1
A getRelayState() 0 3 1
A getIdentityNameId() 0 3 1
A markSecondFactorVerified() 0 3 1
A getSelectedSecondFactor() 0 3 1
A getRequestServiceProvider() 0 3 1
A isForceAuthn() 0 3 1
A isSecondFactorVerified() 0 3 2
A finalizeAuthentication() 0 14 1
A getAuthenticatingIdp() 0 3 1
A markVerifiedBySsoOn2faCookie() 0 4 1
A isVerifiedBySsoOn2faCookie() 0 3 1
A getSsoOn2faCookieFingerprint() 0 3 1
A resolveNameIdValue() 0 12 4
A getResponseAction() 0 3 1
A getResponseContextServiceId() 0 3 1
A responseSent() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like ResponseContext often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ResponseContext, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * Copyright 2014 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
 */
18
19
namespace Surfnet\StepupGateway\GatewayBundle\Saml;
20
21
use DateTime;
22
use DateTimeZone;
23
use DOMDocument;
24
use Exception;
25
use Psr\Log\LoggerInterface;
26
use SAML2\Assertion;
27
use SAML2\XML\saml\Issuer;
28
use Surfnet\SamlBundle\Entity\IdentityProvider;
29
use Surfnet\StepupGateway\GatewayBundle\Entity\SecondFactor;
30
use Surfnet\StepupGateway\GatewayBundle\Entity\ServiceProvider;
31
use Surfnet\StepupGateway\GatewayBundle\Saml\Exception\RuntimeException;
32
use Surfnet\StepupGateway\GatewayBundle\Saml\Proxy\ProxyStateHandler;
33
use Surfnet\StepupGateway\GatewayBundle\Service\SamlEntityService;
34
use Surfnet\StepupGateway\GatewayBundle\Service\SecondFactor\SecondFactorInterface;
35
use Surfnet\StepupGateway\SecondFactorOnlyBundle\Adfs\Exception\AcsLocationNotAllowedException;
36
use Surfnet\StepupGateway\SecondFactorOnlyBundle\Service\Gateway\SecondfactorGsspFallback;
37
38
/**
39
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
40
 */
41
class ResponseContext
42
{
43
    /**
44
     * @var IdentityProvider
45
     */
46
    private $hostedIdentityProvider;
47
48
    /**
49
     * @var SamlEntityService
50
     */
51
    private $samlEntityService;
52
53
    /**
54
     * @var ProxyStateHandler
55
     */
56
    private $stateHandler;
57
58
    /**
59
     * @var LoggerInterface
60
     */
61
    private $logger;
62
63
    /**
64
     * @var DateTime
65
     */
66
    private $generationTime;
67
68
    /**
69
     * @var ServiceProvider
70
     */
71
    private $targetServiceProvider;
72
73
    /**
74
     * @throws Exception
75
     */
76
    public function __construct(
77
        IdentityProvider $identityProvider,
78
        SamlEntityService $samlEntityService,
79
        ProxyStateHandler $stateHandler,
80
        LoggerInterface $logger,
81
        DateTime $now = null
82
    ) {
83
        $this->hostedIdentityProvider = $identityProvider;
84
        $this->samlEntityService      = $samlEntityService;
85
        $this->stateHandler           = $stateHandler;
86
        $this->logger                 = $logger;
87
        $this->generationTime         = is_null($now) ? new DateTime('now', new DateTimeZone('UTC')): $now;
88
    }
89
90
    /**
91
     * @return string|null
92
     */
93
    public function getDestination(): ?string
94
    {
95
        $requestAcsUrl = $this->stateHandler->getRequestAssertionConsumerServiceUrl();
96
97
        return $this->getServiceProvider()->determineAcsLocation($requestAcsUrl, $this->logger);
98
    }
99
100
    /**
101
     * @return string
102
     * @throws AcsLocationNotAllowedException
103
     */
104
    public function getDestinationForAdfs(): string
105
    {
106
        $requestAcsUrl = $this->stateHandler->getRequestAssertionConsumerServiceUrl();
107
108
        return $this->getServiceProvider()->determineAcsLocationForAdfs($requestAcsUrl);
109
    }
110
111
    public function getIssuer(): Issuer
112
    {
113
        $issuer = new Issuer();
114
        $issuer->setValue($this->hostedIdentityProvider->getEntityId());
0 ignored issues
show
Bug introduced by
It seems like $this->hostedIdentityProvider->getEntityId() can also be of type null; however, parameter $value of SAML2\XML\saml\NameIDType::setValue() does only seem to accept string, 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

114
        $issuer->setValue(/** @scrutinizer ignore-type */ $this->hostedIdentityProvider->getEntityId());
Loading history...
115
        return $issuer;
116
    }
117
118
    /**
119
     * @return int
120
     */
121
    public function getIssueInstant(): int
122
    {
123
        return $this->generationTime->getTimestamp();
124
    }
125
126
    /**
127
     * @return null|string
128
     */
129
    public function getInResponseTo(): ?string
130
    {
131
        return $this->stateHandler->getRequestId();
132
    }
133
134
    /**
135
     * @return null|string
136
     */
137
    public function getExpectedInResponseTo(): ?string
138
    {
139
        return $this->stateHandler->getGatewayRequestId();
140
    }
141
142
    /**
143
     * @return null|string
144
     */
145
    public function getRequiredLoa(): ?string
146
    {
147
        return $this->stateHandler->getRequiredLoaIdentifier();
148
    }
149
150
    /**
151
     * @return IdentityProvider
152
     */
153
    public function getIdentityProvider(): IdentityProvider
154
    {
155
        return $this->hostedIdentityProvider;
156
    }
157
158
    /**
159
     * @return null|ServiceProvider
160
     */
161
    public function getServiceProvider(): ?ServiceProvider
162
    {
163
        if (isset($this->targetServiceProvider)) {
164
            return $this->targetServiceProvider;
165
        }
166
167
        $serviceProviderId = $this->stateHandler->getRequestServiceProvider();
168
169
        return $this->targetServiceProvider = $this->samlEntityService->getServiceProvider($serviceProviderId);
0 ignored issues
show
Bug introduced by
It seems like $serviceProviderId can also be of type null; however, parameter $entityId of Surfnet\StepupGateway\Ga...e::getServiceProvider() does only seem to accept string, 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

169
        return $this->targetServiceProvider = $this->samlEntityService->getServiceProvider(/** @scrutinizer ignore-type */ $serviceProviderId);
Loading history...
170
    }
171
172
    /**
173
     * @return null|string
174
     */
175
    public function getRelayState(): ?string
176
    {
177
        return $this->stateHandler->getRelayState();
178
    }
179
180
    /**
181
     * @param Assertion $assertion
182
     * @throws Exception
183
     */
184
    public function saveAssertion(Assertion $assertion): void
185
    {
186
        $this->stateHandler->saveIdentityNameId($this->resolveNameIdValue($assertion));
187
        // same for the entityId of the authenticating Authority
188
        $authenticatingAuthorities = $assertion->getAuthenticatingAuthority();
189
        if (!empty($authenticatingAuthorities)) {
190
            $this->stateHandler->setAuthenticatingIdp(reset($authenticatingAuthorities));
191
        }
192
193
        // And also attempt to save the user's schacHomeOrganization
194
        $attributes = $assertion->getAttributes();
195
        if (!empty($attributes['urn:mace:terena.org:attribute-def:schacHomeOrganization'])) {
196
            $schacHomeOrganization = $attributes['urn:mace:terena.org:attribute-def:schacHomeOrganization'];
197
            $this->stateHandler->setSchacHomeOrganization(reset($schacHomeOrganization));
198
        }
199
200
        $this->stateHandler->saveAssertion($assertion->toXML()->ownerDocument->saveXML());
0 ignored issues
show
Bug introduced by
The method saveXML() does not exist on null. ( Ignorable by Annotation )

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

200
        $this->stateHandler->saveAssertion($assertion->toXML()->ownerDocument->/** @scrutinizer ignore-call */ saveXML());

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
201
    }
202
203
    /**
204
     * @return Assertion
205
     * @throws Exception
206
     */
207
    public function reconstituteAssertion(): Assertion
208
    {
209
        $assertionAsXML    = $this->stateHandler->getAssertion();
210
        $assertionDocument = new DOMDocument();
211
        $assertionDocument->loadXML($assertionAsXML);
212
213
        return new Assertion($assertionDocument->documentElement);
214
    }
215
216
    /**
217
     * @return null|string
218
     */
219
    public function getIdentityNameId(): string
220
    {
221
        return $this->stateHandler->getIdentityNameId();
222
    }
223
224
    /**
225
     * Return the lower-cased schacHomeOrganization value from the assertion.
226
     *
227
     * Comparisons on SHO values should always be case-insensitive. Stepup
228
     * configuration always contains SHO values lower-cased, so this getter
229
     * can be used to compare the SHO with configured values.
230
     *
231
     * @see StepUpAuthenticationService::resolveHighestRequiredLoa()
232
     *
233
     * @return null|string
234
     */
235
    public function getNormalizedSchacHomeOrganization(): ?string
236
    {
237
        $schacHomeOrganization = $this->stateHandler->getSchacHomeOrganization();
238
        if ($schacHomeOrganization === null) {
239
            return null;
240
        }
241
        return strtolower($schacHomeOrganization);
242
    }
243
244
    /**
245
     * @param SecondFactor $secondFactor
246
     */
247
    public function saveSelectedSecondFactor(SecondFactorInterface $secondFactor): void
248
    {
249
        $this->stateHandler->setSelectedSecondFactorId($secondFactor->getSecondFactorId());
250
        $this->stateHandler->setSecondFactorVerified(false);
251
        $this->stateHandler->setSecondFactorIsFallback($secondFactor instanceof SecondfactorGsspFallback);
252
        $this->stateHandler->setPreferredLocale($secondFactor->getDisplayLocale());
253
    }
254
255
    public function getSelectedLocale(): string
256
    {
257
        return $this->stateHandler->getPreferredLocale();
258
    }
259
260
    /**
261
     * @return null|string
262
     */
263
    public function getSelectedSecondFactor(): ?string
264
    {
265
        return $this->stateHandler->getSelectedSecondFactorId();
266
    }
267
268
    public function markSecondFactorVerified(): void
269
    {
270
        $this->stateHandler->setSecondFactorVerified(true);
271
    }
272
273
    public function finalizeAuthentication(): void
274
    {
275
        // The second factor ID is used right before sending the response to verify if the SSO on
276
        // 2FA cookies Second Factor is still known on the platform That's why it is forgotten at
277
        // this point during authentication.
278
        $this->stateHandler->unsetSelectedSecondFactorId();
279
        // Right before sending the response, we check if we need to update the SSO on 2FA cookie
280
        // One of the triggers for storing a new cookie is if the authentication was performed with
281
        // a real Second Factor token. That's why this value is purged from state at this very late
282
        // point in time.
283
        $this->stateHandler->unsetVerifiedBySsoOn2faCookie();
284
285
        $this->stateHandler->setSecondFactorIsFallback(false);
286
        $this->stateHandler->setGsspUserAttributes('', '');
287
    }
288
289
    /**
290
     * @return bool
291
     */
292
    public function isSecondFactorVerified(): bool
293
    {
294
        return $this->stateHandler->getSelectedSecondFactorId() && $this->stateHandler->isSecondFactorVerified();
295
    }
296
297
    public function getResponseAction(): ?string
298
    {
299
        return $this->stateHandler->getResponseAction();
300
    }
301
302
    /**
303
     * Resets some state after the response is sent
304
     * (e.g. resets which second factor was selected and whether it was verified).
305
     */
306
    public function responseSent(): void
307
    {
308
        $this->stateHandler->setSecondFactorVerified(false);
309
        $this->stateHandler->setSsoOn2faCookieFingerprint('');
310
    }
311
312
    /**
313
     * Retrieve the ResponseContextServiceId from state
314
     *
315
     * Used to determine we are dealing with an SFO or regular authentication. Both have different ResponseContext
316
     * instances, and it's imperative that successive consumers use the correct service.
317
     *
318
     * @return string|null
319
     */
320
    public function getResponseContextServiceId(): ?string
321
    {
322
        return $this->stateHandler->getResponseContextServiceId();
323
    }
324
325
    /**
326
     * Either gets the internal-collabPersonId if present or falls back on the regular name id attribute
327
     * @throws Exception
328
     */
329
    private function resolveNameIdValue(Assertion $assertion): string
330
    {
331
        $attributes = $assertion->getAttributes();
332
        if (array_key_exists('urn:mace:surf.nl:attribute-def:internal-collabPersonId', $attributes)) {
333
            return reset($attributes['urn:mace:surf.nl:attribute-def:internal-collabPersonId']);
334
        }
335
        $nameId = $assertion->getNameId();
336
        if ($nameId && is_string($nameId->getValue())) {
337
            return $nameId->getValue();
338
        }
339
340
        throw new RuntimeException('Unable to resolve an identifier from internalCollabPersonId or the Subject NameId');
341
    }
342
343
    public function isForceAuthn(): bool
344
    {
345
        return $this->stateHandler->isForceAuthn();
346
    }
347
348
    public function markVerifiedBySsoOn2faCookie(string $fingerprint): void
349
    {
350
        $this->stateHandler->setVerifiedBySsoOn2faCookie(true);
351
        $this->stateHandler->setSsoOn2faCookieFingerprint($fingerprint);
352
    }
353
354
    public function isVerifiedBySsoOn2faCookie(): bool
355
    {
356
        return $this->stateHandler->isVerifiedBySsoOn2faCookie();
357
    }
358
    public function getSsoOn2faCookieFingerprint(): bool
359
    {
360
        return $this->stateHandler->getSsoOn2faCookieFingerprint();
361
    }
362
363
    public function getAuthenticatingIdp(): ?string
364
    {
365
        return $this->stateHandler->getAuthenticatingIdp();
366
    }
367
368
    public function getRequestServiceProvider(): ?string
369
    {
370
        return $this->stateHandler->getRequestServiceProvider();
371
    }
372
}
373