Passed
Push — feature/run-behat-tests-in-smo... ( f22186...042f4a )
by Michiel
05:22
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 Behat\Symfony2Extension\Context\KernelAwareContext;
25
use RuntimeException;
26
use RobRichards\XMLSecLibs\XMLSecurityKey;
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, KernelAwareContext
44
{
45
    const SSP_URL = 'https://ssp.dev.openconext.local/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(FixtureService $fixtureService)
80
    {
81
        $this->fixtureService = $fixtureService;
82
    }
83
84
    public function setKernel(KernelInterface $kernel)
85
    {
86
        $this->kernel = $kernel;
87
    }
88
89
    /**
90
     * @BeforeScenario
91
     */
92
    public function gatherContexts(BeforeScenarioScope $scope)
93
    {
94
        $environment = $scope->getEnvironment();
95
        $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. ( 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)
102
    {
103
        $this->registerSp($entityId, true);
104
    }
105
106
    /**
107
     * @Given /^an SP with EntityID ([^\']*)$/
108
     */
109
    public function anSPWithEntityID($entityId)
110
    {
111
        $this->registerSp($entityId, false);
112
    }
113
    /**
114
     * @Given /^an IdP with EntityID ([^\']*)$/
115
     */
116
    public function anIdPWithEntityID($entityId)
117
    {
118
        $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

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)
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)
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)
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)
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
        $request = Saml2AuthnRequest::createNew($authnRequest);
200
        $query = $request->buildRequestQuery();
201
202
        $this->getSession()->visit($request->getDestination().'?'.$query);
203
    }
204
205
    /**
206
     * @When /^([^\']*) starts an SFO authentication requiring LoA ([^\']*)$/
207
     */
208
    public function iStartAnSFOAuthenticationWithLoaRequirement($nameId, $loa)
209
    {
210
        $this->iStartAnSFOAuthenticationWithLoa($nameId, $loa);
211
    }
212
    /**
213
     * @When /^([^\']*) starts a forced SFO authentication requiring LoA ([^\']*)$/
214
     */
215
    public function iStartAForcedSFOAuthenticationWithLoaRequirement($nameId, $loa)
216
    {
217
        $this->iStartAnSFOAuthenticationWithLoa($nameId, $loa, true);
218
    }
219
220
    /**
221
     * @When /^([^\']*) starts an ADFS authentication requiring ([^\']*)$/
222
     */
223
    public function iStartAnADFSAuthenticationWithLoaRequirement($nameId, $loa)
224
    {
225
        $requestParams = [
226
            'loa' => $loa,
227
            'nameId' => $nameId,
228
            'entityId' => $this->currentSfoSp['entityId']
229
        ];
230
        $this->getSession()->visit(SamlEntityRepository::SP_ADFS_SSO_LOCATION . '?' . http_build_query($requestParams));
231
        $this->minkContext->pressButton('Submit');
232
    }
233
234
    /**
235
     * @When /^([^\']*) starts an authentication at Default SP$/
236
     */
237
    public function iStartAnAuthenticationAtDefaultSP($nameId)
238
    {
239
        $authnRequest = new AuthnRequest();
240
        // In order to later assert if the response succeeded or failed, set our own dummy ACS location
241
        $authnRequest->setAssertionConsumerServiceURL(SamlEntityRepository::SP_ACS_LOCATION);
242
        $issuerVo = new Issuer();
243
        $issuerVo->setValue('https://ssp.dev.openconext.local/module.php/saml/sp/metadata.php/default-sp');
244
        $authnRequest->setIssuer($issuerVo);
245
        $authnRequest->setDestination(self::SSO_ENDPOINT_URL);
246
        $authnRequest->setProtocolBinding(Constants::BINDING_HTTP_REDIRECT);
247
        $authnRequest->setNameId($this->buildNameId($nameId));
248
        // Sign with random key, does not mather for now.
249
        // todo: use from services_test.yml
250
        $authnRequest->setSignatureKey(
251
            $this->loadPrivateKey(new PrivateKey('/config/ssp/sp.key', 'default'))
252
        );
253
        $authnRequest->setRequestedAuthnContext(
254
            ['AuthnContextClassRef' => ['http://dev.openconext.local/assurance/level2']]
255
        );
256
        $request = Saml2AuthnRequest::createNew($authnRequest);
257
        $query = $request->buildRequestQuery();
258
        $this->getSession()->visit($authnRequest->getDestination().'?'.$query);
259
    }
