iStartAnSFOAuthenticationWithLoa()   B
last analyzed

Complexity

Conditions 9
Paths 22

Size

Total Lines 66
Code Lines 49

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 9
eloc 49
nc 22
nop 5
dl 0
loc 66
rs 7.5571
c 3
b 0
f 0

How to fix   Long Method   

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
2
3
/**
4
 * Copyright 2020 SURFnet B.V.
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\Behat;
20
21
use Behat\Behat\Context\Context;
22
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
23
use Behat\Mink\Driver\Selenium2Driver;
0 ignored issues
show
Bug introduced by
The type Behat\Mink\Driver\Selenium2Driver was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
24
use FriendsOfBehat\SymfonyExtension\Driver\SymfonyDriver;
25
use RobRichards\XMLSecLibs\XMLSecurityKey;
26
use RuntimeException;
27
use SAML2\AuthnRequest;
28
use SAML2\Certificate\Key;
29
use SAML2\Certificate\KeyLoader;
30
use SAML2\Certificate\PrivateKeyLoader;
31
use SAML2\Configuration\PrivateKey;
32
use SAML2\Constants;
33
use SAML2\DOMDocumentFactory;
34
use SAML2\XML\Chunk;
35
use SAML2\XML\saml\Issuer;
36
use SAML2\XML\saml\NameID;
37
use Surfnet\SamlBundle\Entity\IdentityProvider;
38
use Surfnet\SamlBundle\SAML2\AuthnRequest as Saml2AuthnRequest;
39
use Surfnet\StepupGateway\Behat\Repository\SamlEntityRepository;
40
use Surfnet\StepupGateway\Behat\Service\FixtureService;
41
use Symfony\Component\HttpFoundation\Request;
42
use Symfony\Component\HttpFoundation\RequestStack;
43
use Symfony\Component\HttpKernel\KernelInterface;
44
45
class ServiceProviderContext implements Context
46
{
47
    const SSP_URL = 'https://ssp.dev.openconext.local/simplesaml/sp.php';
48
    const SSO_ENDPOINT_URL = 'https://gateway.dev.openconext.local/authentication/single-sign-on';
49
    const SFO_ENDPOINT_URL = 'https://gateway.dev.openconext.local/second-factor-only/single-sign-on';
50
51
    /**
52
     * @var array
53
     */
54
    private $currentSp;
55
56
    /**
57
     * @var array
58
     */
59
    private $currentSfoSp;
60
61
    /**
62
     * @var array
63
     */
64
    private $currentIdP;
65
66
    /**
67
     * @var FixtureService
68
     */
69
    private $fixtureService;
70
71
    /**
72
     * @var KernelInterface
73
     */
74
    private $kernel;
75
76
    /**
77
     * @var MinkContext
78
     */
79
    private $minkContext;
80
81
    public function __construct(
82
        FixtureService $fixtureService,
83
        KernelInterface $kernel
84
    ) {
85
        $this->fixtureService = $fixtureService;
86
        $this->kernel = $kernel;
87
    }
88
89
    #[\Behat\Hook\BeforeScenario]
90
    public function gatherContexts(BeforeScenarioScope $scope): void
91
    {
92
        $environment = $scope->getEnvironment();
93
        $this->minkContext = $environment->getContext(MinkContext::class);
0 ignored issues
show
Bug introduced by
The method getContext() does not exist on Behat\Testwork\Environment\Environment. It seems like you code against a sub-type of Behat\Testwork\Environment\Environment such as Behat\Behat\Context\Envi...lizedContextEnvironment or Behat\Behat\Context\Envi...lizedContextEnvironment or FriendsOfBehat\SymfonyEx...onyExtensionEnvironment. ( Ignorable by Annotation )

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

93
        /** @scrutinizer ignore-call */ 
94
        $this->minkContext = $environment->getContext(MinkContext::class);
Loading history...
94
    }
95
96
    #[\Behat\Step\Given('/^an SFO enabled SP with EntityID ([^\\\']*)$/')]
