1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
/* |
6
|
|
|
* The MIT License (MIT) |
7
|
|
|
* |
8
|
|
|
* Copyright (c) 2014-2018 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\Tests\Functional\Grant\JwtBearer; |
15
|
|
|
|
16
|
|
|
use Base64Url\Base64Url; |
17
|
|
|
use Jose\Component\Core\AlgorithmManager; |
18
|
|
|
use Jose\Component\Core\Converter\StandardConverter; |
19
|
|
|
use Jose\Component\Core\JWK; |
20
|
|
|
use Jose\Component\Signature\Algorithm\HS256; |
21
|
|
|
use Jose\Component\Signature\JWSBuilder; |
22
|
|
|
use Jose\Component\Signature\Serializer\CompactSerializer; |
23
|
|
|
use OAuth2Framework\Component\JwtBearerGrant\JwtBearerGrantType; |
24
|
|
|
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; |
25
|
|
|
|
26
|
|
|
/** |
27
|
|
|
* @group Bundle |
28
|
|
|
* @group Functional |
29
|
|
|
* @group Grant |
30
|
|
|
* @group JwtBearer |
31
|
|
|
*/ |
32
|
|
|
class JwtBearerGrantTest extends WebTestCase |
33
|
|
|
{ |
34
|
|
|
/** |
35
|
|
|
* {@inheritdoc} |
36
|
|
|
*/ |
37
|
|
|
protected function setUp() |
38
|
|
|
{ |
39
|
|
|
if (!class_exists(JwtBearerGrantType::class)) { |
40
|
|
|
$this->markTestSkipped('The component "oauth2-framework/jwt-bearer-grant" is not installed.'); |
41
|
|
|
} |
42
|
|
|
} |
43
|
|
|
|
44
|
|
|
/** |
45
|
|
|
* @test |
46
|
|
|
*/ |
47
|
|
|
public function theRequestHasNoGrantType() |
48
|
|
|
{ |
49
|
|
|
$client = static::createClient(); |
50
|
|
|
$client->request('POST', '/token/get', [], [], ['HTTPS' => 'on'], null); |
51
|
|
|
$response = $client->getResponse(); |
52
|
|
|
self::assertEquals(400, $response->getStatusCode()); |
53
|
|
|
self::assertEquals('{"error":"invalid_request","error_description":"The \"grant_type\" parameter is missing."}', $response->getContent()); |
54
|
|
|
} |
55
|
|
|
|
56
|
|
|
/** |
57
|
|
|
* @test |
58
|
|
|
*/ |
59
|
|
|
public function theAssertionIsMissing() |
60
|
|
|
{ |
61
|
|
|
$client = static::createClient(); |
62
|
|
|
$client->request('POST', '/token/get', ['grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer'], [], ['HTTPS' => 'on'], null); |
63
|
|
|
$response = $client->getResponse(); |
64
|
|
|
self::assertEquals(400, $response->getStatusCode()); |
65
|
|
|
self::assertEquals('{"error":"invalid_request","error_description":"Missing grant type parameter(s): assertion."}', $response->getContent()); |
66
|
|
|
} |
67
|
|
|
|
68
|
|
|
/** |
69
|
|
|
* @test |
70
|
|
|
*/ |
71
|
|
|
public function theAssertionIsInvalid() |
72
|
|
|
{ |
73
|
|
|
$client = static::createClient(); |
74
|
|
|
$client->request('POST', '/token/get', ['grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', 'assertion' => 'FOO'], [], ['HTTPS' => 'on'], null); |
75
|
|
|
$response = $client->getResponse(); |
76
|
|
|
self::assertEquals(400, $response->getStatusCode()); |
77
|
|
|
self::assertEquals('{"error":"invalid_request","error_description":"Unsupported input"}', $response->getContent()); |
78
|
|
|
} |
79
|
|
|
|
80
|
|
|
/** |
81
|
|
|
* @test |
82
|
|
|
*/ |
83
|
|
|
public function theAssertionDoesNotContainTheMandatoryClaims() |
84
|
|
|
{ |
85
|
|
|
$client = static::createClient(); |
86
|
|
|
$assertion = $this->createAnAssertionWithoutClaim(); |
87
|
|
|
$client->request('POST', '/token/get', ['grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', 'assertion' => $assertion], [], ['HTTPS' => 'on'], null); |
88
|
|
|
$response = $client->getResponse(); |
89
|
|
|
self::assertEquals(400, $response->getStatusCode()); |
90
|
|
|
self::assertEquals('{"error":"invalid_request","error_description":"The following claim(s) is/are mandatory: \"iss, sub, aud, exp\"."}', $response->getContent()); |
91
|
|
|
} |
92
|
|
|
|
93
|
|
|
/** |
94
|
|
|
* @test |
95
|
|
|
*/ |
96
|
|
|
public function theAssertionDoesNotContainTheSubjectClaims() |
97
|
|
|
{ |
98
|
|
|
$client = static::createClient(); |
99
|
|
|
$assertion = $this->createAnAssertionWithoutSubject(); |
100
|
|
|
$client->request('POST', '/token/get', ['grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', 'assertion' => $assertion], [], ['HTTPS' => 'on'], null); |
101
|
|
|
$response = $client->getResponse(); |
102
|
|
|
self::assertEquals(400, $response->getStatusCode()); |
103
|
|
|
self::assertEquals('{"error":"invalid_request","error_description":"The following claim(s) is/are mandatory: \"sub, aud, exp\"."}', $response->getContent()); |
104
|
|
|
} |
105
|
|
|
|
106
|
|
|
/** |
107
|
|
|
* @test |
108
|
|
|
*/ |
109
|
|
|
public function theAssertionDoesNotContainTheAudienceClaims() |
110
|
|
|
{ |
111
|
|
|
$client = static::createClient(); |
112
|
|
|
$assertion = $this->createAnAssertionWithoutAudience(); |
113
|
|
|
$client->request('POST', '/token/get', ['grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', 'assertion' => $assertion], [], ['HTTPS' => 'on'], null); |
114
|
|
|
$response = $client->getResponse(); |
115
|
|
|
self::assertEquals(400, $response->getStatusCode()); |
116
|
|
|
self::assertEquals('{"error":"invalid_request","error_description":"The following claim(s) is/are mandatory: \"aud, exp\"."}', $response->getContent()); |
117
|
|
|
} |
118
|
|
|
|
119
|
|
|
/** |
120
|
|
|
* @test |
121
|
|
|
*/ |
122
|
|
|
public function theAssertionDoesNotContainTheExpirationTimeClaims() |
123
|
|
|
{ |
124
|
|
|
$client = static::createClient(); |
125
|
|
|
$assertion = $this->createAnAssertionWithoutExpirationTime(); |
126
|
|
|
$client->request('POST', '/token/get', ['grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', 'assertion' => $assertion], [], ['HTTPS' => 'on'], null); |
127
|
|
|
$response = $client->getResponse(); |
128
|
|
|
self::assertEquals(400, $response->getStatusCode()); |
129
|
|
|
self::assertEquals('{"error":"invalid_request","error_description":"The following claim(s) is/are mandatory: \"exp\"."}', $response->getContent()); |
130
|
|
|
} |
131
|
|
|
|
132
|
|
|
/** |
133
|
|
|
* @test |
134
|
|
|
*/ |
135
|
|
|
public function theAssertionIsValid() |
136
|
|
|
{ |
137
|
|
|
$client = static::createClient(); |
138
|
|
|
$assertion = $this->createAValidAssertion(); |
139
|
|
|
$client->request('POST', '/token/get', ['grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', 'assertion' => $assertion], [], ['HTTPS' => 'on'], null); |
140
|
|
|
$response = $client->getResponse(); |
141
|
|
|
self::assertEquals(200, $response->getStatusCode()); |
142
|
|
|
self::assertRegexp('/\{"token_type"\:"Bearer","access_token"\:"[0-9a-zA-Z-_]+","expires_in":[0-9]{4}\}/', $response->getContent()); |
143
|
|
|
} |
144
|
|
|
|
145
|
|
|
/** |
146
|
|
|
* @return string |
147
|
|
|
*/ |
148
|
|
|
private function createAnAssertionWithoutClaim(): string |
149
|
|
|
{ |
150
|
|
|
$jwk = JWK::create([ |
151
|
|
|
'kty' => 'oct', |
152
|
|
|
'use' => 'sig', |
153
|
|
|
'k' => Base64Url::encode('secret'), |
154
|
|
|
]); |
155
|
|
|
$claims = []; |
156
|
|
|
|
157
|
|
|
return $this->sign($claims, $jwk); |
158
|
|
|
//$jwk = JWK::createFromJson('{"kty":"RSA","n":"sLjaCStJYRr_y7_3GLlDb4bnGJ8XirSdFboYmvA38NXJ6PhIIjr-sFzfwlcpxZxz6zzjXkDFs3AcUOvC3_KRT5tn4XBOHcR6ABrT65dZTe_qalEpYeQG4oxevc01vmD_dD6Ho2O69amT4gscus2pvszFPdraMYybH24aQFztVtc","e":"AQAB","d":"By-tJhxNgpZfeoCW4rl95YYd1aF6iphnnt-PapWEINYAvOmDvWiavL86FiQHPdLr38_9CvMlVvOjIyNDLGonwHynPxAzUsT7M891N9D0cSCv9DlV3uqRVtdqF4MtWtpU5JWJ9q6auL1UPx2tJhOygu9tJ7w0bTGFwrUdb8PSnlE","p":"3p-6HWbX9YcSkeksJXW3_Y2cfZgRCUXH2or1dIidmscb4VVtTUwb-8gGzUDEq4iS_5pgLARl3O4lOHK0n6Qbrw","q":"yzdrGWwgaWqK6e9VFv3NXGeq1TEKHLkXjF7J24XWKm9lSmlssPRv0NwMPVp_CJ39BrLfFtpFr_fh0oG1sVZ5WQ","dp":"UQ6rP0VQ4G77zfCuSD1ibol_LyONIGkt6V6rHHEZoV9ZwWPPVlOd5MDh6R3p_eLOUw6scZpwVE7JcpIhPfcMtQ","dq":"Jg8g_cfkYhnUHm_2bbHm7jF0Ky1eCXcY0-9Eutpb--KVA9SuyI1fC6zKlgsG06RTKRgC9BK5DnXMU1J7ptTdMQ","qi":"17kC87NLUV6z-c-wtmbNqAkDbKmwpb2RMsGUQmhEPJwnWuwEKZpSQz776SUVwoc0xiQ8DpvU_FypflIlm6fq9w"}'); |
|
|
|
|
159
|
|
|
} |
160
|
|
|
|
161
|
|
|
/** |
162
|
|
|
* @return string |
163
|
|
|
*/ |
164
|
|
|
private function createAnAssertionWithoutSubject(): string |
165
|
|
|
{ |
166
|
|
|
$jwk = JWK::create([ |
167
|
|
|
'kty' => 'oct', |
168
|
|
|
'use' => 'sig', |
169
|
|
|
'k' => Base64Url::encode('secret'), |
170
|
|
|
]); |
171
|
|
|
$claims = ['iss' => 'CLIENT_ID_4']; |
172
|
|
|
|
173
|
|
|
return $this->sign($claims, $jwk); |
174
|
|
|
} |
175
|
|
|
|
176
|
|
|
/** |
177
|
|
|
* @return string |
178
|
|
|
*/ |
179
|
|
|
private function createAnAssertionWithoutAudience(): string |
180
|
|
|
{ |
181
|
|
|
$jwk = JWK::create([ |
182
|
|
|
'kty' => 'oct', |
183
|
|
|
'use' => 'sig', |
184
|
|
|
'k' => Base64Url::encode('secret'), |
185
|
|
|
]); |
186
|
|
|
$claims = ['iss' => 'CLIENT_ID_4', 'sub' => 'CLIENT_ID_4']; |
187
|
|
|
|
188
|
|
|
return $this->sign($claims, $jwk); |
189
|
|
|
} |
190
|
|
|
|
191
|
|
|
/** |
192
|
|
|
* @return string |
193
|
|
|
*/ |
194
|
|
|
private function createAnAssertionWithoutExpirationTime(): string |
195
|
|
|
{ |
196
|
|
|
$jwk = JWK::create([ |
197
|
|
|
'kty' => 'oct', |
198
|
|
|
'use' => 'sig', |
199
|
|
|
'k' => Base64Url::encode('secret'), |
200
|
|
|
]); |
201
|
|
|
$claims = [ |
202
|
|
|
'iss' => 'CLIENT_ID_4', |
203
|
|
|
'sub' => 'CLIENT_ID_4', |
204
|
|
|
'aud' => 'https://oauth2.test/' |
205
|
|
|
]; |
206
|
|
|
|
207
|
|
|
return $this->sign($claims, $jwk); |
208
|
|
|
} |
209
|
|
|
|
210
|
|
|
/** |
211
|
|
|
* @return string |
212
|
|
|
*/ |
213
|
|
|
private function createAValidAssertion(): string |
214
|
|
|
{ |
215
|
|
|
$jwk = JWK::create([ |
216
|
|
|
'kty' => 'oct', |
217
|
|
|
'use' => 'sig', |
218
|
|
|
'k' => Base64Url::encode('secret'), |
219
|
|
|
]); |
220
|
|
|
$claims = [ |
221
|
|
|
'iss' => 'CLIENT_ID_4', |
222
|
|
|
'sub' => 'CLIENT_ID_4', |
223
|
|
|
'aud' => 'https://oauth2.test/', |
224
|
|
|
'exp' => time()+3600, |
225
|
|
|
]; |
226
|
|
|
|
227
|
|
|
return $this->sign($claims, $jwk); |
228
|
|
|
} |
229
|
|
|
|
230
|
|
|
/** |
231
|
|
|
* @param array $claims |
232
|
|
|
* @param JWK $jwk |
233
|
|
|
* |
234
|
|
|
* @return string |
235
|
|
|
*/ |
236
|
|
|
private function sign(array $claims, JWK $jwk): string |
237
|
|
|
{ |
238
|
|
|
$jsonConverter = new StandardConverter(); |
239
|
|
|
$jwsBuilder = new JWSBuilder( |
240
|
|
|
$jsonConverter, |
241
|
|
|
AlgorithmManager::create([new HS256()]) |
242
|
|
|
); |
243
|
|
|
$payload = $jsonConverter->encode($claims); |
244
|
|
|
$jws = $jwsBuilder |
245
|
|
|
->create() |
246
|
|
|
->withPayload($payload) |
247
|
|
|
->addSignature($jwk, ['alg' => 'HS256']) |
248
|
|
|
->build(); |
249
|
|
|
$token = (new CompactSerializer($jsonConverter))->serialize($jws); |
250
|
|
|
|
251
|
|
|
return $token; |
252
|
|
|
} |
253
|
|
|
} |
254
|
|
|
|
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.
The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.
This check looks for comments that seem to be mostly valid code and reports them.