Passed
Push — develop ( d2d0cb...6c46b1 )
by Pieter van der
02:42
created

iStartAnSsoAuthenticationWithLoaRequirement()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 37
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 6
eloc 27
c 2
b 0
f 0
nc 6
nop 2
dl 0
loc 37
rs 8.8657
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;
24
use RuntimeException;
25
use RobRichards\XMLSecLibs\XMLSecurityKey;
26
use SAML2\AuthnRequest;
27
use SAML2\Certificate\Key;
28
use SAML2\Certificate\KeyLoader;
29
use SAML2\Certificate\PrivateKeyLoader;
30
use SAML2\Configuration\PrivateKey;
31
use SAML2\Constants;
32
use SAML2\XML\saml\Issuer;
33
use SAML2\XML\saml\NameID;
34
use Surfnet\SamlBundle\Entity\IdentityProvider;
35
use Surfnet\SamlBundle\SAML2\AuthnRequest as Saml2AuthnRequest;
36
use Surfnet\StepupGateway\Behat\Repository\SamlEntityRepository;
37
use Surfnet\StepupGateway\Behat\Service\FixtureService;
38
use Symfony\Component\HttpFoundation\Request;
39
use Symfony\Component\HttpFoundation\RequestStack;
40
use Symfony\Component\HttpKernel\KernelInterface;
41
42
class ServiceProviderContext implements Context
43
{
44
    const SSP_URL = 'https://ssp.dev.openconext.local/simplesaml/sp.php';
45
    const SSO_ENDPOINT_URL = 'https://gateway.dev.openconext.local/authentication/single-sign-on';
46
    const SFO_ENDPOINT_URL = 'https://gateway.dev.openconext.local/second-factor-only/single-sign-on';
47
48
    /**
49
     * @var array
50
     */
51
    private $currentSp;
52
53
    /**
54
     * @var array
55
     */
56
    private $currentSfoSp;
57
58
    /**
59
     * @var array
60
     */
61
    private $currentIdP;
62
63
    /**
64
     * @var FixtureService
65
     */
66
    private $fixtureService;
67
68
    /**
69
     * @var KernelInterface
70
     */
71
    private $kernel;
72
73
    /**
74
     * @var MinkContext
75
     */
76
    private $minkContext;
77
78
    public function __construct(
79
        FixtureService $fixtureService,
80
        KernelInterface $kernel
81
    ) {
82
        $this->fixtureService = $fixtureService;
83
        $this->kernel = $kernel;
84
    }
85
86
    /**
87
     * @BeforeScenario
88
     */
89
    public function gatherContexts(BeforeScenarioScope $scope): void
90
    {
91
        $environment = $scope->getEnvironment();
92
        $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

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

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