97
    public function anSFOEnabledSPWithEntityID($entityId): void
98
    {
99
        $this->registerSp($entityId, true);
100
    }
101
102
    #[\Behat\Step\Given('/^an SP with EntityID ([^\\\']*)$/')]
103
    public function anSPWithEntityID($entityId): void
104
    {
105
        $this->registerSp($entityId, false);
106
    }
107
    #[\Behat\Step\Given('/^an IdP with EntityID ([^\\\']*)$/')]
108
    public function anIdPWithEntityID($entityId): void
109
    {
110
        $this->registerIdp($entityId, false);
0 ignored issues
show
Unused Code introduced by
The call to Surfnet\StepupGateway\Be...rContext::registerIdP() has too many arguments starting with false. ( Ignorable by Annotation )

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

110
        $this->/** @scrutinizer ignore-call */ 
111
               registerIdp($entityId, false);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
111
    }
112
113
    private function registerSp($entityId, $sfoEnabled): void
114
    {
115
        $publicKeyLoader = new KeyLoader();
116
        $publicKeyLoader->loadCertificateFile('/config/ssp/sp.crt');
117
        $keys = $publicKeyLoader->getKeys();
118
        /** @var Key $cert */
119
        $cert = $keys->first();
120
121
        $spEntity = $this->fixtureService->registerSP($entityId, $cert['X509Certificate'], $sfoEnabled);
122
123
        $spEntity['configuration'] = json_decode($spEntity['configuration'], true);
124
        if ($sfoEnabled) {
125
            $this->currentSfoSp = $spEntity;
126
        } else {
127
            $this->currentSp = $spEntity;
128
        }
129
    }
130
131
    private function registerIdP($entityId): void
132
    {
133
        $publicKeyLoader = new KeyLoader();
134
        $publicKeyLoader->loadCertificateFile('/config/ssp/idp.crt');
135
        $keys = $publicKeyLoader->getKeys();
136
        /** @var Key $cert */
137
        $cert = $keys->first();
138
139
        $idpEntity = $this->fixtureService->registerIdP($entityId, $cert['X509Certificate']);
140
141
        $idpEntity['configuration'] = json_decode($idpEntity['configuration'], true);
142
        $this->currentIdP = $idpEntity;
143
    }
144
145
    #[\Behat\Step\When('/^([^\\\']*) starts an SFO authentication$/')]
146
    public function iStartAnSFOAuthentication($nameId): void
147
    {
148
        $this->iStartAnSFOAuthenticationWithLoa($nameId, "self-asserted");
149
    }
150
151
    #[\Behat\Step\When('/^([^\\\']*) starts an SFO authentication with LoA ([^\\\']*)$/')]
152
    public function iStartAnSFOAuthenticationWithLoa($nameId, string $loa, bool $forceAuthN = false, ?string $gsspFallbackSubject = null, ?string $gsspFallbackInstitution = null): void
