1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.syncope.fit.core;
20
21 import static org.junit.jupiter.api.Assertions.assertEquals;
22 import static org.junit.jupiter.api.Assertions.assertFalse;
23 import static org.junit.jupiter.api.Assertions.assertNotEquals;
24 import static org.junit.jupiter.api.Assertions.assertNotNull;
25 import static org.junit.jupiter.api.Assertions.assertTrue;
26 import static org.junit.jupiter.api.Assertions.fail;
27 import static org.junit.jupiter.api.Assumptions.assumeFalse;
28
29 import com.nimbusds.jose.JOSEException;
30 import com.nimbusds.jose.JWSAlgorithm;
31 import com.nimbusds.jose.JWSHeader;
32 import com.nimbusds.jose.KeyLengthException;
33 import com.nimbusds.jose.crypto.MACSigner;
34 import com.nimbusds.jwt.JWT;
35 import com.nimbusds.jwt.JWTClaimsSet;
36 import com.nimbusds.jwt.PlainJWT;
37 import com.nimbusds.jwt.SignedJWT;
38 import java.security.AccessControlException;
39 import java.security.NoSuchAlgorithmException;
40 import java.security.spec.InvalidKeySpecException;
41 import java.text.ParseException;
42 import java.time.OffsetDateTime;
43 import java.time.temporal.ChronoUnit;
44 import java.util.Calendar;
45 import java.util.Date;
46 import java.util.List;
47 import java.util.Map;
48 import java.util.Set;
49 import java.util.UUID;
50 import javax.ws.rs.core.Response;
51 import org.apache.commons.lang3.RandomStringUtils;
52 import org.apache.commons.lang3.tuple.Triple;
53 import org.apache.syncope.client.lib.SyncopeClient;
54 import org.apache.syncope.common.lib.SyncopeConstants;
55 import org.apache.syncope.common.lib.request.UserCR;
56 import org.apache.syncope.common.lib.to.UserTO;
57 import org.apache.syncope.common.rest.api.RESTHeaders;
58 import org.apache.syncope.common.rest.api.service.AccessTokenService;
59 import org.apache.syncope.common.rest.api.service.UserSelfService;
60 import org.apache.syncope.core.spring.security.jws.AccessTokenJWSSigner;
61 import org.apache.syncope.core.spring.security.jws.AccessTokenJWSVerifier;
62 import org.apache.syncope.fit.AbstractITCase;
63 import org.apache.syncope.fit.core.reference.CustomJWTSSOProvider;
64 import org.junit.jupiter.api.BeforeAll;
65 import org.junit.jupiter.api.Test;
66
67
68
69
70 public class JWTITCase extends AbstractITCase {
71
72 private static AccessTokenJWSSigner JWS_SIGNER;
73
74 private static AccessTokenJWSVerifier JWS_VERIFIER;
75
76 @BeforeAll
77 public static void setupVerifier() throws Exception {
78 JWS_SIGNER = new AccessTokenJWSSigner(JWS_ALGORITHM, JWS_KEY);
79 JWS_VERIFIER = new AccessTokenJWSVerifier(JWS_ALGORITHM, JWS_KEY);
80 }
81
82 @Test
83 public void getJWTToken() throws ParseException, JOSEException {
84
85 SyncopeClient localClient = CLIENT_FACTORY.create(ADMIN_UNAME, ADMIN_PWD);
86 AccessTokenService accessTokenService = localClient.getService(AccessTokenService.class);
87
88 Response response = accessTokenService.login();
89 String token = response.getHeaderString(RESTHeaders.TOKEN);
90 assertNotNull(token);
91 String expiration = response.getHeaderString(RESTHeaders.TOKEN_EXPIRE);
92 assertNotNull(expiration);
93
94
95 SignedJWT jwt = SignedJWT.parse(token);
96 jwt.verify(JWS_VERIFIER);
97 assertTrue(jwt.verify(JWS_VERIFIER));
98
99 Date now = new Date();
100
101
102 Date tokenDate = jwt.getJWTClaimsSet().getExpirationTime();
103 assertNotNull(tokenDate);
104
105 Date parsedDate = new Date(OffsetDateTime.parse(expiration).
106 truncatedTo(ChronoUnit.SECONDS).toInstant().toEpochMilli());
107
108 assertEquals(tokenDate, parsedDate);
109 assertTrue(parsedDate.after(now));
110
111
112 Date issueTime = jwt.getJWTClaimsSet().getIssueTime();
113 assertNotNull(issueTime);
114 assertTrue(issueTime.before(now));
115
116
117 assertEquals(ADMIN_UNAME, jwt.getJWTClaimsSet().getSubject());
118 assertEquals(JWT_ISSUER, jwt.getJWTClaimsSet().getIssuer());
119
120
121 Date notBeforeTime = jwt.getJWTClaimsSet().getNotBeforeTime();
122 assertNotNull(notBeforeTime);
123 assertTrue(notBeforeTime.before(now));
124 }
125
126 @Test
127 public void queryUsingToken() throws ParseException {
128
129 SyncopeClient localClient = CLIENT_FACTORY.create(ADMIN_UNAME, ADMIN_PWD);
130 AccessTokenService accessTokenService = localClient.getService(AccessTokenService.class);
131
132 Response response = accessTokenService.login();
133 String token = response.getHeaderString(RESTHeaders.TOKEN);
134 assertNotNull(token);
135
136
137 SyncopeClient jwtClient = CLIENT_FACTORY.create(token);
138 UserSelfService jwtUserSelfService = jwtClient.getService(UserSelfService.class);
139 jwtUserSelfService.read();
140
141
142 jwtClient = CLIENT_FACTORY.create(token + "xyz");
143 jwtUserSelfService = jwtClient.getService(UserSelfService.class);
144 try {
145 jwtUserSelfService.read();
146 fail("Failure expected on a modified token");
147 } catch (AccessControlException e) {
148 assertEquals("Invalid signature found in JWT", e.getMessage());
149 }
150 }
151
152 @Test
153 public void tokenValidation() throws ParseException, JOSEException {
154
155 SyncopeClient localClient = CLIENT_FACTORY.create(ADMIN_UNAME, ADMIN_PWD);
156 AccessTokenService accessTokenService = localClient.getService(AccessTokenService.class);
157
158 Response response = accessTokenService.login();
159 String token = response.getHeaderString(RESTHeaders.TOKEN);
160 assertNotNull(token);
161 SignedJWT jwt = SignedJWT.parse(token);
162 String tokenId = jwt.getJWTClaimsSet().getJWTID();
163
164
165 Date currentTime = new Date();
166
167 Calendar expiration = Calendar.getInstance();
168 expiration.setTime(currentTime);
169 expiration.add(Calendar.MINUTE, 5);
170
171 JWTClaimsSet.Builder claimsSet = new JWTClaimsSet.Builder().
172 jwtID(tokenId).
173 subject(ADMIN_UNAME).
174 issueTime(currentTime).
175 issuer(JWT_ISSUER).
176 expirationTime(expiration.getTime()).
177 notBeforeTime(currentTime);
178 jwt = new SignedJWT(new JWSHeader(JWS_SIGNER.getJwsAlgorithm()), claimsSet.build());
179 jwt.sign(JWS_SIGNER);
180 String signed = jwt.serialize();
181
182 SyncopeClient jwtClient = CLIENT_FACTORY.create(signed);
183 UserSelfService jwtUserSelfService = jwtClient.getService(UserSelfService.class);
184 jwtUserSelfService.read();
185 }
186
187 @Test
188 public void invalidIssuer() throws ParseException, JOSEException {
189
190 SyncopeClient localClient = CLIENT_FACTORY.create(ADMIN_UNAME, ADMIN_PWD);
191 AccessTokenService accessTokenService = localClient.getService(AccessTokenService.class);
192
193 Response response = accessTokenService.login();
194 String token = response.getHeaderString(RESTHeaders.TOKEN);
195 SignedJWT jwt = SignedJWT.parse(token);
196 String tokenId = jwt.getJWTClaimsSet().getJWTID();
197
198
199 Date currentTime = new Date();
200
201 Calendar expiration = Calendar.getInstance();
202 expiration.setTime(currentTime);
203 expiration.add(Calendar.MINUTE, 5);
204
205 JWTClaimsSet.Builder claimsSet = new JWTClaimsSet.Builder().
206 jwtID(tokenId).
207 subject(ADMIN_UNAME).
208 issueTime(currentTime).
209 issuer("UnknownIssuer").
210 expirationTime(expiration.getTime()).
211 notBeforeTime(currentTime);
212 jwt = new SignedJWT(new JWSHeader(JWS_SIGNER.getJwsAlgorithm()), claimsSet.build());
213 jwt.sign(JWS_SIGNER);
214 String signed = jwt.serialize();
215
216 SyncopeClient jwtClient = CLIENT_FACTORY.create(signed);
217 UserSelfService jwtUserSelfService = jwtClient.getService(UserSelfService.class);
218 try {
219 jwtUserSelfService.read();
220 fail("Failure expected on an invalid issuer");
221 } catch (AccessControlException e) {
222
223 }
224 }
225
226 @Test
227 public void expiredToken() throws ParseException, JOSEException {
228
229 SyncopeClient localClient = CLIENT_FACTORY.create(ADMIN_UNAME, ADMIN_PWD);
230 AccessTokenService accessTokenService = localClient.getService(AccessTokenService.class);
231
232 Response response = accessTokenService.login();
233 String token = response.getHeaderString(RESTHeaders.TOKEN);
234 assertNotNull(token);
235 SignedJWT jwt = SignedJWT.parse(token);
236 String tokenId = jwt.getJWTClaimsSet().getJWTID();
237
238
239 Date currentTime = new Date();
240
241 JWTClaimsSet.Builder claimsSet = new JWTClaimsSet.Builder().
242 jwtID(tokenId).
243 subject(ADMIN_UNAME).
244 issueTime(currentTime).
245 issuer(JWT_ISSUER).
246 expirationTime(new Date(currentTime.getTime() - 5000L)).
247 notBeforeTime(currentTime);
248 jwt = new SignedJWT(new JWSHeader(JWS_SIGNER.getJwsAlgorithm()), claimsSet.build());
249 jwt.sign(JWS_SIGNER);
250 String signed = jwt.serialize();
251
252 SyncopeClient jwtClient = CLIENT_FACTORY.create(signed);
253 UserSelfService jwtUserSelfService = jwtClient.getService(UserSelfService.class);
254 try {
255 jwtUserSelfService.read();
256 fail("Failure expected on an expired token");
257 } catch (AccessControlException e) {
258
259 }
260 }
261
262 @Test
263 public void notBefore() throws ParseException, JOSEException {
264
265 SyncopeClient localClient = CLIENT_FACTORY.create(ADMIN_UNAME, ADMIN_PWD);
266 AccessTokenService accessTokenService = localClient.getService(AccessTokenService.class);
267
268 Response response = accessTokenService.login();
269 String token = response.getHeaderString(RESTHeaders.TOKEN);
270 assertNotNull(token);
271 SignedJWT jwt = SignedJWT.parse(token);
272 String tokenId = jwt.getJWTClaimsSet().getJWTID();
273
274
275 Date currentTime = new Date();
276
277 Calendar expiration = Calendar.getInstance();
278 expiration.setTime(currentTime);
279 expiration.add(Calendar.MINUTE, 5);
280
281 JWTClaimsSet.Builder claimsSet = new JWTClaimsSet.Builder().
282 jwtID(tokenId).
283 subject(ADMIN_UNAME).
284 issueTime(currentTime).
285 issuer(JWT_ISSUER).
286 expirationTime(expiration.getTime()).
287 notBeforeTime(new Date(currentTime.getTime() + 60000L));
288 jwt = new SignedJWT(new JWSHeader(JWS_SIGNER.getJwsAlgorithm()), claimsSet.build());
289 jwt.sign(JWS_SIGNER);
290 String signed = jwt.serialize();
291
292 SyncopeClient jwtClient = CLIENT_FACTORY.create(signed);
293 UserSelfService jwtUserSelfService = jwtClient.getService(UserSelfService.class);
294 try {
295 jwtUserSelfService.read();
296 fail("Failure expected on a token that is not valid yet");
297 } catch (AccessControlException e) {
298
299 }
300 }
301
302 @Test
303 public void noSignature() throws ParseException {
304
305 SyncopeClient localClient = CLIENT_FACTORY.create(ADMIN_UNAME, ADMIN_PWD);
306 AccessTokenService accessTokenService = localClient.getService(AccessTokenService.class);
307
308 Response response = accessTokenService.login();
309 String token = response.getHeaderString(RESTHeaders.TOKEN);
310 assertNotNull(token);
311 JWT jwt = SignedJWT.parse(token);
312
313
314 JWTClaimsSet.Builder claimsSet = new JWTClaimsSet.Builder(jwt.getJWTClaimsSet());
315 jwt = new PlainJWT(claimsSet.build());
316 String bearer = jwt.serialize();
317
318 SyncopeClient jwtClient = CLIENT_FACTORY.create(bearer);
319 UserSelfService jwtUserSelfService = jwtClient.getService(UserSelfService.class);
320 try {
321 jwtUserSelfService.read();
322 fail("Failure expected on no signature");
323 } catch (AccessControlException e) {
324
325 }
326 }
327
328 @Test
329 public void unknownId() throws ParseException, JOSEException {
330
331 SyncopeClient localClient = CLIENT_FACTORY.create(ADMIN_UNAME, ADMIN_PWD);
332 AccessTokenService accessTokenService = localClient.getService(AccessTokenService.class);
333
334 Response response = accessTokenService.login();
335 String token = response.getHeaderString(RESTHeaders.TOKEN);
336 assertNotNull(token);
337 SignedJWT jwt = SignedJWT.parse(token);
338
339
340 JWTClaimsSet.Builder claimsSet = new JWTClaimsSet.Builder(jwt.getJWTClaimsSet()).
341 jwtID(UUID.randomUUID().toString());
342 jwt = new SignedJWT(new JWSHeader(JWS_SIGNER.getJwsAlgorithm()), claimsSet.build());
343 jwt.sign(JWS_SIGNER);
344 String signed = jwt.serialize();
345
346 SyncopeClient jwtClient = CLIENT_FACTORY.create(signed);
347 UserSelfService jwtUserSelfService = jwtClient.getService(UserSelfService.class);
348 try {
349 jwtUserSelfService.read();
350 fail("Failure expected on an unknown id");
351 } catch (AccessControlException e) {
352
353 }
354 }
355
356 @Test
357 public void thirdPartyToken() throws ParseException, JOSEException {
358 assumeFalse(JWSAlgorithm.Family.RSA.contains(JWS_ALGORITHM));
359
360
361 Date currentTime = new Date();
362
363 Calendar expiration = Calendar.getInstance();
364 expiration.setTime(currentTime);
365 expiration.add(Calendar.MINUTE, 5);
366
367 JWTClaimsSet.Builder claimsSet = new JWTClaimsSet.Builder().
368 jwtID(UUID.randomUUID().toString()).
369 subject("puccini@apache.org").
370 issueTime(currentTime).
371 issuer(CustomJWTSSOProvider.ISSUER).
372 expirationTime(expiration.getTime()).
373 notBeforeTime(currentTime);
374 SignedJWT jwt = new SignedJWT(new JWSHeader(JWS_ALGORITHM), claimsSet.build());
375 jwt.sign(new MACSigner(CustomJWTSSOProvider.CUSTOM_KEY));
376 String signed = jwt.serialize();
377
378 SyncopeClient jwtClient = CLIENT_FACTORY.create(signed);
379
380 Triple<Map<String, Set<String>>, List<String>, UserTO> self = jwtClient.self();
381 assertFalse(self.getLeft().isEmpty());
382 assertEquals("puccini", self.getRight().getUsername());
383 }
384
385 @Test
386 public void thirdPartyTokenUnknownUser() throws ParseException, JOSEException {
387 assumeFalse(JWSAlgorithm.Family.RSA.contains(JWS_ALGORITHM));
388
389
390 Date currentTime = new Date();
391
392 Calendar expiration = Calendar.getInstance();
393 expiration.setTime(currentTime);
394 expiration.add(Calendar.MINUTE, 5);
395
396 JWTClaimsSet.Builder claimsSet = new JWTClaimsSet.Builder().
397 jwtID(UUID.randomUUID().toString()).
398 subject("strauss@apache.org").
399 issueTime(currentTime).
400 issuer(CustomJWTSSOProvider.ISSUER).
401 expirationTime(expiration.getTime()).
402 notBeforeTime(currentTime);
403 SignedJWT jwt = new SignedJWT(new JWSHeader(JWS_SIGNER.getJwsAlgorithm()), claimsSet.build());
404 jwt.sign(JWS_SIGNER);
405 String signed = jwt.serialize();
406
407 SyncopeClient jwtClient = CLIENT_FACTORY.create(signed);
408
409 try {
410 jwtClient.self();
411 fail("Failure expected on an unknown subject");
412 } catch (AccessControlException e) {
413
414 }
415 }
416
417 @Test
418 public void thirdPartyTokenUnknownIssuer() throws ParseException, JOSEException {
419 assumeFalse(JWSAlgorithm.Family.RSA.contains(JWS_ALGORITHM));
420
421
422 Date currentTime = new Date();
423
424 Calendar expiration = Calendar.getInstance();
425 expiration.setTime(currentTime);
426 expiration.add(Calendar.MINUTE, 5);
427
428 JWTClaimsSet.Builder claimsSet = new JWTClaimsSet.Builder().
429 jwtID(UUID.randomUUID().toString()).
430 subject("puccini@apache.org").
431 issueTime(currentTime).
432 issuer(CustomJWTSSOProvider.ISSUER + "_").
433 expirationTime(expiration.getTime()).
434 notBeforeTime(currentTime);
435 SignedJWT jwt = new SignedJWT(new JWSHeader(JWS_SIGNER.getJwsAlgorithm()), claimsSet.build());
436 jwt.sign(JWS_SIGNER);
437 String signed = jwt.serialize();
438
439 SyncopeClient jwtClient = CLIENT_FACTORY.create(signed);
440
441 try {
442 jwtClient.self();
443 fail("Failure expected on an unknown issuer");
444 } catch (AccessControlException e) {
445
446 }
447 }
448
449 @Test
450 public void thirdPartyTokenBadSignature()
451 throws ParseException, KeyLengthException, NoSuchAlgorithmException,
452 InvalidKeySpecException, JOSEException {
453
454 assumeFalse(JWSAlgorithm.Family.RSA.contains(JWS_ALGORITHM));
455
456
457 Date currentTime = new Date();
458
459 Calendar expiration = Calendar.getInstance();
460 expiration.setTime(currentTime);
461 expiration.add(Calendar.MINUTE, 5);
462
463 JWTClaimsSet.Builder claimsSet = new JWTClaimsSet.Builder().
464 jwtID(UUID.randomUUID().toString()).
465 subject("puccini@apache.org").
466 issueTime(currentTime).
467 issuer(CustomJWTSSOProvider.ISSUER).
468 expirationTime(expiration.getTime()).
469 notBeforeTime(currentTime);
470
471 AccessTokenJWSSigner customJWSSigner =
472 new AccessTokenJWSSigner(JWS_ALGORITHM, RandomStringUtils.randomAlphanumeric(512));
473
474 SignedJWT jwt = new SignedJWT(new JWSHeader(customJWSSigner.getJwsAlgorithm()), claimsSet.build());
475 jwt.sign(customJWSSigner);
476 String signed = jwt.serialize();
477
478 SyncopeClient jwtClient = CLIENT_FACTORY.create(signed);
479
480 try {
481 jwtClient.self();
482 fail("Failure expected on a bad signature");
483 } catch (AccessControlException e) {
484
485 }
486 }
487
488 @Test
489 public void issueSYNCOPE1420() throws ParseException {
490 Long orig = confParamOps.get(SyncopeConstants.MASTER_DOMAIN, "jwt.lifetime.minutes", null, Long.class);
491 try {
492
493 confParamOps.set(SyncopeConstants.MASTER_DOMAIN, "jwt.lifetime.minutes", 0);
494
495 UserCR userCR = UserITCase.getUniqueSample("syncope164@syncope.apache.org");
496 UserTO user = createUser(userCR).getEntity();
497 assertNotNull(user);
498
499
500 String jwt = CLIENT_FACTORY.create(user.getUsername(), "password123").getJWT();
501
502 Date expirationTime = SignedJWT.parse(jwt).getJWTClaimsSet().getExpirationTime();
503 assertNotNull(expirationTime);
504
505
506 try {
507 Thread.sleep(1000L);
508 } catch (InterruptedException e) {
509
510 }
511 assertTrue(expirationTime.before(new Date()));
512
513
514
515 String newJWT = CLIENT_FACTORY.create(user.getUsername(), "password123").getJWT();
516 assertNotEquals(jwt, newJWT);
517 } finally {
518 confParamOps.set(SyncopeConstants.MASTER_DOMAIN, "jwt.lifetime.minutes", orig);
519 }
520 }
521 }