View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  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,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
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   * Some tests for JWT Tokens.
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          // Get the token
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          // Validate the signature
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         // Verify the expiry header matches that of the token
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         // Verify issuedAt
112         Date issueTime = jwt.getJWTClaimsSet().getIssueTime();
113         assertNotNull(issueTime);
114         assertTrue(issueTime.before(now));
115 
116         // Validate subject + issuer
117         assertEquals(ADMIN_UNAME, jwt.getJWTClaimsSet().getSubject());
118         assertEquals(JWT_ISSUER, jwt.getJWTClaimsSet().getIssuer());
119 
120         // Verify NotBefore
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         // Get the token
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         // Query the UserSelfService using the token
137         SyncopeClient jwtClient = CLIENT_FACTORY.create(token);
138         UserSelfService jwtUserSelfService = jwtClient.getService(UserSelfService.class);
139         jwtUserSelfService.read();
140 
141         // Test a "bad" token
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         // Get an initial token
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         // Create a new token using the Id of the first token
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         // Get an initial token
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         // Create a new token using the Id of the first token
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             // expected
223         }
224     }
225 
226     @Test
227     public void expiredToken() throws ParseException, JOSEException {
228         // Get an initial token
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         // Create a new token using the Id of the first token
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             // expected
259         }
260     }
261 
262     @Test
263     public void notBefore() throws ParseException, JOSEException {
264         // Get an initial token
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         // Create a new token using the Id of the first token
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             // expected
299         }
300     }
301 
302     @Test
303     public void noSignature() throws ParseException {
304         // Get an initial token
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         // Create a new token using the Id of the first token
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             // expected
325         }
326     }
327 
328     @Test
329     public void unknownId() throws ParseException, JOSEException {
330         // Get an initial token
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         // Create a new token using an unknown Id
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             // expected
353         }
354     }
355 
356     @Test
357     public void thirdPartyToken() throws ParseException, JOSEException {
358         assumeFalse(JWSAlgorithm.Family.RSA.contains(JWS_ALGORITHM));
359 
360         // Create a new token
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         // Create a new token
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             // expected
414         }
415     }
416 
417     @Test
418     public void thirdPartyTokenUnknownIssuer() throws ParseException, JOSEException {
419         assumeFalse(JWSAlgorithm.Family.RSA.contains(JWS_ALGORITHM));
420 
421         // Create a new token
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             // expected
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         // Create a new token
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             // expected
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             // set for immediate JWT expiration
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             // login, get JWT with  expiryTime
500             String jwt = CLIENT_FACTORY.create(user.getUsername(), "password123").getJWT();
501 
502             Date expirationTime = SignedJWT.parse(jwt).getJWTClaimsSet().getExpirationTime();
503             assertNotNull(expirationTime);
504 
505             // wait for 1 sec, check that JWT is effectively expired
506             try {
507                 Thread.sleep(1000L);
508             } catch (InterruptedException e) {
509                 // ignore
510             }
511             assertTrue(expirationTime.before(new Date()));
512 
513             // login again, get new JWT
514             // (even if ExpiredAccessTokenCleanup did not run yet, as it is scheduled every 5 minutes)
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 }