153
    {
154
        $authnRequest = new AuthnRequest();
155
        // In order to later assert if the response succeeded or failed, set our own dummy ACS location
156
        $authnRequest->setAssertionConsumerServiceURL(SamlEntityRepository::SP_ACS_LOCATION);
157
        $issuerVo = new Issuer();
158
        $issuerVo->setValue($this->currentSfoSp['entityId']);
159
        $authnRequest->setIssuer($issuerVo);
160
        $authnRequest->setDestination(self::SFO_ENDPOINT_URL);
161
        $authnRequest->setProtocolBinding(Constants::BINDING_HTTP_REDIRECT);
162
        $authnRequest->setNameId($this->buildNameId($nameId));
163
        if ($forceAuthN) {
164
            $authnRequest->setForceAuthn(true);
165
        }
166
        // Sign with random key, does not mather for now.
167
        $authnRequest->setSignatureKey(
168
            $this->loadPrivateKey(new PrivateKey('/config/ssp/sp.key', 'default'))
169
        );
170
        switch ($loa) {
171
            case "1":
172
            case "1.5":
173
            case "2":
174
            case "3":
175
                $authnRequest->setRequestedAuthnContext(
176
                    ['AuthnContextClassRef' => ['http://dev.openconext.local/assurance/sfo-level' . $loa]]
177
                );
178
            break;
179
            case "self-asserted":
180
                $authnRequest->setRequestedAuthnContext(
181
                    ['AuthnContextClassRef' => ['http://dev.openconext.local/assurance/sfo-level1.5']]
182
                );
183
            break;
184
            default:
185
                throw new RuntimeException(sprintf('The specified LoA-%s is not supported', $loa));
186
        }
187
188
        if ($gsspFallbackSubject != null) {
189
            $dom = DOMDocumentFactory::create();
190
            $ce = $dom->createElementNS('urn:mace:surf.nl:stepup:gssp-extensions', 'gssp:UserAttributes');
191
            $ce->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance');
192
            $ce->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:xs', 'http://www.w3.org/2001/XMLSchema');
193
194
            foreach ([
195
                'urn:mace:dir:attribute-def:mail' => $gsspFallbackSubject,
196
                'urn:mace:terena.org:attribute-def:schacHomeOrganization' => $gsspFallbackInstitution,
197
198
            ] as $name => $value) {
199
                $attrib = $ce->ownerDocument->createElementNS('urn:oasis:names:tc:SAML:2.0:assertion', 'saml:Attribute');
0 ignored issues
show
Bug introduced by
The method createElementNS() 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

199
                /** @scrutinizer ignore-call */ 
200
                $attrib = $ce->ownerDocument->createElementNS('urn:oasis:names:tc:SAML:2.0:assertion', 'saml:Attribute');

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...
200
                $attrib->setAttribute('NameFormat', 'urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified');
201
                $attrib->setAttribute('Name', $name);
202
                $attribValue = $ce->ownerDocument->createElementNS('urn:oasis:names:tc:SAML:2.0:assertion', 'saml:AttributeValue', $value);
203
                $attribValue->setAttribute('xsi:type', 'xs:string');
204
                $attrib->appendChild($attribValue);
205
206
                $ce->appendChild($attrib);
207
            }
208
209
            $ext = $authnRequest->getExtensions();
210
            $ext['saml:Extensions'] = new Chunk($ce);
211
            $authnRequest->setExtensions($ext);
212
        }
213
        $request = Saml2AuthnRequest::createNew($authnRequest);
214
        $query = $request->buildRequestQuery();
215
216
        $this->getSession()->visit($request->getDestination().'?'.$query);
217
    }
218
219
    #[\Behat\Step\When('/^([^\\\']*) starts an SFO authentication requiring LoA ([^\\\']*)$/')]
220
    public function iStartAnSFOAuthenticationWithLoaRequirement($nameId, $loa): void
221
    {
222
        $this->iStartAnSFOAuthenticationWithLoa($nameId, $loa);
223
    }
224
    #[\Behat\Step\When('/^([^\\\']*) starts a forced SFO authentication requiring LoA ([^\\\']*)$/')]
225
    public function iStartAForcedSFOAuthenticationWithLoaRequirement($nameId, $loa): void
226
    {
227
        $this->iStartAnSFOAuthenticationWithLoa($nameId, $loa, true);
228
    }
229
230
    #[\Behat\Step\When('/^([^\\\']*) starts an SFO authentication with GSSP fallback requiring LoA ([^\\\']*) and Gssp extension subject ([^\\\']*) and institution ([^\\\']*)$/')]
231
    public function iStartAForcedSFOAuthenticationWithLoaRequirementAndGsspExtension($nameId, $loa, $subject, $institution): void
232
    {
233
        $this->iStartAnSFOAuthenticationWithLoa($nameId, $loa, false, $subject, $institution);
234
    }
235
236
    #[\Behat\Step\When('/^([^\\\']*) starts an ADFS authentication requiring ([^\\\']*)$/')]
