blob: e60023b3ca08324cd39236720c4226de8d45427b [file] [log] [blame]
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001<?php
2
3namespace OAuth2\GrantType;
4
5use OAuth2\Storage\Bootstrap;
6use OAuth2\Server;
7use OAuth2\Request\TestRequest;
8use OAuth2\Response;
9use OAuth2\Encryption\Jwt;
10use PHPUnit\Framework\TestCase;
11
12class JwtBearerTest extends TestCase
13{
14 private $privateKey;
15
16 public function setUp()
17 {
18 $this->privateKey = <<<EOD
19-----BEGIN RSA PRIVATE KEY-----
20MIICXAIBAAKBgQC5/SxVlE8gnpFqCxgl2wjhzY7ucEi00s0kUg3xp7lVEvgLgYcA
21nHiWp+gtSjOFfH2zsvpiWm6Lz5f743j/FEzHIO1owR0p4d9pOaJK07d01+RzoQLO
22IQAgXrr4T1CCWUesncwwPBVCyy2Mw3Nmhmr9MrF8UlvdRKBxriRnlP3qJQIDAQAB
23AoGAVgJJVU4fhYMu1e5JfYAcTGfF+Gf+h3iQm4JCpoUcxMXf5VpB9ztk3K7LRN5y
24kwFuFALpnUAarRcUPs0D8FoP4qBluKksbAtgHkO7bMSH9emN+mH4le4qpFlR7+P1
253fLE2Y19IBwPwEfClC+TpJvuog6xqUYGPlg6XLq/MxQUB4ECQQDgovP1v+ONSeGS
26R+NgJTR47noTkQT3M2izlce/OG7a+O0yw6BOZjNXqH2wx3DshqMcPUFrTjibIClP
27l/tEQ3ShAkEA0/TdBYDtXpNNjqg0R9GVH2pw7Kh68ne6mZTuj0kCgFYpUF6L6iMm
28zXamIJ51rTDsTyKTAZ1JuAhAsK/M2BbDBQJAKQ5fXEkIA+i+64dsDUR/hKLBeRYG
29PFAPENONQGvGBwt7/s02XV3cgGbxIgAxqWkqIp0neb9AJUoJgtyaNe3GQQJANoL4
30QQ0af0NVJAZgg8QEHTNL3aGrFSbzx8IE5Lb7PLRsJa5bP5lQxnDoYuU+EI/Phr62
31niisp/b/ZDGidkTMXQJBALeRsH1I+LmICAvWXpLKa9Gv0zGCwkuIJLiUbV9c6CVh
32suocCAteQwL5iW2gA4AnYr5OGeHFsEl7NCQcwfPZpJ0=
33-----END RSA PRIVATE KEY-----
34EOD;
35 }
36
37 public function testMalformedJWT()
38 {
39 $server = $this->getTestServer();
40 $request = TestRequest::createPost(array(
41 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // valid grant type
42 ));
43
44 //Get the jwt and break it
45 $jwt = $this->getJWT();
46 $jwt = substr_replace($jwt, 'broken', 3, 6);
47
48 $request->request['assertion'] = $jwt;
49
50 $server->grantAccessToken($request, $response = new Response());
51
52 $this->assertEquals($response->getStatusCode(), 400);
53 $this->assertEquals($response->getParameter('error'), 'invalid_request');
54 $this->assertEquals($response->getParameter('error_description'), 'JWT is malformed');
55 }
56
57 public function testBrokenSignature()
58 {
59 $server = $this->getTestServer();
60 $request = TestRequest::createPost(array(
61 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // valid grant type
62 ));
63
64 //Get the jwt and break signature
65 $jwt = $this->getJWT() . 'notSupposeToBeHere';
66 $request->request['assertion'] = $jwt;
67
68 $server->grantAccessToken($request, $response = new Response());
69
70 $this->assertEquals($response->getStatusCode(), 400);
71 $this->assertEquals($response->getParameter('error'), 'invalid_grant');
72 $this->assertEquals($response->getParameter('error_description'), 'JWT failed signature verification');
73 }
74
75 public function testExpiredJWT()
76 {
77 $server = $this->getTestServer();
78 $request = TestRequest::createPost(array(
79 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // valid grant type
80 ));
81
82 //Get an expired JWT
83 $jwt = $this->getJWT(1234);
84 $request->request['assertion'] = $jwt;
85
86 $server->grantAccessToken($request, $response = new Response());
87
88 $this->assertEquals($response->getStatusCode(), 400);
89 $this->assertEquals($response->getParameter('error'), 'invalid_grant');
90 $this->assertEquals($response->getParameter('error_description'), 'JWT has expired');
91 }
92
93 public function testBadExp()
94 {
95 $server = $this->getTestServer();
96 $request = TestRequest::createPost(array(
97 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // valid grant type
98 ));
99
100 //Get an expired JWT
101 $jwt = $this->getJWT('badtimestamp');
102 $request->request['assertion'] = $jwt;
103
104 $server->grantAccessToken($request, $response = new Response());
105
106 $this->assertEquals($response->getStatusCode(), 400);
107 $this->assertEquals($response->getParameter('error'), 'invalid_grant');
108 $this->assertEquals($response->getParameter('error_description'), 'Expiration (exp) time must be a unix time stamp');
109 }
110
111 public function testNoAssert()
112 {
113 $server = $this->getTestServer();
114 $request = TestRequest::createPost(array(
115 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // valid grant type
116 ));
117
118 //Do not pass the assert (JWT)
119
120 $server->grantAccessToken($request, $response = new Response());
121
122 $this->assertEquals($response->getStatusCode(), 400);
123 $this->assertEquals($response->getParameter('error'), 'invalid_request');
124 $this->assertEquals($response->getParameter('error_description'), 'Missing parameters: "assertion" required');
125 }
126
127 public function testNotBefore()
128 {
129 $server = $this->getTestServer();
130 $request = TestRequest::createPost(array(
131 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // valid grant type
132 ));
133
134 //Get a future NBF
135 $jwt = $this->getJWT(null, time() + 10000);
136 $request->request['assertion'] = $jwt;
137
138 $server->grantAccessToken($request, $response = new Response());
139
140 $this->assertEquals($response->getStatusCode(), 400);
141 $this->assertEquals($response->getParameter('error'), 'invalid_grant');
142 $this->assertEquals($response->getParameter('error_description'), 'JWT cannot be used before the Not Before (nbf) time');
143 }
144
145 public function testBadNotBefore()
146 {
147 $server = $this->getTestServer();
148 $request = TestRequest::createPost(array(
149 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // valid grant type
150 ));
151
152 //Get a non timestamp nbf
153 $jwt = $this->getJWT(null, 'notatimestamp');
154 $request->request['assertion'] = $jwt;
155
156 $server->grantAccessToken($request, $response = new Response());
157
158 $this->assertEquals($response->getStatusCode(), 400);
159 $this->assertEquals($response->getParameter('error'), 'invalid_grant');
160 $this->assertEquals($response->getParameter('error_description'), 'Not Before (nbf) time must be a unix time stamp');
161 }
162
163 public function testNonMatchingAudience()
164 {
165 $server = $this->getTestServer('http://google.com/oauth/o/auth');
166 $request = TestRequest::createPost(array(
167 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // valid grant type
168 'assertion' => $this->getJWT(),
169 ));
170
171 $server->grantAccessToken($request, $response = new Response());
172
173 $this->assertEquals($response->getStatusCode(), 400);
174 $this->assertEquals($response->getParameter('error'), 'invalid_grant');
175 $this->assertEquals($response->getParameter('error_description'), 'Invalid audience (aud)');
176 }
177
178 public function testBadClientID()
179 {
180 $server = $this->getTestServer();
181 $request = TestRequest::createPost(array(
182 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // valid grant type
183 'assertion' => $this->getJWT(null, null, null, 'bad_client_id'),
184 ));
185
186 $server->grantAccessToken($request, $response = new Response());
187
188 $this->assertEquals($response->getStatusCode(), 400);
189 $this->assertEquals($response->getParameter('error'), 'invalid_grant');
190 $this->assertEquals($response->getParameter('error_description'), 'Invalid issuer (iss) or subject (sub) provided');
191 }
192
193 public function testBadSubject()
194 {
195 $server = $this->getTestServer();
196 $request = TestRequest::createPost(array(
197 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // valid grant type
198 'assertion' => $this->getJWT(null, null, 'anotheruser@ourdomain,com'),
199 ));
200
201 $server->grantAccessToken($request, $response = new Response());
202
203 $this->assertEquals($response->getStatusCode(), 400);
204 $this->assertEquals($response->getParameter('error'), 'invalid_grant');
205 $this->assertEquals($response->getParameter('error_description'), 'Invalid issuer (iss) or subject (sub) provided');
206 }
207
208 public function testMissingKey()
209 {
210 $server = $this->getTestServer();
211 $request = TestRequest::createPost(array(
212 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // valid grant type
213 'assertion' => $this->getJWT(null, null, null, 'Missing Key Cli,nt'),
214 ));
215
216 $server->grantAccessToken($request, $response = new Response());
217
218 $this->assertEquals($response->getStatusCode(), 400);
219 $this->assertEquals($response->getParameter('error'), 'invalid_grant');
220 $this->assertEquals($response->getParameter('error_description'), 'Invalid issuer (iss) or subject (sub) provided');
221 }
222
223 public function testValidJwt()
224 {
225 $server = $this->getTestServer();
226 $request = TestRequest::createPost(array(
227 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // valid grant type
228 'assertion' => $this->getJWT(), // valid assertion
229 ));
230
231 $token = $server->grantAccessToken($request, new Response());
232 $this->assertNotNull($token);
233 $this->assertArrayHasKey('access_token', $token);
234 }
235
236 public function testValidJwtWithScope()
237 {
238 $server = $this->getTestServer();
239 $request = TestRequest::createPost(array(
240 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // valid grant type
241 'assertion' => $this->getJWT(null, null, null, 'Test Client ID'), // valid assertion
242 'scope' => 'scope1', // valid scope
243 ));
244 $token = $server->grantAccessToken($request, new Response());
245
246 $this->assertNotNull($token);
247 $this->assertArrayHasKey('access_token', $token);
248 $this->assertArrayHasKey('scope', $token);
249 $this->assertEquals($token['scope'], 'scope1');
250 }
251
252 public function testValidJwtInvalidScope()
253 {
254 $server = $this->getTestServer();
255 $request = TestRequest::createPost(array(
256 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // valid grant type
257 'assertion' => $this->getJWT(null, null, null, 'Test Client ID'), // valid assertion
258 'scope' => 'invalid-scope', // invalid scope
259 ));
260 $token = $server->grantAccessToken($request, $response = new Response());
261
262 $this->assertEquals($response->getStatusCode(), 400);
263 $this->assertEquals($response->getParameter('error'), 'invalid_scope');
264 $this->assertEquals($response->getParameter('error_description'), 'An unsupported scope was requested');
265 }
266
267 public function testValidJti()
268 {
269 $server = $this->getTestServer();
270 $request = TestRequest::createPost(array(
271 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // valid grant type
272 'assertion' => $this->getJWT(null, null, 'testuser@ourdomain.com', 'Test Client ID', 'unused_jti'), // valid assertion with invalid scope
273 ));
274 $token = $server->grantAccessToken($request, $response = new Response());
275
276 $this->assertNotNull($token);
277 $this->assertArrayHasKey('access_token', $token);
278 }
279
280 public function testInvalidJti()
281 {
282 $server = $this->getTestServer();
283 $request = TestRequest::createPost(array(
284 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // valid grant type
285 'assertion' => $this->getJWT(99999999900, null, 'testuser@ourdomain.com', 'Test Client ID', 'used_jti'), // valid assertion with invalid scope
286 ));
287 $token = $server->grantAccessToken($request, $response = new Response());
288
289 $this->assertEquals($response->getStatusCode(), 400);
290 $this->assertEquals($response->getParameter('error'), 'invalid_grant');
291 $this->assertEquals($response->getParameter('error_description'), 'JSON Token Identifier (jti) has already been used');
292 }
293
294 public function testJtiReplayAttack()
295 {
296 $server = $this->getTestServer();
297 $request = TestRequest::createPost(array(
298 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // valid grant type
299 'assertion' => $this->getJWT(99999999900, null, 'testuser@ourdomain.com', 'Test Client ID', 'totally_new_jti'), // valid assertion with invalid scope
300 ));
301 $token = $server->grantAccessToken($request, $response = new Response());
302
303 $this->assertNotNull($token);
304 $this->assertArrayHasKey('access_token', $token);
305
306 //Replay the same request
307 $token = $server->grantAccessToken($request, $response = new Response());
308
309 $this->assertEquals($response->getStatusCode(), 400);
310 $this->assertEquals($response->getParameter('error'), 'invalid_grant');
311 $this->assertEquals($response->getParameter('error_description'), 'JSON Token Identifier (jti) has already been used');
312 }
313
314 /**
315 * Generates a JWT
316 * @param $exp The expiration date. If the current time is greater than the exp, the JWT is invalid.
317 * @param $nbf The "not before" time. If the current time is less than the nbf, the JWT is invalid.
318 * @param $sub The subject we are acting on behalf of. This could be the email address of the user in the system.
319 * @param $iss The issuer, usually the client_id.
320 * @return string
321 */
322 private function getJWT($exp = null, $nbf = null, $sub = null, $iss = 'Test Client ID', $jti = null)
323 {
324 if (!$exp) {
325 $exp = time() + 1000;
326 }
327
328 if (!$sub) {
329 $sub = "testuser@ourdomain.com";
330 }
331
332 $params = array(
333 'iss' => $iss,
334 'exp' => $exp,
335 'iat' => time(),
336 'sub' => $sub,
337 'aud' => 'http://myapp.com/oauth/auth',
338 );
339
340 if ($nbf) {
341 $params['nbf'] = $nbf;
342 }
343
344 if ($jti) {
345 $params['jti'] = $jti;
346 }
347
348 $jwtUtil = new Jwt();
349
350 return $jwtUtil->encode($params, $this->privateKey, 'RS256');
351 }
352
353 private function getTestServer($audience = 'http://myapp.com/oauth/auth')
354 {
355 $storage = Bootstrap::getInstance()->getMemoryStorage();
356 $server = new Server($storage);
357 $server->addGrantType(new JwtBearer($storage, $audience, new Jwt()));
358
359 return $server;
360 }
361}