FeatureContext::iPassThroughTheIdP()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 0
dl 0
loc 5
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
    #[\Behat\Hook\BeforeFeature]
81
    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

81
    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...
82
    {
83
        // Generate test databases
84
        echo "Preparing test schemas\n";
85
        shell_exec("/var/www/html/bin/console doctrine:schema:drop --env=smoketest --force");
86
        shell_exec("/var/www/html/bin/console doctrine:schema:create --env=smoketest");
87
    }
88
89
    #[\Behat\Hook\BeforeScenario]
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
    #[\Behat\Step\Given('/^a user from "([^"]*)" identified by "([^"]*)" with a vetted "([^"]*)" token$/')]
97
    public function aUserIdentifiedByWithAVettedToken($institution, $nameId, $tokenType): void
98
    {
99
        switch (strtolower($tokenType)) {
100
            case "yubikey":
101
                $this->currentToken = $this->fixtureService->registerYubikeyToken($nameId, $institution);
102
                break;
103
            case "sms":
104
                $this->currentToken = $this->fixtureService->registerSmsToken($nameId, $institution);
105
                break;
106
            case "tiqr":
107
                $this->currentToken = $this->fixtureService->registerTiqrToken($nameId, $institution);
108
                break;
109
        }
110
    }
111
112
    #[\Behat\Step\Given('/^a user from "([^"]*)" identified by "([^"]*)" with a self-asserted "([^"]*)" token$/')]
113
    public function aUserIdentifiedByWithASelfAssertedToken($institution, $nameId, $tokenType): void
114
    {
115
        switch (strtolower($tokenType)) {
116
            case "yubikey":
117
                $this->currentToken = $this->fixtureService->registerYubikeyToken($nameId, $institution, true);
118
                break;
119
            case "sms":
120
                $this->currentToken = $this->fixtureService->registerSmsToken($nameId, $institution, true);
121
                break;
122
            case "tiqr":
123
                $this->currentToken = $this->fixtureService->registerTiqrToken($nameId, $institution, true);
124
                break;
125
        }
126
    }
127
128
    #[\Behat\Step\Then('I should see the Yubikey OTP screen')]
129
    public function iShouldSeeTheYubikeyOtpScreen(): void
130
    {
131
        $this->minkContext->assertPageContainsText('Your YubiKey-code');
132
    }
133
134
    #[\Behat\Step\Then('I should see the SMS verification screen')]
135
    public function iShouldSeeTheSMSScreen(): void
136
    {
137
        $this->minkContext->assertPageContainsText('Enter the received SMS-code');
138
        $this->minkContext->assertPageContainsText('Send again');
139
    }
140
141
    #[\Behat\Step\Given('/^I should see the Tiqr authentication screen$/')]
142
    public function iShouldSeeTheTiqrAuthenticationScreen(): void
143
    {
144
        $this->pressButtonWhenNoJavascriptSupport();
145
        $this->minkContext->assertPageContainsText('Log in with Tiqr');
146
    }
147
148
    #[\Behat\Step\When('I enter the OTP')]
149
    public function iEnterTheOtp(): void
150
    {
151
        $this->minkContext->fillField('gateway_verify_yubikey_otp_otp', 'bogus-otp-we-use-a-mock-yubikey-service');
152
        $this->minkContext->pressButton('gateway_verify_yubikey_otp_submit');
153
        $this->pressButtonWhenNoJavascriptSupport();
154
    }
155
156
    #[\Behat\Step\When('I enter the SMS verification code')]
157
    public function iEnterTheSmsVerificationCode(): void
158
    {
159
        $cookieValue = $this->minkContext->getSession()->getDriver()->getCookie('smoketest-sms-service');
160
        if ($cookieValue === null) {
161
            throw new RuntimeException('Unable to load the smoketest-sms-service cookie');
162
        }
163
        $matches = [];
164
        preg_match('/^Your\ SMS\ code:\ (.*)$/', $cookieValue, $matches);
165
        $this->minkContext->fillField('gateway_verify_sms_challenge_challenge', $matches[1]);
166
        $this->minkContext->pressButton('gateway_verify_sms_challenge_verify_challenge');
167
        $this->pressButtonWhenNoJavascriptSupport();
168
    }
169
170
    #[\Behat\Step\When('I enter the expired SMS verification code')]
171
    public function iEnterTheExpiredSmsVerificationCode(): void
172
    {
173
        $cookieValue = $this->minkContext->getSession()->getDriver()->getCookie('smoketest-sms-service');
174
        $matches = [];
175
        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

175
        preg_match('/^Your\ SMS\ code:\ (.*)$/', /** @scrutinizer ignore-type */ $cookieValue, $matches);
Loading history...
176
        $this->minkContext->fillField('gateway_verify_sms_challenge_challenge', $matches[1]);
177
        $this->minkContext->pressButton('gateway_verify_sms_challenge_verify_challenge');
178
    }
179
180
    #[\Behat\Step\When('I finish the Tiqr authentication')]