237
    public function iStartAnADFSAuthenticationWithLoaRequirement($nameId, $loa): void
238
    {
239
        $requestParams = [
240
            'loa' => $loa,
241
            'nameId' => $nameId,
242
            'entityId' => $this->currentSfoSp['entityId']
243
        ];
244
        $this->getSession()->visit(SamlEntityRepository::SP_ADFS_SSO_LOCATION . '?' . http_build_query($requestParams));
245
        $this->pressButtonWhenNoJavascriptSupport();
246
    }
247
248
    #[\Behat\Step\When('/^([^\\\']*) starts an authentication at Default SP$/')]
249
    public function iStartAnAuthenticationAtDefaultSP($nameId): void
250
    {
251
        $authnRequest = new AuthnRequest();
252
        // In order to later assert if the response succeeded or failed, set our own dummy ACS location
253
        $authnRequest->setAssertionConsumerServiceURL(SamlEntityRepository::SP_ACS_LOCATION);
254
        $issuerVo = new Issuer();
255
        $issuerVo->setValue('https://ssp.dev.openconext.local/module.php/saml/sp/metadata.php/default-sp');
256
        $authnRequest->setIssuer($issuerVo);
257
        $authnRequest->setDestination(self::SSO_ENDPOINT_URL);
258
        $authnRequest->setProtocolBinding(Constants::BINDING_HTTP_REDIRECT);
259
        $authnRequest->setNameId($this->buildNameId($nameId));
260
        // Sign with random key, does not mather for now.
261
        $authnRequest->setSignatureKey(
262
            $this->loadPrivateKey(new PrivateKey('/config/ssp/sp.key', 'default'))
263
        );
264
        $authnRequest->setRequestedAuthnContext(
265
            ['AuthnContextClassRef' => ['http://dev.openconext.local/assurance/loa2']]
266
        );
267
        $request = Saml2AuthnRequest::createNew($authnRequest);
268
        $query = $request->buildRequestQuery();
269
        $this->getSession()->visit($authnRequest->getDestination().'?'.$query);
270
    }
271
272
    #[\Behat\Step\When('/^([^\\\']*) starts an authentication requiring LoA ([^\\\']*)$/')]
273
    public function iStartAnSsoAuthenticationWithLoaRequirement($nameId, $loa): void
274
    {
275
        $authnRequest = new AuthnRequest();
276
        // In order to later assert if the response succeeded or failed, set our own dummy ACS location
277
        $authnRequest->setAssertionConsumerServiceURL(SamlEntityRepository::SP_ACS_LOCATION);
278
        $issuerVo = new Issuer();
279
        $issuerVo->setValue($this->currentSp['entityId']);
280
        $authnRequest->setIssuer($issuerVo);
281
        $authnRequest->setDestination(self::SSO_ENDPOINT_URL);
282
        $authnRequest->setProtocolBinding(Constants::BINDING_HTTP_REDIRECT);
283
        $authnRequest->setNameId($this->buildNameId($nameId));
284
        // Sign with random key, does not mather for now.
285
        $authnRequest->setSignatureKey(
286
            $this->loadPrivateKey(new PrivateKey('/config/ssp/sp.key', 'default'))
287
        );
288
289
        switch ($loa) {
290
            case "1":
291
            case "1.5":
292
            case "2":
293
            case "3":
294
                $authnRequest->setRequestedAuthnContext(
295
                    ['AuthnContextClassRef' => ['http://dev.openconext.local/assurance/loa' . $loa]]
296
                );
297
                break;
298
            case "self-asserted":
299
                $authnRequest->setRequestedAuthnContext(
300
                    ['AuthnContextClassRef' => ['http://dev.openconext.local/assurance/loa1.5']]
301
                );
302
                break;
303
            default:
304
                throw new RuntimeException(sprintf('The specified LoA-%s is not supported', $loa));
305
        }
306
307
        $request = Saml2AuthnRequest::createNew($authnRequest);
308
        $query = $request->buildRequestQuery();
309
310
        $this->getSession()->visit($request->getDestination().'?'.$query);
311
    }
