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\BeforeFeatureScope;
23
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
24
use Behat\Mink\Exception\ExpectationException;
25
use DMore\ChromeDriver\ChromeDriver;
26
use FriendsOfBehat\SymfonyExtension\Driver\SymfonyDriver;
27
use Psr\Log\LoggerInterface;
28
use RuntimeException;
29
use SAML2\Compat\ContainerSingleton;
30
use Surfnet\SamlBundle\Tests\TestSaml2Container;
31
use Surfnet\StepupGateway\Behat\Service\FixtureService;
32
33
class FeatureContext implements Context
34
{
35
    /**
36
     * @var FixtureService
37
     */
38
    private $fixtureService;
39
40
    private $whitelistedInstitutions = [];
41
42
    /**
43
     * @var MinkContext
44
     */
45
    private $minkContext;
46
47
    /**
48
     * @var array
49
     */
50
    private $currentToken;
51
52
    private $sso2faCookieName;
53
54
    /**
55
     * @var string|null
56
     */
57
    private $previousSsoOn2faCookieValue;
58
59
    /**
60
     * @var string
61
     */
62
    private $sessCookieName;
63
64
    /**
65
     * @var string
66
     */
67
    private $cookieDomain;
68
69
    public function __construct(FixtureService $fixtureService, LoggerInterface $logger)
70
    {
71
        $this->fixtureService = $fixtureService;
72
        $this->sso2faCookieName = 'stepup-gateway_sso-on-second-factor-authentication';
73
        $this->sessCookieName = 'MOCKSESSID';
74
        $this->cookieDomain = '.gateway.dev.openconext.local';
75
76
        // Set a test container for the SAML2 Library to work with (the compat container is broken)
77
        ContainerSingleton::setContainer(new TestSaml2Container($logger));
78
    }
79
80
    /**
81
     * @BeforeFeature
82
     */
83
    public static function setupDatabase(BeforeFeatureScope $scope): void
0 ignored issues
show
Unused Code introduced by
The parameter $scope is not used and could be removed. ( Ignorable by Annotation )

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

83
    public static function setupDatabase(/** @scrutinizer ignore-unused */ BeforeFeatureScope $scope): void

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
84
    {
85
        // Generate test databases
86
        echo "Preparing test schemas\n";
87
        shell_exec("/var/www/html/bin/console doctrine:schema:drop --env=smoketest --force");
88
        shell_exec("/var/www/html/bin/console doctrine:schema:create --env=smoketest");
89
    }
90
91
    /**
92
     * @BeforeScenario
93
     */
94
    public function gatherContexts(BeforeScenarioScope $scope): void
95
    {
96
        $environment = $scope->getEnvironment();
97
        $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

97
        /** @scrutinizer ignore-call */ 
98
        $this->minkContext = $environment->getContext(MinkContext::class);
Loading history...
98
    }
99
100
    /**
101
     * @Given /^a user from "([^"]*)" identified by "([^"]*)" with a vetted "([^"]*)" token$/
102
     */
103
    public function aUserIdentifiedByWithAVettedToken($institution, $nameId, $tokenType): void
104
    {
105
        switch (strtolower($tokenType)) {
106
            case "yubikey":
107
                $this->currentToken = $this->fixtureService->registerYubikeyToken($nameId, $institution);
108
                break;
109
            case "sms":
110
                $this->currentToken = $this->fixtureService->registerSmsToken($nameId, $institution);
111
                break;
112
            case "tiqr":
113
                $this->currentToken = $this->fixtureService->registerTiqrToken($nameId, $institution);
114
                break;
115
        }
116
    }
117
118
    /**
119
     * @Given /^a user from "([^"]*)" identified by "([^"]*)" with a self-asserted "([^"]*)" token$/
120
     */
121
    public function aUserIdentifiedByWithASelfAssertedToken($institution, $nameId, $tokenType): void
122
    {
123
        switch (strtolower($tokenType)) {
124
            case "yubikey":
125
                $this->currentToken = $this->fixtureService->registerYubikeyToken($nameId, $institution, true);
126
                break;
127
            case "sms":
128
                $this->currentToken = $this->fixtureService->registerSmsToken($nameId, $institution, true);
129
                break;
130
            case "tiqr":
131
                $this->currentToken = $this->fixtureService->registerTiqrToken($nameId, $institution, true);
132
                break;
133
        }
134
    }
135
136
    /**
137
     * @Then I should see the Yubikey OTP screen
138
     */
139
    public function iShouldSeeTheYubikeyOtpScreen(): void
140
    {
141
        $this->minkContext->assertPageContainsText('Your YubiKey-code');
142
    }
143
144
    /**
145
     * @Then I should see the SMS verification screen
146
     */
147
    public function iShouldSeeTheSMSScreen(): void
148
    {
149
        $this->minkContext->assertPageContainsText('Enter the received SMS-code');
150
        $this->minkContext->assertPageContainsText('Send again');
151
    }
152
153
    /**
154
     * @Given /^I should see the Tiqr authentication screen$/
155
     */
156
    public function iShouldSeeTheTiqrAuthenticationScreen(): void
157
    {
158
        $this->pressButtonWhenNoJavascriptSupport();
159
        $this->minkContext->assertPageContainsText('Log in with Tiqr');
160
    }
161
162
    /**
163
     * @When I enter the OTP
164
     */
165
    public function iEnterTheOtp(): void
166
    {
167
        $this->minkContext->fillField('gateway_verify_yubikey_otp_otp', 'bogus-otp-we-use-a-mock-yubikey-service');
168
        $this->minkContext->pressButton('gateway_verify_yubikey_otp_submit');
169
        $this->pressButtonWhenNoJavascriptSupport();
170
    }
171
172
    /**
173
     * @When I enter the SMS verification code
174
     */
175
    public function iEnterTheSmsVerificationCode(): void
176
    {
177
        $cookieValue = $this->minkContext->getSession()->getDriver()->getCookie('smoketest-sms-service');
178
        if ($cookieValue === null) {
179
            throw new RuntimeException('Unable to load the smoketest-sms-service cookie');
180
        }
181
        $matches = [];
182
        preg_match('/^Your\ SMS\ code:\ (.*)$/', $cookieValue, $matches);
183
        $this->minkContext->fillField('gateway_verify_sms_challenge_challenge', $matches[1]);
184
        $this->minkContext->pressButton('gateway_verify_sms_challenge_verify_challenge');
185
        $this->pressButtonWhenNoJavascriptSupport();
186
    }
187
188
    /**
189
     * @When I enter the expired SMS verification code
190
     */
191
    public function iEnterTheExpiredSmsVerificationCode(): void
192
    {
193
        $cookieValue = $this->minkContext->getSession()->getDriver()->getCookie('smoketest-sms-service');
194
        $matches = [];
195
        preg_match('/^Your\ SMS\ code:\ (.*)$/', $cookieValue, $matches);
0 ignored issues
show
Bug introduced by
It seems like $cookieValue can also be of type null; however, parameter $subject of preg_match() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

195
        preg_match('/^Your\ SMS\ code:\ (.*)$/', /** @scrutinizer ignore-type */ $cookieValue, $matches);
Loading history...
196
        $this->minkContext->fillField('gateway_verify_sms_challenge_challenge', $matches[1]);
197
        $this->minkContext->pressButton('gateway_verify_sms_challenge_verify_challenge');
198
    }
199
200
    /**
201
     * @When I finish the Tiqr authentication
202
     */
203
    public function iFinishGsspAuthentication(): void
204
    {
205
        $this->pressButtonWhenNoJavascriptSupport();
206
        $this->pressButtonWhenNoJavascriptSupport();
207
    }
208
209
    /**
210
     * @Given /^a whitelisted institution ([^"]*)$/
211
     */
212
    public function aWhitelistedInstitution($institution): void
213
    {
214
        $this->whitelistedInstitutions[] = $this->fixtureService->whitelist($institution)['institution'];
215
    }
216
217
    /**
218
     * @Given /^an institution "([^"]*)" that allows "([^"]*)"$/
219
     */
220
    public function anInstitutionThatAllows(string $institution, string $option): void
221
    {
222
        switch(true) {
223
            case $option === 'sso_on_2fa':
224
                $optionColumnName = 'sso_on2fa_enabled';
225
                break;
226
            default:
227
                throw new RuntimeException(sprintf('Option "%s" is not supported', $option));
228
        }
229
        $this->fixtureService->configureBoolean($institution, $optionColumnName, true);
230
    }
231
232
    /**
233
     * @Then /^I select my ([^"]*) token on the WAYG$/
234
     */
235
    public function iShouldSelectMyTokenOnTheWAYG($tokenType): void
236
    {
237
        switch (strtolower($tokenType)) {
238
            case "yubikey":
239
                $this->minkContext->pressButton('gateway_choose_second_factor_choose_yubikey');
240
                break;
241
            case "sms":
242
                $this->minkContext->pressButton('gateway_choose_second_factor_choose_sms');
243
                break;
244
            case "tiqr":
245
                $this->minkContext->pressButton('gateway_choose_second_factor_choose_tiqr');
246
                break;
247
        }
248
    }
249
250
    /**
251
     * @Then /^I should be on the WAYG$/
252
     */
253
    public function iShouldBeOnTheWAYG(): void
254
    {
255
        $this->minkContext->assertPageContainsText('Choose a token for login');
256
    }
257
258
    /**
259
     * @Then /^an error response is posted back to the SP$/
260
     */
261
    public function anErrorResponseIsPostedBackToTheSP(): void
262
    {
263
        $this->pressButtonWhenNoJavascriptSupport();
264
    }
265
266
    /**
267
     * @Given /^I cancel the authentication$/
268
     */
269
    public function iCancelTheAuthentication(): void
270
    {
271
        $this->minkContext->pressButton('Cancel');
272
    }
273
274
    /**
275
     * @Given /^I pass through the Gateway$/
276
     */
277
    public function iPassThroughTheGateway(): void
278
    {
279
        $this->pressButtonWhenNoJavascriptSupport();
280
    }
281
282
    /**
283
     * @Given /^I pass through the IdP/
284
     */
285
    public function iPassThroughTheIdP(): void
286
    {
287
        if ($this->minkContext->getSession()->getDriver() instanceof SymfonyDriver) {
288
            $this->minkContext->pressButton('Yes, continue');
289
        }
290
    }
291
292
    /**
293
     * @Then /^the response should have a SSO\-2FA cookie$/
294
     * @throws ExpectationException
295
     */
296
    public function theResponseShouldHaveASSO2FACookie(): void
297
    {
298
        $this->minkContext->visit('https://gateway.dev.openconext.local/info');
299
        $cookieValue = $this->minkContext->getSession()->getCookie($this->sso2faCookieName);
300
        // Store the previous cookie value
301
        $this->previousSsoOn2faCookieValue = $cookieValue;
302
        $this->validateSsoOn2faCookie($cookieValue);
303
    }
304
305
306
307
    /**
308
     * @Then /^the response should have a valid session cookie$/
309
     * @throws ExpectationException
310
     */
311
    public function validateSessionCookie(): void
312
    {
313
        $this->minkContext->visit('https://gateway.dev.openconext.local/info');
314
315
        $driver = $this->minkContext->getSession()->getDriver();
316
317
        $cookie = $this->minkContext->getSession()->getCookie($this->sessCookieName);
318
        if ($cookie === null) {
319
            throw new ExpectationException(
320
                'No session cookie found',
321
                $this->minkContext->getSession()->getDriver()
322
            );
323
        }
324
325
        if (!$driver instanceof ChromeDriver) {
326
            return;
327
        }
328
329
        $sessionCookie = null;
330
        foreach ($driver->getCookies() as $cookie) {
331
            if ($cookie['name'] === $this->sessCookieName) {
332
                $sessionCookie = $cookie;
333
                break;
334
            }
335
        }
336
        if ($sessionCookie === null) {
337
            throw new ExpectationException(
338
                'No session cookie found',
339
                $this->minkContext->getSession()->getDriver()
340
            );
341
        }
342
343
        if (!array_key_exists('domain', $sessionCookie) || $sessionCookie['domain'] !== $this->cookieDomain) {
0 ignored issues
show
Bug introduced by
$sessionCookie of type string is incompatible with the type ArrayObject|array expected by parameter $array of array_key_exists(). ( Ignorable by Annotation )

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

343
        if (!array_key_exists('domain', /** @scrutinizer ignore-type */ $sessionCookie) || $sessionCookie['domain'] !== $this->cookieDomain) {
Loading history...
344
            throw new ExpectationException(
345
                'The domain of the session cookie is invalid',
346
                $this->minkContext->getSession()->getDriver()
347
            );
348
        };
349
    }
350
351
    /**
352
     * @Then /^the response should not have a SSO\-2FA cookie$/
353
     * @throws ExpectationException
354
     */
355
    public function theResponseShouldNotHaveASSO2FACookie(): void
356
    {
357
        $this->minkContext->visit('https://gateway.dev.openconext.local/info');
358
        $cookie = $this->minkContext->getSession()->getCookie($this->sso2faCookieName);
359
        if (!is_null($cookie)) {
360
            throw new ExpectationException(
361
                'The SSO cookie must NOT be present',
362
                $this->minkContext->getSession()->getDriver()
363
            );
364
        }
365
    }
366
367
    /**
368
     * @Then /^a new SSO\-2FA cookie was written$/
369
     * @throws ExpectationException
370
     */
371
    public function theSSO2FACookieIsRewritten(): void
372
    {
373
        $this->minkContext->visit('https://gateway.dev.openconext.local/info');
374
        $cookieValue = $this->minkContext->getSession()->getCookie($this->sso2faCookieName);
375
        $this->validateSsoOn2faCookie($cookieValue);
376
377
        if ($this->previousSsoOn2faCookieValue === $cookieValue) {
378
            throw new ExpectationException(
379
                'The SSO on 2FA cookie did not change since the previous response',
380
                $this->minkContext->getSession()->getDriver()
381
            );
382
        }
383
    }
384
385
    /**
386
     * @Then /^the existing SSO\-2FA cookie was used$/
387
     * @throws ExpectationException
388
     */
389
    public function theSSO2FACookieRemainedTheSame(): void
390
    {
391
        $this->minkContext->visit('https://gateway.dev.openconext.local/info');
392
        $cookieValue = $this->minkContext->getSession()->getCookie($this->sso2faCookieName);
393
        $this->validateSsoOn2faCookie($cookieValue);
394
        if ($this->previousSsoOn2faCookieValue !== $cookieValue) {
395
            throw new ExpectationException(
396
                sprintf(
397
                    'The SSO on 2FA cookie changed since the previous response %s vs %s',
398
                    $this->previousSsoOn2faCookieValue,
399
                    $cookieValue
400
                ),
401
                $this->minkContext->getSession()->getDriver()
402
            );
403
        }
404
    }
405
406
    /**
407
     * @Given /^the user cleared cookies from browser$/
408
     */
409
    public function userClearedCookies(): void
410
    {
411
        $this->minkContext->visit('https://gateway.dev.openconext.local/info');
412
        $this->minkContext->getSession()->setCookie($this->sso2faCookieName, null);
413
    }
414
415
    /**
416
     * @Given /^the SSO\-2FA cookie should contain "([^"]*)"$/
417
     */
418
    public function theSSO2FACookieShouldContain($expectedCookieValue): void
419
    {
420
        $this->minkContext->visit('https://gateway.dev.openconext.local/info');
421
        $cookieValue = $this->minkContext->getSession()->getCookie($this->sso2faCookieName);
422
        if (strstr($cookieValue, $expectedCookieValue) === false) {
0 ignored issues
show
Bug introduced by
It seems like $cookieValue can also be of type null; however, parameter $haystack of strstr() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

422
        if (strstr(/** @scrutinizer ignore-type */ $cookieValue, $expectedCookieValue) === false) {
Loading history...
423
            throw new ExpectationException(
424
                sprintf(
425
                    'The SSO on 2FA cookie did not contain the expected value: "%s", actual contents: "%s"',
426
                    $expectedCookieValue,
427
                    $cookieValue
428
                ),
429
                $this->minkContext->getSession()->getDriver()
430
            );
431
        }
432
    }
433
434
    private function getCookieNames(array $responseCookieHeaders): array
0 ignored issues
show
Unused Code introduced by
The method getCookieNames() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
435
    {
436
        $response = [];
437
        foreach($responseCookieHeaders as $cookie) {
438
            $parts = explode('=', $cookie);
439
            $response[] = array_shift($parts);
440
        }
441
        return $response;
442
    }
443
444
    /**
445
     * @throws ExpectationException
446
     */
447
    private function validateSsoOn2faCookie(?string $cookieValue): void
448
    {
449
        if (empty($cookieValue)) {
450
            throw new ExpectationException(
451
                sprintf(
452
                    'The SSO on 2FA cookie was not present, or empty. Cookie name: %s',
453
                    $this->sso2faCookieName
454
                ),
455
                $this->minkContext->getSession()->getDriver()
456
            );
457
        }
458
    }
459
460
    private function pressButtonWhenNoJavascriptSupport()
461
    {
462
        if ($this->minkContext->getSession()->getDriver() instanceof SymfonyDriver) {
463
            $this->minkContext->pressButton('Submit');
464
        }
465
    }
466
}
467