Passed
Pull Request — release/4.2 (#316)
by Michiel
09:19 queued 04:40
created

FeatureContext::iShouldSeeTheYubikeyOtpScreen()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
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 Exception;
26
use RuntimeException;
27
use Surfnet\StepupGateway\Behat\Service\FixtureService;
28
use function is_null;
29
use function sprintf;
30
31
class FeatureContext implements Context
32
{
33
    /**
34
     * @var FixtureService
35
     */
36
    private $fixtureService;
37
38
    private $whitelistedInstitutions = [];
39
40
    /**
41
     * @var MinkContext
42
     */
43
    private $minkContext;
44
45
    /**
46
     * @var array
47
     */
48
    private $currentToken;
49
50
    private $sso2faCookieName;
51
52
    /**
53
     * @var string|null
54
     */
55
    private $previousSsoOn2faCookieValue;
56
57
    public function __construct(FixtureService $fixtureService)
58
    {
59
        $this->fixtureService = $fixtureService;
60
        $this->sso2faCookieName = 'stepup-gateway_sso-on-second-factor-authentication';
61
    }
62
63
    /**
64
     * @BeforeFeature
65
     */
66
    public static function setupDatabase(BeforeFeatureScope $scope)
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

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

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...
67
    {
68
        // Generate test databases
69
        echo "Preparing test schemas\n";
70
        shell_exec("/var/www/bin/console doctrine:schema:drop --env=smoketest --force");
71
        shell_exec("/var/www/bin/console doctrine:schema:create --env=smoketest");
72
    }
73
74
    /**
75
     * @BeforeScenario
76
     */
77
    public function gatherContexts(BeforeScenarioScope $scope)
78
    {
79
        $environment = $scope->getEnvironment();
80
        $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

80
        /** @scrutinizer ignore-call */ 
81
        $this->minkContext = $environment->getContext(MinkContext::class);
Loading history...
81
    }
82
83
    /**
84
     * @Given /^a user from "([^"]*)" identified by "([^"]*)" with a vetted "([^"]*)" token$/
85
     */
86
    public function aUserIdentifiedByWithAVettedToken($institution, $nameId, $tokenType)
87
    {
88
        switch (strtolower($tokenType)) {
89
            case "yubikey":
90
                $this->currentToken = $this->fixtureService->registerYubikeyToken($nameId, $institution);
91
                break;
92
            case "sms":
93
                $this->currentToken = $this->fixtureService->registerSmsToken($nameId, $institution);
94
                break;
95
            case "tiqr":
96
                $this->currentToken = $this->fixtureService->registerTiqrToken($nameId, $institution);
97
                break;
98
        }
99
    }
100
101
    /**
102
     * @Given /^a user from "([^"]*)" identified by "([^"]*)" with a self-asserted "([^"]*)" token$/
103
     */
104
    public function aUserIdentifiedByWithASelfAssertedToken($institution, $nameId, $tokenType)
105
    {
106
        switch (strtolower($tokenType)) {
107
            case "yubikey":
108
                $this->currentToken = $this->fixtureService->registerYubikeyToken($nameId, $institution, true);
109
                break;
110
            case "sms":
111
                $this->currentToken = $this->fixtureService->registerSmsToken($nameId, $institution, true);
112
                break;
113
            case "tiqr":
114
                $this->currentToken = $this->fixtureService->registerTiqrToken($nameId, $institution, true);
115
                break;
116
        }
117
    }
118
119
    /**
120
     * @Then I should see the Yubikey OTP screen
121
     */
122
    public function iShouldSeeTheYubikeyOtpScreen()
123
    {
124
        $this->minkContext->assertPageContainsText('Your YubiKey-code');
125
    }
126
127
    /**
128
     * @Then I should see the SMS verification screen
129
     */
130
    public function iShouldSeeTheSMSScreen()
131
    {
132
        $this->minkContext->assertPageContainsText('Enter the received SMS-code');
133
        $this->minkContext->assertPageContainsText('Send again');
134
    }
135
136
    /**
137
     * @Given /^I should see the Tiqr authentication screen$/
138
     */
139
    public function iShouldSeeTheTiqrAuthenticationScreen()
140
    {
141
        $this->minkContext->pressButton('Submit');
142
        $this->minkContext->assertPageContainsText('Log in with Tiqr');
143
    }
144
145
    /**
146
     * @When I enter the OTP
147
     */
148
    public function iEnterTheOtp()
149
    {
150
        $this->minkContext->fillField('gateway_verify_yubikey_otp_otp', 'bogus-otp-we-use-a-mock-yubikey-service');
151
        $this->minkContext->pressButton('gateway_verify_yubikey_otp_submit');
152
        $this->minkContext->pressButton('Submit');
153
    }
154
155
    /**
156
     * @When I enter the SMS verification code
157
     */
158
    public function iEnterTheSmsVerificationCode()
159
    {
160
        $cookieValue = $this->minkContext->getSession()->getDriver()->getCookie('smoketest-sms-service');
161
        $matches = [];
162
        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

162
        preg_match('/^Your\ SMS\ code:\ (.*)$/', /** @scrutinizer ignore-type */ $cookieValue, $matches);
Loading history...
163
        $this->minkContext->fillField('gateway_verify_sms_challenge_challenge', $matches[1]);
164
        $this->minkContext->pressButton('gateway_verify_sms_challenge_verify_challenge');
165
        $this->minkContext->pressButton('Submit');
166
    }
167
168
    /**
169
     * @When I enter the expired SMS verification code
170
     */
171
    public function iEnterTheExpiredSmsVerificationCode()
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
    /**
181
     * @When I finish the Tiqr authentication
182
     */
183
    public function iFinishGsspAuthentication()
184
    {
185
        $this->minkContext->pressButton('Submit');
186
        $this->minkContext->pressButton('Submit');
187
    }
188
189
    /**
190
     * @Given /^a whitelisted institution ([^"]*)$/
191
     */
192
    public function aWhitelistedInstitution($institution)
193
    {
194
        $this->whitelistedInstitutions[] = $this->fixtureService->whitelist($institution)['institution'];
195
    }
196
197
    /**
198
     * @Given /^an institution "([^"]*)" that allows "([^"]*)"$/
199
     */
200
    public function anInstitutionThatAllows(string $institution, string $option)
201
    {
202
        switch(true) {
203
            case $option === 'sso_on_2fa':
204
                $optionColumnName = 'sso_on2fa_enabled';
205
                break;
206
            default:
207
                throw new RuntimeException(sprintf('Option "%s" is not supported', $option));
208
        }
209
        $this->fixtureService->configureBoolean($institution, $optionColumnName, true);
210
    }
211
212
    /**
213
     * @Then /^I select my ([^"]*) token on the WAYG$/
214
     */
215
    public function iShouldSelectMyTokenOnTheWAYG($tokenType)
216
    {
217
        switch (strtolower($tokenType)) {
218
            case "yubikey":
219
                $this->minkContext->pressButton('gateway_choose_second_factor_choose_yubikey');
220
                break;
221
            case "sms":
222
                $this->minkContext->pressButton('gateway_choose_second_factor_choose_sms');
223
                break;
224
            case "tiqr":
225
                $this->minkContext->pressButton('gateway_choose_second_factor_choose_tiqr');
226
                break;
227
        }
228
    }
229
230
    /**
231
     * @Then /^I should be on the WAYG$/
232
     */
233
    public function iShouldBeOnTheWAYG()
234
    {
235
        $this->minkContext->assertPageContainsText('Choose a token for login');
236
    }
237
238
    /**
239
     * @Then /^an error response is posted back to the SP$/
240
     */
241
    public function anErrorResponseIsPostedBackToTheSP()
242
    {
243
        $this->minkContext->pressButton('Submit');
244
245
    }
246
247
    /**
248
     * @Given /^I cancel the authentication$/
249
     */
250
    public function iCancelTheAuthentication()
251
    {
252
        $this->minkContext->pressButton('Cancel');
253
    }
254
255
    /**
256
     * @Given /^I pass through the Gateway$/
257
     */
258
    public function iPassThroughTheGateway()
259
    {
260
        $this->minkContext->pressButton('Submit');
261
    }
262
263
    /**
264
     * @Given /^I pass through the IdP/
265
     */
266
    public function iPassThroughTheIdP()
267
    {
268
        $this->minkContext->pressButton('Submit');
269
    }
270
271
    /**
272
     * @Then /^the response should have a SSO\-2FA cookie$/
273
     * @throws ExpectationException
274
     */
275
    public function theResponseShouldHaveASSO2FACookie()
276
    {
277
        $this->minkContext->visit('https://gateway.stepup.example.com/info');
278
        $cookieValue = $this->minkContext->getSession()->getCookie($this->sso2faCookieName);
279
        // Store the previous cookie value
280
        $this->previousSsoOn2faCookieValue = $cookieValue;
281
        $this->validateSsoOn2faCookie($cookieValue);
282
    }
283
284
    /**
285
     * @Then /^the response should not have a SSO\-2FA cookie$/
286
     * @throws ExpectationException
287
     */
288
    public function theResponseShouldNotHaveASSO2FACookie()
289
    {
290
        $this->minkContext->visit('https://gateway.stepup.example.com/info');
291
        $cookie = $this->minkContext->getSession()->getCookie($this->sso2faCookieName);
292
        if (!is_null($cookie)) {
293
            throw new ExpectationException(
294
                'The SSO cookie must NOT be present',
295
                $this->minkContext->getSession()->getDriver()
296
            );
297
        }
298
    }
299
300
    /**
301
     * @Then /^a new SSO\-2FA cookie was written$/
302
     * @throws ExpectationException
303
     */
304
    public function theSSO2FACookieIsRewritten()
305
    {
306
        $this->minkContext->visit('https://gateway.stepup.example.com/info');
307
        $cookieValue = $this->minkContext->getSession()->getCookie($this->sso2faCookieName);
308
        $this->validateSsoOn2faCookie($cookieValue);
309
        if ($this->previousSsoOn2faCookieValue === $cookieValue) {
310
            throw new ExpectationException(
311
                'The SSO on 2FA cookie did not change since the previous response',
312
                $this->minkContext->getSession()->getDriver()
313
            );
314
        }
315
    }
316
317
    /**
318
     * @Then /^the existing SSO\-2FA cookie was used$/
319
     * @throws ExpectationException
320
     */
321
    public function theSSO2FACookieRemainedTheSame()
322
    {
323
        $this->minkContext->visit('https://gateway.stepup.example.com/info');
324
        $cookieValue = $this->minkContext->getSession()->getCookie($this->sso2faCookieName);
325
        $this->validateSsoOn2faCookie($cookieValue);
326
        if ($this->previousSsoOn2faCookieValue !== $cookieValue) {
327
            throw new ExpectationException(
328
                sprintf(
329
                    'The SSO on 2FA cookie changed since the previous response %s vs %s',
330
                    $this->previousSsoOn2faCookieValue,
331
                    $cookieValue
332
                ),
333
                $this->minkContext->getSession()->getDriver()
334
            );
335
        }
336
    }
337
338
    /**
339
     * @Given /^the user cleared cookies from browser$/
340
     */
341
    public function userClearedCookies()
342
    {
343
        $this->minkContext->visit('https://gateway.stepup.example.com/info');
344
        $this->minkContext->getSession()->setCookie($this->sso2faCookieName, null);
345
    }
346
347
    /**
348
     * @Given /^the SSO\-2FA cookie should contain "([^"]*)"$/
349
     */
350
    public function theSSO2FACookieShouldContain($expectedCookieValue)
351
    {
352
        $this->minkContext->visit('https://gateway.stepup.example.com/info');
353
        $cookieValue = $this->minkContext->getSession()->getCookie($this->sso2faCookieName);
354
        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

354
        if (strstr(/** @scrutinizer ignore-type */ $cookieValue, $expectedCookieValue) === false) {
Loading history...
355
            throw new ExpectationException(
356
                sprintf(
357
                    'The SSO on 2FA cookie did not contain the expected value: "%s", actual contents: "%s"',
358
                    $expectedCookieValue,
359
                    $cookieValue
360
                ),
361
                $this->minkContext->getSession()->getDriver()
362
            );
363
        }
364
365
    }
366
367
    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...
368
    {
369
        $response = [];
370
        foreach($responseCookieHeaders as $cookie) {
371
            $parts = explode('=', $cookie);
372
            $response[] = array_shift($parts);
373
        }
374
        return $response;
375
    }
376
377
    /**
378
     * @throws ExpectationException
379
     */
380
    private function validateSsoOn2faCookie(?string $cookieValue)
381
    {
382
        if (empty($cookieValue)) {
383
            throw new ExpectationException(
384
                sprintf(
385
                    'The SSO on 2FA cookie was not present, or empty. Cookie name: %s',
386
                    $this->sso2faCookieName
387
                ),
388
                $this->minkContext->getSession()->getDriver()
389
            );
390
        }
391
    }
392
}
393