Issues (95)

Branch: main

features/bootstrap/ServiceProviderContext.php (4 issues)

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
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
    /**
90
     * @BeforeScenario
91
     */
92
    public function gatherContexts(BeforeScenarioScope $scope): void
93
    {
94
        $environment = $scope->getEnvironment();
95
        $this->minkContext = $environment->getContext(MinkContext::class);
0 ignored issues
show
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

95
        /** @scrutinizer ignore-call */ 
96
        $this->minkContext = $environment->getContext(MinkContext::class);
Loading history...
96
    }
97
98
    /**
99
     * @Given /^an SFO enabled SP with EntityID ([^\']*)$/
100
     */
101
    public function anSFOEnabledSPWithEntityID($entityId): void
102
    {
103
        $this->registerSp($entityId, true);
104
    }
105
106
    /**
107
     * @Given /^an SP with EntityID ([^\']*)$/
108
     */
109
    public function anSPWithEntityID($entityId): void
110
    {
111
        $this->registerSp($entityId, false);
112
    }
113
    /**
114
     * @Given /^an IdP with EntityID ([^\']*)$/
115
     */
116
    public function anIdPWithEntityID($entityId): void
117
    {
118
        $this->registerIdp($entityId, false);
0 ignored issues
show
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

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

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