Completed
Push — main ( 8d5570...416cf3 )
by
unknown
23s queued 15s
created

pressButtonWhenNoJavascriptSupport()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 2
nc 2
nop 0
dl 0
loc 4
rs 10
c 0
b 0
f 0
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\XML\saml\Issuer;
34
use SAML2\XML\saml\NameID;
35
use Surfnet\SamlBundle\Entity\IdentityProvider;
36
use Surfnet\SamlBundle\SAML2\AuthnRequest as Saml2AuthnRequest;
37
use Surfnet\StepupGateway\Behat\Repository\SamlEntityRepository;
38
use Surfnet\StepupGateway\Behat\Service\FixtureService;
39
use Symfony\Component\HttpFoundation\Request;
40
use Symfony\Component\HttpFoundation\RequestStack;
41
use Symfony\Component\HttpKernel\KernelInterface;
42
43
class ServiceProviderContext implements Context
44
{
45
    const SSP_URL = 'https://ssp.dev.openconext.local/simplesaml/sp.php';
46
    const SSO_ENDPOINT_URL = 'https://gateway.dev.openconext.local/authentication/single-sign-on';
47
    const SFO_ENDPOINT_URL = 'https://gateway.dev.openconext.local/second-factor-only/single-sign-on';
48
49
    /**
50
     * @var array
51
     */
52
    private $currentSp;
53
54
    /**
55
     * @var array
56
     */
57
    private $currentSfoSp;
58
59
    /**
60
     * @var array
61
     */
62
    private $currentIdP;
63
64
    /**
65
     * @var FixtureService
66
     */
67
    private $fixtureService;
68
69
    /**
70
     * @var KernelInterface
71
     */
72
    private $kernel;
73
74
    /**
75
     * @var MinkContext
76
     */
77
    private $minkContext;
78
79
    public function __construct(
80
        FixtureService $fixtureService,
81
        KernelInterface $kernel
82
    ) {
83
        $this->fixtureService = $fixtureService;
84
        $this->kernel = $kernel;
85
    }
86
87
    /**
88
     * @BeforeScenario
89
     */
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
    /**
97
     * @Given /^an SFO enabled SP with EntityID ([^\']*)$/
98
     */
99
    public function anSFOEnabledSPWithEntityID($entityId): void
100
    {
101
        $this->registerSp($entityId, true);
102
    }
103
104
    /**
105
     * @Given /^an SP with EntityID ([^\']*)$/
106
     */
107
    public function anSPWithEntityID($entityId): void
108
    {
109
        $this->registerSp($entityId, false);
110
    }
111
    /**
112
     * @Given /^an IdP with EntityID ([^\']*)$/
113
     */
114
    public function anIdPWithEntityID($entityId): void
115
    {
116
        $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

116
        $this->/** @scrutinizer ignore-call */ 
117
               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...
117
    }
118
119
    private function registerSp($entityId, $sfoEnabled): void
120
    {
121
        $publicKeyLoader = new KeyLoader();
122
        $publicKeyLoader->loadCertificateFile('/config/ssp/sp.crt');
123
        $keys = $publicKeyLoader->getKeys();
124
        /** @var Key $cert */
125
        $cert = $keys->first();
126
127
        $spEntity = $this->fixtureService->registerSP($entityId, $cert['X509Certificate'], $sfoEnabled);
128
129
        $spEntity['configuration'] = json_decode($spEntity['configuration'], true);
130
        if ($sfoEnabled) {
131
            $this->currentSfoSp = $spEntity;
132
        } else {
133
            $this->currentSp = $spEntity;
134
        }
135
    }
136
137
    private function registerIdP($entityId): void
138
    {
139
        $publicKeyLoader = new KeyLoader();
140
        $publicKeyLoader->loadCertificateFile('/config/ssp/idp.crt');
141
        $keys = $publicKeyLoader->getKeys();
142
        /** @var Key $cert */
143
        $cert = $keys->first();
144
145
        $idpEntity = $this->fixtureService->registerIdP($entityId, $cert['X509Certificate']);
146
147
        $idpEntity['configuration'] = json_decode($idpEntity['configuration'], true);
148
        $this->currentIdP = $idpEntity;
149
    }
150
151
    /**
152
     * @When /^([^\']*) starts an SFO authentication$/
153
     */
154
    public function iStartAnSFOAuthentication($nameId): void
155
    {
156
        $this->iStartAnSFOAuthenticationWithLoa($nameId, "self-asserted");
157
    }
158
159
    /**
160
     * @When /^([^\']*) starts an SFO authentication with LoA ([^\']*)$/
161
     */
162
    public function iStartAnSFOAuthenticationWithLoa($nameId, string $loa, bool $forceAuthN = false): void
163
    {
164
        $authnRequest = new AuthnRequest();
165
        // In order to later assert if the response succeeded or failed, set our own dummy ACS location
166
        $authnRequest->setAssertionConsumerServiceURL(SamlEntityRepository::SP_ACS_LOCATION);
167
        $issuerVo = new Issuer();
168
        $issuerVo->setValue($this->currentSfoSp['entityId']);
169
        $authnRequest->setIssuer($issuerVo);
170
        $authnRequest->setDestination(self::SFO_ENDPOINT_URL);
171
        $authnRequest->setProtocolBinding(Constants::BINDING_HTTP_REDIRECT);
172
        $authnRequest->setNameId($this->buildNameId($nameId));
173
        if ($forceAuthN) {
174
            $authnRequest->setForceAuthn(true);
175
        }
176
        // Sign with random key, does not mather for now.
177
        $authnRequest->setSignatureKey(
178
            $this->loadPrivateKey(new PrivateKey('/config/ssp/sp.key', 'default'))
179
        );
180
        switch ($loa) {
181
            case "1":
182
            case "1.5":
183
            case "2":
184
            case "3":
185
                $authnRequest->setRequestedAuthnContext(
186
                    ['AuthnContextClassRef' => ['http://dev.openconext.local/assurance/sfo-level' . $loa]]
187
                );
188
            break;
189
            case "self-asserted":
190
                $authnRequest->setRequestedAuthnContext(
191
                    ['AuthnContextClassRef' => ['http://dev.openconext.local/assurance/sfo-level1.5']]
192
                );
193
            break;
194
            default:
195
                throw new RuntimeException(sprintf('The specified LoA-%s is not supported', $loa));
196
        }
197
        $request = Saml2AuthnRequest::createNew($authnRequest);
198
        $query = $request->buildRequestQuery();
199
200
        $this->getSession()->visit($request->getDestination().'?'.$query);
201
    }
202
203
    /**
204
     * @When /^([^\']*) starts an SFO authentication requiring LoA ([^\']*)$/
205
     */
206
    public function iStartAnSFOAuthenticationWithLoaRequirement($nameId, $loa): void
207
    {
208
        $this->iStartAnSFOAuthenticationWithLoa($nameId, $loa);
209
    }
210
    /**
211
     * @When /^([^\']*) starts a forced SFO authentication requiring LoA ([^\']*)$/
212
     */
213
    public function iStartAForcedSFOAuthenticationWithLoaRequirement($nameId, $loa): void
214
    {
215
        $this->iStartAnSFOAuthenticationWithLoa($nameId, $loa, true);
216
    }
217
218
    /**
219
     * @When /^([^\']*) starts an ADFS authentication requiring ([^\']*)$/
220
     */
221
    public function iStartAnADFSAuthenticationWithLoaRequirement($nameId, $loa): void
222
    {
223
        $requestParams = [
224
            'loa' => $loa,
225
            'nameId' => $nameId,
226
            'entityId' => $this->currentSfoSp['entityId']
227
        ];
228
        $this->getSession()->visit(SamlEntityRepository::SP_ADFS_SSO_LOCATION . '?' . http_build_query($requestParams));
229
        $this->pressButtonWhenNoJavascriptSupport();
230
    }
231
232
    /**
233
     * @When /^([^\']*) starts an authentication at Default SP$/
234
     */
235
    public function iStartAnAuthenticationAtDefaultSP($nameId): void
236
    {
237
        $authnRequest = new AuthnRequest();
238
        // In order to later assert if the response succeeded or failed, set our own dummy ACS location
239
        $authnRequest->setAssertionConsumerServiceURL(SamlEntityRepository::SP_ACS_LOCATION);
240
        $issuerVo = new Issuer();
241
        $issuerVo->setValue('https://ssp.dev.openconext.local/module.php/saml/sp/metadata.php/default-sp');
242
        $authnRequest->setIssuer($issuerVo);
243
        $authnRequest->setDestination(self::SSO_ENDPOINT_URL);
244
        $authnRequest->setProtocolBinding(Constants::BINDING_HTTP_REDIRECT);
245
        $authnRequest->setNameId($this->buildNameId($nameId));
246
        // Sign with random key, does not mather for now.
247
        $authnRequest->setSignatureKey(
248
            $this->loadPrivateKey(new PrivateKey('/config/ssp/sp.key', 'default'))
249
        );
250
        $authnRequest->setRequestedAuthnContext(
251
            ['AuthnContextClassRef' => ['http://dev.openconext.local/assurance/loa2']]
252
        );
253
        $request = Saml2AuthnRequest::createNew($authnRequest);
254
        $query = $request->buildRequestQuery();
255
        $this->getSession()->visit($authnRequest->getDestination().'?'.$query);
256
    }
257
258
    /**
259
     * @When /^([^\']*) starts an authentication requiring LoA ([^\']*)$/
260
     */
261
    public function iStartAnSsoAuthenticationWithLoaRequirement($nameId, $loa): void
262
    {
263
        $authnRequest = new AuthnRequest();
264
        // In order to later assert if the response succeeded or failed, set our own dummy ACS location
265
        $authnRequest->setAssertionConsumerServiceURL(SamlEntityRepository::SP_ACS_LOCATION);
266
        $issuerVo = new Issuer();
267
        $issuerVo->setValue($this->currentSp['entityId']);
268
        $authnRequest->setIssuer($issuerVo);
269
        $authnRequest->setDestination(self::SSO_ENDPOINT_URL);
270
        $authnRequest->setProtocolBinding(Constants::BINDING_HTTP_REDIRECT);
271
        $authnRequest->setNameId($this->buildNameId($nameId));
272
        // Sign with random key, does not mather for now.
273
        $authnRequest->setSignatureKey(
274
            $this->loadPrivateKey(new PrivateKey('/config/ssp/sp.key', 'default'))
275
        );
276
277
        switch ($loa) {
278
            case "1":
279
            case "1.5":
280
            case "2":
281
            case "3":
282
                $authnRequest->setRequestedAuthnContext(
283
                    ['AuthnContextClassRef' => ['http://dev.openconext.local/assurance/loa' . $loa]]
284
                );
285
                break;
286
            case "self-asserted":
287
                $authnRequest->setRequestedAuthnContext(
288
                    ['AuthnContextClassRef' => ['http://dev.openconext.local/assurance/loa1.5']]
289
                );
290
                break;
291
            default:
292
                throw new RuntimeException(sprintf('The specified LoA-%s is not supported', $loa));
293
        }
294
295
        $request = Saml2AuthnRequest::createNew($authnRequest);
296
        $query = $request->buildRequestQuery();
297
298
        $this->getSession()->visit($request->getDestination().'?'.$query);
299
    }
300
301
    /**
302
     * @When /^I authenticate at the IdP as ([^\']*)$/
303
     */
304
    public function iAuthenticateAtTheIdp($username): void
305
    {
306
        $this->minkContext->fillField('username', $username);
307
        $this->minkContext->fillField('password', $username);
308
        // Submit the form
309
        $this->minkContext->pressButton('Login');
310
311
        if ($this->getSession()->getDriver() instanceof SymfonyDriver) {
312
            // Submit the SAML Response from SimpleSamplPHP IdP
313
            $this->minkContext->pressButton('Yes, continue');
314
        }
315
    }
316
317
    /**
318
     * @return IdentityProvider
319
     */
320
    public function getIdentityProvider()
321
    {
322
        /** @var RequestStack $stack */
323
324
        $stack = $this->kernel->getContainer()->get('request_stack');
325
        $stack->push(Request::create('https://gateway.dev.openconext.local'));
326
        $ip = $this->kernel->getContainer()->get('surfnet_saml.hosted.identity_provider');
327
        $stack->pop();
328
329
        return $ip;
330
    }
331
332
    private static function loadPrivateKey(PrivateKey $key)
333
    {
334
        $keyLoader = new PrivateKeyLoader();
335
        $privateKey = $keyLoader->loadPrivateKey($key);
336
337
        $key = new XMLSecurityKey(XMLSecurityKey::RSA_SHA256, ['type' => 'private']);
338
        $key->loadKey($privateKey->getKeyAsString());
339
340
        return $key;
341
    }
342
343
    /**
344
     * @Given /^I log out at the IdP$/
345
     */
346
    public function iLogOutAtTheIdP(): void
347
    {
348
        $this->minkContext->visit(self::SSP_URL);
349
        $this->minkContext->pressButton('Logout');
350
    }
351
352
    private function getSession()
353
    {
354
        return $this->minkContext->getSession();
355
    }
356
357
    private function buildNameId(string $nameId): NameID
358
    {
359
        $nameIdVo = new NameID();
360
        $nameIdVo->setValue($nameId);
361
        $nameIdVo->setFormat(Constants::NAMEFORMAT_UNSPECIFIED);
362
        return $nameIdVo;
363
    }
364
365
    private function pressButtonWhenNoJavascriptSupport()
366
    {
367
        if ($this->minkContext->getSession()->getDriver() instanceof SymfonyDriver) {
368
            $this->minkContext->pressButton('Submit');
369
        }
370
    }
371
}
372