181
    public function iFinishGsspAuthentication(): void
182
    {
183
        $this->pressButtonWhenNoJavascriptSupport();
184
        $this->pressButtonWhenNoJavascriptSupport();
185
    }
186
187
    #[\Behat\Step\Given('/^a whitelisted institution ([^"]*)$/')]
188
    public function aWhitelistedInstitution($institution): void
189
    {
190
        $this->whitelistedInstitutions[] = $this->fixtureService->whitelist($institution)['institution'];
191
    }
192
193
    #[\Behat\Step\Given('/^an institution "([^"]*)" that allows "([^"]*)"$/')]
194
    public function anInstitutionThatAllows(string $institution, string $option): void
195
    {
196
        switch(true) {
197
            case $option === 'sso_on_2fa':
198
                $optionColumnName = 'sso_on2fa_enabled';
199
                break;
200
            case $option === 'sso_registration_bypass':
201
                $optionColumnName = 'sso_registration_bypass';
202
                break;
203
            default:
204
                throw new RuntimeException(sprintf('Option "%s" is not supported', $option));
205
        }
206
        $this->fixtureService->configureBoolean($institution, $optionColumnName, true);
207
    }
208
209
    #[\Behat\Step\Then('/^I select my ([^"]*) token on the WAYG$/')]
210
    public function iShouldSelectMyTokenOnTheWAYG($tokenType): void
211
    {
212
        switch (strtolower($tokenType)) {
213
            case "yubikey":
214
                $this->minkContext->pressButton('gateway_choose_second_factor_choose_yubikey');
215
                break;
216
            case "sms":
217
                $this->minkContext->pressButton('gateway_choose_second_factor_choose_sms');
218
                break;
219
            case "tiqr":
220
                $this->minkContext->pressButton('gateway_choose_second_factor_choose_tiqr');
221
                break;
222
        }
223
    }
224
225
    #[\Behat\Step\Then('/^I should be on the WAYG$/')]
226
    public function iShouldBeOnTheWAYG(): void
227
    {
228
        $this->minkContext->assertPageContainsText('Choose a token for login');
229
    }
230
231
    #[\Behat\Step\Then('/^an error response is posted back to the SP$/')]
232
    public function anErrorResponseIsPostedBackToTheSP(): void
233
    {
234
        $this->pressButtonWhenNoJavascriptSupport();
235
    }
236
237
    #[\Behat\Step\Given('/^I cancel the authentication$/')]
238
    public function iCancelTheAuthentication(): void
239
    {
240
        $this->minkContext->pressButton('Cancel');
241
    }
242
243
    #[\Behat\Step\Given('/^I pass through the Gateway$/')]
244
    public function iPassThroughTheGateway(): void
245
    {
246
        $this->pressButtonWhenNoJavascriptSupport();
247
    }
248
249
    #[\Behat\Step\Given('/^I pass through the IdP/')]
250
    public function iPassThroughTheIdP(): void
251
    {
252
        if ($this->minkContext->getSession()->getDriver() instanceof SymfonyDriver) {
253
            $this->minkContext->pressButton('Yes, continue');
254
        }
255
    }
256
257
    /**
258
     * @throws ExpectationException
259
     */
260
    #[\Behat\Step\Then('/^the response should have a SSO\-2FA cookie$/')]
261
    public function theResponseShouldHaveASSO2FACookie(): void
262
    {
263
        $this->minkContext->visit('https://gateway.dev.openconext.local/info');
264
        $cookieValue = $this->minkContext->getSession()->getCookie($this->sso2faCookieName);
265
        // Store the previous cookie value
266
        $this->previousSsoOn2faCookieValue = $cookieValue;
267
        $this->validateSsoOn2faCookie($cookieValue);
268
    }
269
270
271
272
    /**
273
     * @throws ExpectationException
274
     */
275
    #[\Behat\Step\Then('/^the response should have a valid session cookie$/')]
276
    public function validateSessionCookie(): void
277
    {
278
        $this->minkContext->visit('https://gateway.dev.openconext.local/info');
279
280
        $driver = $this->minkContext->getSession()->getDriver();
281
282
        $cookie = $this->minkContext->getSession()->getCookie($this->sessCookieName);
283
        if ($cookie === null) {
284
            throw new ExpectationException(
285
                'No session cookie found',
286
                $this->minkContext->getSession()->getDriver()
287
            );
288
        }
289
290
        if (!$driver instanceof ChromeDriver) {
291
            return;
292
        }
293
294
        $sessionCookie = null;
295
        foreach ($driver->getCookies() as $cookie) {
296
            if ($cookie['name'] === $this->sessCookieName) {
297
                $sessionCookie = $cookie;
298
                break;
299
            }
300
        }
301
        if ($sessionCookie === null) {
302
            throw new ExpectationException(
303
                'No session cookie found',
304
                $this->minkContext->getSession()->getDriver()
305
            );
306
        }
307
308
        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

308
        if (!array_key_exists('domain', /** @scrutinizer ignore-type */ $sessionCookie) || $sessionCookie['domain'] !== $this->cookieDomain) {
Loading history...
309
            throw new ExpectationException(
310
                'The domain of the session cookie is invalid',
311
                $this->minkContext->getSession()->getDriver()
312
            );
313
        };
314
    }