260
261
    /**
262
     * @When /^([^\']*) starts an authentication requiring LoA ([^\']*)$/
263
     */
264
    public function iStartAnSsoAuthenticationWithLoaRequirement($nameId, $loa)
265
    {
266
        $authnRequest = new AuthnRequest();
267
        // In order to later assert if the response succeeded or failed, set our own dummy ACS location
268
        $authnRequest->setAssertionConsumerServiceURL(SamlEntityRepository::SP_ACS_LOCATION);
269
        $issuerVo = new Issuer();
270
        $issuerVo->setValue($this->currentSp['entityId']);
271
        $authnRequest->setIssuer($issuerVo);
272
        $authnRequest->setDestination(self::SSO_ENDPOINT_URL);
273
        $authnRequest->setProtocolBinding(Constants::BINDING_HTTP_REDIRECT);
274
        $authnRequest->setNameId($this->buildNameId($nameId));
275
        // Sign with random key, does not mather for now.
276
        $authnRequest->setSignatureKey(
277
            $this->loadPrivateKey(new PrivateKey('/config/ssp/sp.key', 'default'))
278
        );
279
280
        switch ($loa) {
281
            case "1":
282
            case "1.5":
283
            case "2":
284
            case "3":
285
                $authnRequest->setRequestedAuthnContext(
286
                    ['AuthnContextClassRef' => ['http://dev.openconext.local/assurance/loa' . $loa]]
287
                );
288
                break;
289
            case "self-asserted":
290
                $authnRequest->setRequestedAuthnContext(
291
                    ['AuthnContextClassRef' => ['http://dev.openconext.local/assurance/loa1.5']]
292
                );
293
                break;
294
            default:
295
                throw new RuntimeException(sprintf('The specified LoA-%s is not supported', $loa));
296
        }
297
298
        $request = Saml2AuthnRequest::createNew($authnRequest);
299
        $query = $request->buildRequestQuery();
300
        $this->getSession()->visit($request->getDestination().'?'.$query);
301
    }
302
303
    /**
304
     * @When /^I authenticate at the IdP as ([^\']*)$/
305
     */
306
    public function iAuthenticateAtTheIdp($username)
307
    {
308
        $this->minkContext->fillField('username', $username);
309
        $this->minkContext->fillField('password', $username);
310
        // Submit the form
311
        $this->minkContext->pressButton('Login');
312
        if (!$this->getSession()->getDriver() instanceof Selenium2Driver) {
313
            // Submit the SAML Response from SimpleSamplPHP IdP
314
            $this->minkContext->pressButton('Yes, continue');
315
        }
316
    }
317
318
    /**
319
     * @return IdentityProvider
320
     */
321
    public function getIdentityProvider()
322
    {
323
        /** @var RequestStack $stack */
324
325
        $stack = $this->kernel->getContainer()->get('request_stack');
326
        $stack->push(Request::create('https://gateway.dev.openconext.local'));
327
        $ip = $this->kernel->getContainer()->get('surfnet_saml.hosted.identity_provider');
328
        $stack->pop();
329
330
        return $ip;
331
    }
332
333
    private static function loadPrivateKey(PrivateKey $key)
334
    {
335
        $keyLoader = new PrivateKeyLoader();
336
        $privateKey = $keyLoader->loadPrivateKey($key);
337
338
        $key = new XMLSecurityKey(XMLSecurityKey::RSA_SHA256, ['type' => 'private']);
339
        $key->loadKey($privateKey->getKeyAsString());
340
341
        return $key;
342
    }
343
344
    /**
345
     * @Given /^I log out at the IdP$/
346
     */
347
    public function iLogOutAtTheIdP()
348
    {
349
        $this->minkContext->visit(self::SSP_URL);
350
        $this->minkContext->pressButton('Logout');
351
    }
352
353
    private function getSession()
354
    {
355
        return $this->minkContext->getSession();
356
    }
357
358
    private function buildNameId(string $nameId): NameID
359
    {
360
        $nameIdVo = new NameID();
361
        $nameIdVo->setValue($nameId);
362
        $nameIdVo->setFormat(Constants::NAMEFORMAT_UNSPECIFIED);
363
        return $nameIdVo;
364
    }
365
}
366