312
313
    #[\Behat\Step\When('/^I authenticate at the IdP as ([^\\\']*)$/')]
314
    public function iAuthenticateAtTheIdp($username): void
315
    {
316
        $this->minkContext->fillField('username', $username);
317
        $this->minkContext->fillField('password', $username);
318
        // Submit the form
319
        $this->minkContext->pressButton('Login');
320
321
        if ($this->getSession()->getDriver() instanceof SymfonyDriver) {
322
            // Submit the SAML Response from SimpleSamplPHP IdP
323
            $this->minkContext->pressButton('Yes, continue');
324
        }
325
    }
326
327
    #[\Behat\Step\When('/^I authenticate at AzureMFA as "([^"]*)"$/')]
328
    public function iAuthenticateAtAzureMfaAs($username): void
329
    {
330
        $this->minkContext->assertPageAddress('https://azuremfa.dev.openconext.local/mock/sso');
331
        $attributes = sprintf('[
332
            {
333
                "name":"urn:mace:dir:attribute-def:mail",
334
                "value": [
335
                    "%s"
336
                ]
337
            },
338
            {
339
                "name": "http://schemas.microsoft.com/claims/authnmethodsreferences",
340
                "value": [
341
                    "http://schemas.microsoft.com/claims/multipleauthn"
342
                ]
343
            }
344
          ]
345
        ', $username);
346
        $this->minkContext->fillField('attributes', $attributes);
347
        $this->minkContext->pressButton('success');
348
349
        $this->minkContext->assertPageAddress('https://azuremfa.dev.openconext.local/mock/sso');
350
        $this->minkContext->pressButton('Submit assertion');
351
352
        $this->minkContext->assertPageAddress('https://gateway.dev.openconext.local/test/authentication/consume-assertion');
353
    }
354
355
    #[\Behat\Step\When('/^I cancel the authentication at AzureMFA$/')]
356
    public function iCancelTheAuthenticationAtAzureMfa(): void
357
    {
358
        $this->minkContext->assertPageAddress('https://azuremfa.dev.openconext.local/mock/sso');
359
        $this->minkContext->pressButton('cancel');
360
361
        $this->minkContext->assertPageAddress('https://azuremfa.dev.openconext.local/mock/sso');
362
        $this->minkContext->pressButton('Submit assertion');
363
364
        $this->minkContext->assertPageAddress('https://gateway.dev.openconext.local/test/authentication/consume-assertion');
365
    }
366
367
    private static function loadPrivateKey(PrivateKey $key)
368
    {
369
        $keyLoader = new PrivateKeyLoader();
370
        $privateKey = $keyLoader->loadPrivateKey($key);
371
372
        $key = new XMLSecurityKey(XMLSecurityKey::RSA_SHA256, ['type' => 'private']);
373
        $key->loadKey($privateKey->getKeyAsString());
374
375
        return $key;
376
    }
377
378
    #[\Behat\Step\Given('/^I log out at the IdP$/')]
379
    public function iLogOutAtTheIdP(): void
380
    {
381
        $this->minkContext->visit(self::SSP_URL);
382
        $this->minkContext->pressButton('Logout');
383
    }
384
385
    private function getSession()
386
    {
387
        return $this->minkContext->getSession();
388
    }
389
390
    private function buildNameId(string $nameId): NameID
391
    {
392
        $nameIdVo = new NameID();
393
        $nameIdVo->setValue($nameId);
394
        $nameIdVo->setFormat(Constants::NAMEFORMAT_UNSPECIFIED);
395
        return $nameIdVo;
396
    }
397
398
    private function pressButtonWhenNoJavascriptSupport()
399
    {
400
        if ($this->minkContext->getSession()->getDriver() instanceof SymfonyDriver) {
401
            $this->minkContext->pressButton('Submit');
402
        }
403
    }
404
}
405