315
316
    /**
317
     * @throws ExpectationException
318
     */
319
    #[\Behat\Step\Then('/^the response should not have a SSO\-2FA cookie$/')]
320
    public function theResponseShouldNotHaveASSO2FACookie(): void
321
    {
322
        $this->minkContext->visit('https://gateway.dev.openconext.local/info');
323
        $cookie = $this->minkContext->getSession()->getCookie($this->sso2faCookieName);
324
        if (!is_null($cookie)) {
325
            throw new ExpectationException(
326
                'The SSO cookie must NOT be present',
327
                $this->minkContext->getSession()->getDriver()
328
            );
329
        }
330
    }
331
332
    /**
333
     * @throws ExpectationException
334
     */
335
    #[\Behat\Step\Then('/^a new SSO\-2FA cookie was written$/')]
336
    public function theSSO2FACookieIsRewritten(): void
337
    {
338
        $this->minkContext->visit('https://gateway.dev.openconext.local/info');
339
        $cookieValue = $this->minkContext->getSession()->getCookie($this->sso2faCookieName);
340
        $this->validateSsoOn2faCookie($cookieValue);
341
342
        if ($this->previousSsoOn2faCookieValue === $cookieValue) {
343
            throw new ExpectationException(
344
                sprintf('The SSO on 2FA cookie did not change since the previous response: "%s" !== "%s"', $this->previousSsoOn2faCookieValue, $cookieValue),
345
                $this->minkContext->getSession()->getDriver()
346
            );
347
        }
348
    }
349
350
    /**
351
     * @throws ExpectationException
352
     */
353
    #[\Behat\Step\Then('/^the existing SSO\-2FA cookie was used$/')]
354
    public function theSSO2FACookieRemainedTheSame(): void
355
    {
356
        $this->minkContext->visit('https://gateway.dev.openconext.local/info');
357
        $cookieValue = $this->minkContext->getSession()->getCookie($this->sso2faCookieName);
358
        $this->validateSsoOn2faCookie($cookieValue);
359
        if ($this->previousSsoOn2faCookieValue !== $cookieValue) {
360
            throw new ExpectationException(
361
                sprintf(
362
                    'The SSO on 2FA cookie changed since the previous response %s vs %s',
363
                    $this->previousSsoOn2faCookieValue,
364
                    $cookieValue
365
                ),
366
                $this->minkContext->getSession()->getDriver()
367
            );
368
        }
369
    }
370
371
    #[\Behat\Step\Given('/^the user cleared cookies from browser$/')]
372
    public function userClearedCookies(): void
373
    {
374
        $this->minkContext->visit('https://gateway.dev.openconext.local/info');
375
        $this->minkContext->getSession()->setCookie($this->sso2faCookieName, null);
376
    }
377
378
    #[\Behat\Step\Given('/^the SSO\-2FA cookie should contain "([^"]*)"$/')]
379
    public function theSSO2FACookieShouldContain($expectedCookieValue): void
380
    {
381
        $this->minkContext->visit('https://gateway.dev.openconext.local/info');
382
        $cookieValue = $this->minkContext->getSession()->getCookie($this->sso2faCookieName);
383
        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

383
        if (strstr(/** @scrutinizer ignore-type */ $cookieValue, $expectedCookieValue) === false) {
Loading history...
384
            throw new ExpectationException(
385
                sprintf(
386
                    'The SSO on 2FA cookie did not contain the expected value: "%s", actual contents: "%s"',
387
                    $expectedCookieValue,
388
                    $cookieValue
389
                ),
390
                $this->minkContext->getSession()->getDriver()
391
            );
392
        }
393
    }
394
395
    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...
396
    {
397
        $response = [];
398
        foreach($responseCookieHeaders as $cookie) {
399
            $parts = explode('=', $cookie);
400
            $response[] = array_shift($parts);
401
        }
402
        return $response;
403
    }
404
405
    /**
406
     * @throws ExpectationException
407
     */
408
    private function validateSsoOn2faCookie(?string $cookieValue): void
409
    {
410
        if (empty($cookieValue)) {
411
            throw new ExpectationException(
412
                sprintf(
413
                    'The SSO on 2FA cookie was not present, or empty. Cookie name: %s',
414
                    $this->sso2faCookieName
415
                ),
416
                $this->minkContext->getSession()->getDriver()
417
            );
418
        }
419
    }
420
421
    private function pressButtonWhenNoJavascriptSupport()
422
    {
423
        if ($this->minkContext->getSession()->getDriver() instanceof SymfonyDriver) {
424
            $this->minkContext->pressButton('Submit');
425
        }
426
    }
427
}
428