Failed Conditions
Push — master ( 516359...a8b39a )
by Florent
04:00
created

OIDCContext   A

Complexity

Total Complexity 26

Size/Duplication

Total Lines 503
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 11

Importance

Changes 0
Metric Value
wmc 26
lcom 1
cbo 11
dl 0
loc 503
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * The MIT License (MIT)
7
 *
8
 * Copyright (c) 2014-2017 Spomky-Labs
9
 *
10
 * This software may be modified and distributed under the terms
11
 * of the MIT license.  See the LICENSE file for details.
12
 */
13
14
namespace OAuth2Framework\Bundle\Server\Tests\Context;
15
16
/*
17
 * The MIT License (MIT)
18
 *
19
 * Copyright (c) 2014-2017 Spomky-Labs
20
 *
21
 * This software may be modified and distributed under the terms
22
 * of the MIT license.  See the LICENSE file for details.
23
 */
24
25
use Assert\Assertion;
26
use Behat\Behat\Context\Context;
27
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
28
use Behat\Behat\Tester\Exception\PendingException;
29
use Behat\Gherkin\Node\PyStringNode;
30
use Behat\MinkExtension\Context\MinkContext;
31
use Behat\Symfony2Extension\Context\KernelDictionary;
32
use Jose\JWTCreatorInterface;
33
use Jose\Loader;
34
use Jose\Object\JWSInterface;
35
use OAuth2Framework\Bundle\Server\Model\ClientRepository;
36
use OAuth2Framework\Component\Server\Model\Client\Client;
37
use OAuth2Framework\Component\Server\Model\Client\ClientId;
38
39
final class OIDCContext implements Context
40
{
41
    use KernelDictionary;
42
43
    /**
44
     * @var ResponseTypeContext
45
     */
46
    private $responseTypeContext;
47
48
    /**
49
     * @var MinkContext
50
     */
51
    private $minkContext;
52
53
    /**
54
     * @BeforeScenario
55
     *
56
     * @param BeforeScenarioScope $scope
57
     */
58
    public function gatherContexts(BeforeScenarioScope $scope)
59
    {
60
        $environment = $scope->getEnvironment();
61
62
        $this->minkContext = $environment->getContext(MinkContext::class);
63
        $this->responseTypeContext = $environment->getContext(ResponseTypeContext::class);
64
    }
65
66
    /**
67
     * @When a client send a Userinfo request without access token
68
     */
69
    public function aClientSendAUserinfoRequestWithoutAccessToken()
70
    {
71
        $this->minkContext->getSession()->getDriver()->getClient()->request(
72
            'GET',
73
            'https://oauth2.test/userinfo',
74
            [],
75
            [],
76
            []
77
        );
78
    }
79
80
    /**
81
     * @When a client sends a valid Userinfo request
82
     */
83
    public function aClientSendsAValidUserinfoRequest()
84
    {
85
        $this->minkContext->getSession()->getDriver()->getClient()->request(
86
            'GET',
87
            'https://oauth2.test/userinfo',
88
            [],
89
            [],
90
            [
91
                'HTTP_Authorization' => 'Bearer VALID_ACCESS_TOKEN_FOR_USERINFO',
92
            ]
93
        );
94
    }
95
96
    /**
97
     * @Then the response contains an Id Token with the following claims for the client :clientId
98
     */
99
    public function theResponseContainsAnIdTokenWithTheFollowingClaimsForTheClient($clientId, PyStringNode $expectedClaims)
100
    {
101
        $client = $this->getContainer()->get(ClientRepository::class)->find(ClientId::create($clientId));
102
        Assertion::isInstanceOf($client, Client::class);
103
        $claims = json_decode($expectedClaims->getRaw(), true);
104
        $response = $this->minkContext->getSession()->getPage()->getContent();
105
        $loader = new Loader();
106
        $jwt = $loader->load($response);
107
        Assertion::isInstanceOf($jwt, JWSInterface::class);
108
        Assertion::true(empty(array_diff($claims, $jwt->getClaims())));
109
    }
110
111
    /**
112
     * @When a client sends a Userinfo request but the access token has no openid scope
113
     */
114
    public function aClientSendsAUserinfoRequestButTheAccessTokenHasNoOpenidScope()
115
    {
116
        $this->minkContext->getSession()->getDriver()->getClient()->request(
117
            'GET',
118
            'https://oauth2.test/userinfo',
119
            [],
120
            [],
121
            [
122
                'HTTP_Authorization' => 'Bearer INVALID_ACCESS_TOKEN_FOR_USERINFO',
123
            ]
124
        );
125
    }
126
127
    /**
128
     * @When a client sends a Userinfo request but the access token has not been issued through the authorization endpoint
129
     */
130
    public function aClientSendsAUserinfoRequestButTheAccessTokenHasNotBeenIssuedThroughTheAuthorizationEndpoint()
131
    {
132
        $this->minkContext->getSession()->getDriver()->getClient()->request(
133
            'GET',
134
            'https://oauth2.test/userinfo',
135
            [],
136
            [],
137
            [
138
                'HTTP_Authorization' => 'Bearer ACCESS_TOKEN_ISSUED_THROUGH_TOKEN_ENDPOINT',
139
            ]
140
        );
141
    }
142
143
    /**
144
     * @When A client sends a request to get the keys used by this authorization server
145
     */
146
    public function aClientSendsARequestToGetTheKeysUsedByThisAuthorizationServer()
147
    {
148
        $this->minkContext->getSession()->getDriver()->getClient()->request(
149
            'GET',
150
            'https://oauth2.test/keys/public.jwkset',
151
            [],
152
            [],
153
            []
154
        );
155
    }
156
157
    /**
158
     * @When A client sends a request to get the Session Management iFrame
159
     */
160
    public function aClientSendsARequestToGetTheSessionManagementIframe()
161
    {
162
        $this->minkContext->getSession()->getDriver()->getClient()->request(
163
            'GET',
164
            'https://oauth2.test/session/manager/iframe',
165
            [],
166
            [],
167
            []
168
        );
169
    }
170
171
    /**
172
     * @Given a client send a request to the metadata endpoint
173
     */
174
    public function aClientSendARequestToTheMetadataEndpoint()
175
    {
176
        throw new PendingException();
177
        $this->minkContext->getSession()->getDriver()->getClient()->request(
178
            'GET',
179
            'https://oauth2.test/.well-known/openid-configuration',
180
            [],
181
            [],
182
            []
183
        );
184
    }
185
186
    /**
187
     * @Given A client sends an authorization request with max_age parameter but the user has to authenticate again
188
     */
189
    public function aClientSendsAnAuthorizationRequestWithMaxAgeParameterButTheUserHasToAuthenticateAgain()
190
    {
191
        $this->minkContext->getSession()->getDriver()->getClient()->followRedirects(false);
192
        $this->minkContext->getSession()->getDriver()->getClient()->request(
193
            'GET',
194
            'https://oauth2.test/authorize',
195
            [
196
                'client_id' => 'client1',
197
                'redirect_uri' => 'https://example.com/redirection/callback',
198
                'response_type' => 'code',
199
                'state' => '0123456789',
200
                'max_age' => '60',
201
            ],
202
            [],
203
            []
204
        );
205
    }
206
207
    /**
208
     * @Given A client sends an authorization request with max_age parameter
209
     */
210
    public function aClientSendsAnAuthorizationRequestWithMaxAgeParameter()
211
    {
212
        $this->minkContext->getSession()->getDriver()->getClient()->request(
213
            'GET',
214
            'https://oauth2.test/authorize',
215
            [
216
                'client_id' => 'client1',
217
                'redirect_uri' => 'https://example.com/redirection/callback',
218
                'response_type' => 'code',
219
                'state' => '0123456789',
220
                'max_age' => '3600',
221
            ],
222
            [],
223
            []
224
        );
225
    }
226
227
    /**
228
     * @Given A client sends an authorization request with "prompt=none" parameter but the user has to authenticate again
229
     */
230
    public function aClientSendsAnAuthorizationRequestWithPromptNoneParameterButTheUserHasToAuthenticateAgain()
231
    {
232
        $this->minkContext->getSession()->getDriver()->getClient()->followRedirects(false);
233
        $this->minkContext->getSession()->getDriver()->getClient()->request(
234
            'GET',
235
            'https://oauth2.test/authorize',
236
            [
237
                'client_id' => 'client1',
238
                'redirect_uri' => 'https://example.com/redirection/callback',
239
                'response_type' => 'code',
240
                'state' => '0123456789',
241
                'prompt' => 'none',
242
            ],
243
            [],
244
            []
245
        );
246
    }
247
248
    /**
249
     * @Given A client sends an authorization request with "prompt=none consent" parameter
250
     */
251
    public function aClientSendsAnAuthorizationRequestWithPromptNoneConsentParameter()
252
    {
253
        $this->minkContext->getSession()->getDriver()->getClient()->followRedirects(false);
254
        $this->minkContext->getSession()->getDriver()->getClient()->request(
255
            'GET',
256
            'https://oauth2.test/authorize',
257
            [
258
                'client_id' => 'client1',
259
                'redirect_uri' => 'https://example.com/redirection/callback',
260
                'response_type' => 'code',
261
                'state' => '0123456789',
262
                'prompt' => 'none consent',
263
            ],
264
            [],
265
            []
266
        );
267
    }
268
269
    /**
270
     * @Given A client sends an authorization request with "prompt=login" parameter
271
     */
272
    public function aClientSendsAnAuthorizationRequestWithPromptLoginParameter()
273
    {
274
        $this->minkContext->getSession()->getDriver()->getClient()->followRedirects(false);
275
        $this->minkContext->getSession()->getDriver()->getClient()->request(
276
            'GET',
277
            'https://oauth2.test/authorize',
278
            [
279
                'client_id' => 'client1',
280
                'redirect_uri' => 'https://example.com/redirection/callback',
281
                'response_type' => 'code',
282
                'state' => '0123456789',
283
                'prompt' => 'login',
284
            ],
285
            [],
286
            []
287
        );
288
    }
289
290
    /**
291
     * @Given A client sends an authorization request that was already accepted and saved by the resource owner
292
     */
293
    public function aClientSendsAnAuthorizationRequestThatWasAlreadyAcceptedAndSavedByTheResourceOwner()
294
    {
295
        $this->minkContext->getSession()->getDriver()->getClient()->followRedirects(false);
296
        $this->minkContext->getSession()->getDriver()->getClient()->request(
297
            'GET',
298
            'https://oauth2.test/authorize',
299
            [
300
                'client_id' => 'client1',
301
                'redirect_uri' => 'https://example.com/redirection/callback',
302
                'response_type' => 'code',
303
                'state' => '0123456789',
304
                'scope' => 'openid profile phone address email',
305
            ],
306
            [],
307
            []
308
        );
309
    }
310
311
    /**
312
     * @Given A client sends an authorization request that was already accepted and saved by the resource owner with "prompt=consent"
313
     */
314
    public function aClientSendsAnAuthorizationRequestThatWasAlreadyAcceptedAndSavedByTheResourceOwnerWithPromptConsent()
315
    {
316
        $this->minkContext->getSession()->getDriver()->getClient()->request(
317
            'GET',
318
            'https://oauth2.test/authorize',
319
            [
320
                'client_id' => 'client1',
321
                'redirect_uri' => 'https://example.com/redirection/callback',
322
                'response_type' => 'code',
323
                'state' => '0123456789',
324
                'prompt' => 'consent',
325
                'scope' => 'openid profile phone address email',
326
            ],
327
            [],
328
            []
329
        );
330
    }
331
332
    /**
333
     * @Given A client sends an authorization request with ui_locales parameter and at least one locale is supported
334
     */
335
    public function aClientSendsAnAuthorizationRequestWithUiLocalesParameterAndAtLeastOneLocaleIsSupported()
336
    {
337
        $this->minkContext->getSession()->getDriver()->getClient()->request(
338
            'GET',
339
            'https://oauth2.test/authorize',
340
            [
341
                'client_id' => 'client1',
342
                'redirect_uri' => 'https://example.com/redirection/callback',
343
                'response_type' => 'code',
344
                'state' => '0123456789',
345
                'ui_locales' => 'fr en',
346
            ],
347
            [],
348
            []
349
        );
350
    }
351
352
    /**
353
     * @Then the consent screen should be translated
354
     */
355
    public function theConsentScreenShouldBeTranslated()
356
    {
357
        $content = $this->minkContext->getSession()->getPage()->getContent();
358
359
        Assertion::contains($content, 'a besoin de votre autorisation pour accéder à vos resources.');
360
    }
361
362
    /**
363
     * @Given A client sends an authorization request with ui_locales parameter and none of them is supported
364
     */
365
    public function aClientSendsAnAuthorizationRequestWithUiLocalesParameterAndNoneOfThemIsSupported()
366
    {
367
        $this->minkContext->getSession()->getDriver()->getClient()->request(
368
            'GET',
369
            'https://oauth2.test/authorize',
370
            [
371
                'client_id' => 'client1',
372
                'redirect_uri' => 'https://example.com/redirection/callback',
373
                'response_type' => 'code',
374
                'state' => '0123456789',
375
                'ui_locales' => 'ru de',
376
            ],
377
            [],
378
            []
379
        );
380
    }
381
382
    /**
383
     * @Then the consent screen should not be translated
384
     */
385
    public function theConsentScreenShouldNotBeTranslated()
386
    {
387
        $content = $this->minkContext->getSession()->getPage()->getContent();
388
389
        Assertion::contains($content, 'needs your authorization to get access on your resources.');
390
    }
391
392
    /**
393
     * @When A client sends a valid authorization request with a valid id_token_hint parameter
394
     */
395
    public function aClientSendsAValidAuthorizationRequestWithAValidIdTokenHintParameter()
396
    {
397
        $this->minkContext->getSession()->getDriver()->getClient()->request(
398
            'GET',
399
            'https://oauth2.test/authorize',
400
            [
401
                'client_id' => 'client1',
402
                'redirect_uri' => 'https://example.com/redirection/callback',
403
                'response_type' => 'code',
404
                'state' => '0123456789',
405
                'id_token_hint' => $this->generateValidIdToken(),
406
            ],
407
            [],
408
            []
409
        );
410
    }
411
412
    /**
413
     * @When A client sends a valid authorization request with a valid id_token_hint parameter but the current user does not correspond
414
     */
415
    public function aClientSendsAValidAuthorizationRequestWithAValidIdTokenHintParameterButTheCurrentUserDoesNotCorrespond()
416
    {
417
        $isFollowingRedirects = $this->minkContext->getSession()->getDriver()->getClient()->isFollowingRedirects();
418
        $this->minkContext->getSession()->getDriver()->getClient()->followRedirects(false);
419
        $this->minkContext->getSession()->getDriver()->getClient()->request(
420
            'GET',
421
            'https://oauth2.test/authorize',
422
            [
423
                'client_id' => 'client1',
424
                'redirect_uri' => 'https://example.com/redirection/callback',
425
                'response_type' => 'code',
426
                'state' => '0123456789',
427
                'id_token_hint' => $this->generateValidIdToken(),
428
            ],
429
            [],
430
            []
431
        );
432
        $this->minkContext->getSession()->getDriver()->getClient()->followRedirects($isFollowingRedirects);
433
    }
434
435
    /**
436
     * @When A client sends a valid authorization request with an invalid id_token_hint parameter
437
     */
438
    public function aClientSendsAValidAuthorizationRequestWithAnInvalidIdTokenHintParameter()
439
    {
440
        $isFollowingRedirects = $this->minkContext->getSession()->getDriver()->getClient()->isFollowingRedirects();
441
        $this->minkContext->getSession()->getDriver()->getClient()->followRedirects(false);
442
        $this->minkContext->getSession()->getDriver()->getClient()->request(
443
            'GET',
444
            'https://oauth2.test/authorize',
445
            [
446
                'client_id' => 'client1',
447
                'redirect_uri' => 'https://example.com/redirection/callback',
448
                'response_type' => 'code',
449
                'state' => '0123456789',
450
                'id_token_hint' => 'BAD_VALUE !!!!!!!!!!!!!!!!!!!!!!!!!!',
451
            ],
452
            [],
453
            []
454
        );
455
        $this->minkContext->getSession()->getDriver()->getClient()->followRedirects($isFollowingRedirects);
456
    }
457
458
    /**
459
     * @When A client sends a valid authorization request with a valid id_token_hint parameter but signed with an unsupported algorithm
460
     */
461
    public function aClientSendsAValidAuthorizationRequestWithAValidIdTokenHintParameterButSignedWithAnUnsupportedAlgorithm()
462
    {
463
        $isFollowingRedirects = $this->minkContext->getSession()->getDriver()->getClient()->isFollowingRedirects();
464
        $this->minkContext->getSession()->getDriver()->getClient()->followRedirects(false);
465
        $this->minkContext->getSession()->getDriver()->getClient()->request(
466
            'GET',
467
            'https://oauth2.test/authorize',
468
            [
469
                'client_id' => 'client1',
470
                'redirect_uri' => 'https://example.com/redirection/callback',
471
                'response_type' => 'code',
472
                'state' => '0123456789',
473
                'id_token_hint' => 'eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIiwia2lkIjoiS1JTV3dLcENFODFrb0hTa0ZwbFhBOTFnOTFkM253YTQ1MVdGWnd0enlGSGxDMHl4cjluaU13Ymd1NmFBY21yMkFkZ01GcU1Sd2phWlFXLXdYMURTTEEifQ.eyJuYW1lIjoiSm9obiBEb2UiLCJnaXZlbl9uYW1lIjoiSm9obiIsIm1pZGRsZV9uYW1lIjoiSmFjayIsImZhbWlseV9uYW1lIjoiRG9lIiwibmlja25hbWUiOiJMaXR0bGUgSm9obiIsInByZWZlcnJlZF91c2VybmFtZSI6ImotZCIsInByb2ZpbGUiOiJodHRwczpcL1wvcHJvZmlsZS5kb2UuZnJcL2pvaG5cLyIsInBpY3R1cmUiOiJodHRwczpcL1wvd3d3Lmdvb2dsZS5jb20iLCJ3ZWJzaXRlIjoiaHR0cHM6XC9cL2pvaG4uZG9lLmNvbSIsImdlbmRlciI6Ik0iLCJiaXJ0aGRhdGUiOiIxOTUwLTAxLTAxIiwiem9uZWluZm8iOiJFdXJvcGVcL1BhcmlzIiwibG9jYWxlIjoiZW4iLCJ1cGRhdGVkX2F0IjoxNDg1NDMxMjMyLCJlbWFpbCI6InJvb3RAbG9jYWxob3N0LmNvbSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwicGhvbmVfbnVtYmVyIjoiKzAxMjM0NTY3ODkiLCJwaG9uZV9udW1iZXJfdmVyaWZpZWQiOnRydWUsInN1YiI6IlVncU80U0xjTnVwWUJYekdKNXVuQjR0SWY1UTlabzVHYXU1cDJ2QjJGbGZyQTZ2MU1YS09Ib2JvOS12STU1Q2kiLCJpYXQiOjE0ODk2NjU4MjAsIm5iZiI6MTQ4OTY2NTgyMCwiZXhwIjoxNDg5NjY5NDIwLCJqdGkiOiJBNllYZDM5MkdKSGRTZTl5dHhaNGc4ZUpORjg1c0pRdS13IiwiaXNzIjoiaHR0cHM6XC9cL3d3dy5teS1zZXJ2aWNlLmNvbSJ9.',
474
            ],
475
            [],
476
            []
477
        );
478
        $this->minkContext->getSession()->getDriver()->getClient()->followRedirects($isFollowingRedirects);
479
    }
480
481
    /**
482
     * @When a client that set userinfo algorithm parameters sends a valid Userinfo request
483
     */
484
    public function aClientThatSetUserinfoAlgorithmParametersSendsAValidUserinfoRequest()
485
    {
486
        $this->minkContext->getSession()->getDriver()->getClient()->request(
487
            'GET',
488
            'https://oauth2.test/userinfo',
489
            [],
490
            [],
491
            [
492
                'HTTP_Authorization' => 'Bearer VALID_ACCESS_TOKEN_FOR_SIGNED_USERINFO',
493
            ]
494
        );
495
    }
496
497
    /**
498
     * @return string
499
     */
500
    private function generateValidIdToken(): string
501
    {
502
        $headers = [
503
            'typ' => 'JWT',
504
            'alg' => 'RS256',
505
            'kid' => 'KRSWwKpCE81koHSkFplXA91g91d3nwa451WFZwtzyFHlC0yxr9niMwbgu6aAcmr2AdgMFqMRwjaZQW-wX1DSLA',
506
        ];
507
508
        $payload = [
509
            'name' => 'John Doe',
510
            'given_name' => 'John',
511
            'middle_name' => 'Jack',
512
            'family_name' => 'Doe',
513
            'nickname' => 'Little John',
514
            'preferred_username' => 'j-d',
515
            'profile' => 'https://profile.doe.fr/john/',
516
            'picture' => 'https://www.google.com',
517
            'website' => 'https://john.doe.com',
518
            'gender' => 'M',
519
            'birthdate' => '1950-01-01',
520
            'zoneinfo' => 'Europe/Paris',
521
            'locale' => 'en',
522
            'updated_at' => time() - 10,
523
            'email' => '[email protected]',
524
            'email_verified' => false,
525
            'phone_number' => '+0123456789',
526
            'phone_number_verified' => true,
527
            'sub' => 'UgqO4SLcNupYBXzGJ5unB4tIf5Q9Zo5Gau5p2vB2FlfrA6v1MXKOHobo9-vI55Ci',
528
            'iat' => time(),
529
            'nbf' => time(),
530
            'exp' => time() + 1800,
531
            'jti' => 'A6YXd392GJHdSe9ytxZ4g8eJNF85sJQu-w',
532
            'iss' => 'https://www.my-service.com',
533
        ];
534
535
        $key = $this->getContainer()->get('oauth2_server.grant.id_token.key_set')->selectKey('sig', 'RS256');
536
        Assertion::notNull($key);
537
538
        return $this->getContainer()->get(JWTCreatorInterface::class)->sign($payload, $headers, $key);
539
    }
540
}
541