|
36 | 36 |
|
37 | 37 | import javax.crypto.SecretKey;
|
38 | 38 |
|
| 39 | +import com.nimbusds.jose.JOSEException; |
39 | 40 | import com.nimbusds.jose.JOSEObjectType;
|
40 | 41 | import com.nimbusds.jose.JWSAlgorithm;
|
41 | 42 | import com.nimbusds.jose.JWSHeader;
|
42 | 43 | import com.nimbusds.jose.JWSSigner;
|
43 | 44 | import com.nimbusds.jose.crypto.MACSigner;
|
44 | 45 | import com.nimbusds.jose.crypto.RSASSASigner;
|
| 46 | +import com.nimbusds.jose.jwk.JWKSet; |
| 47 | +import com.nimbusds.jose.jwk.RSAKey; |
| 48 | +import com.nimbusds.jose.jwk.gen.RSAKeyGenerator; |
45 | 49 | import com.nimbusds.jose.jwk.source.JWKSource;
|
46 | 50 | import com.nimbusds.jose.proc.BadJOSEException;
|
47 | 51 | import com.nimbusds.jose.proc.DefaultJOSEObjectTypeVerifier;
|
|
82 | 86 | import static org.mockito.ArgumentMatchers.eq;
|
83 | 87 | import static org.mockito.BDDMockito.given;
|
84 | 88 | import static org.mockito.Mockito.mock;
|
| 89 | +import static org.mockito.Mockito.times; |
85 | 90 | import static org.mockito.Mockito.verify;
|
86 | 91 | import static org.mockito.Mockito.verifyNoInteractions;
|
87 | 92 | import static org.mockito.Mockito.verifyNoMoreInteractions;
|
@@ -660,6 +665,81 @@ public void decodeWhenCacheThenRetrieveFromCache() {
|
660 | 665 | verifyNoInteractions(restOperations);
|
661 | 666 | }
|
662 | 667 |
|
| 668 | + @Test |
| 669 | + public void decodeWhenCacheAndUnknownKidShouldTriggerFetchOfJwkSet() throws JOSEException { |
| 670 | + RestOperations restOperations = mock(RestOperations.class); |
| 671 | + |
| 672 | + Cache cache = mock(Cache.class); |
| 673 | + given(cache.get(eq(JWK_SET_URI), any(Callable.class))).willReturn(JWK_SET); |
| 674 | + |
| 675 | + RSAKey rsaJWK = new RSAKeyGenerator(2048) |
| 676 | + .keyID("new_kid") |
| 677 | + .generate(); |
| 678 | + String jwkSetWithNewKid = new JWKSet(rsaJWK).toPublicJWKSet().toString(); |
| 679 | + given(restOperations.exchange(any(RequestEntity.class), eq(String.class))) |
| 680 | + .willReturn(new ResponseEntity<>(jwkSetWithNewKid, HttpStatus.OK)); |
| 681 | + |
| 682 | + // @formatter:off |
| 683 | + NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(JWK_SET_URI) |
| 684 | + .cache(cache) |
| 685 | + .restOperations(restOperations) |
| 686 | + .build(); |
| 687 | + // @formatter:on |
| 688 | + |
| 689 | + // Decode JWT with new KID |
| 690 | + JWSSigner signer = new RSASSASigner(rsaJWK); |
| 691 | + JWTClaimsSet claimsSet = new JWTClaimsSet.Builder() |
| 692 | + .expirationTime(Date.from(Instant.now().plusSeconds(60))) |
| 693 | + .build(); |
| 694 | + SignedJWT signedJWT = new SignedJWT(new JWSHeader.Builder(JWSAlgorithm.RS256).keyID(rsaJWK.getKeyID()).build(), claimsSet); |
| 695 | + signedJWT.sign(signer); |
| 696 | + String token = signedJWT.serialize(); |
| 697 | + |
| 698 | + jwtDecoder.decode(token); |
| 699 | + |
| 700 | + ArgumentCaptor<RequestEntity> requestEntityCaptor = ArgumentCaptor.forClass(RequestEntity.class); |
| 701 | + verify(restOperations).exchange(requestEntityCaptor.capture(), eq(String.class)); |
| 702 | + verifyNoMoreInteractions(restOperations); |
| 703 | + assertThat(requestEntityCaptor.getValue().getHeaders().getAccept()).contains(MediaType.APPLICATION_JSON, APPLICATION_JWK_SET_JSON); |
| 704 | + } |
| 705 | + |
| 706 | + @Test |
| 707 | + public void decodeWithoutCacheSpecifiedAndUnknownKidShouldTriggerFetchOfJwkSet() throws JOSEException { |
| 708 | + RestOperations restOperations = mock(RestOperations.class); |
| 709 | + |
| 710 | + RSAKey rsaJWK = new RSAKeyGenerator(2048) |
| 711 | + .keyID("new_kid") |
| 712 | + .generate(); |
| 713 | + String jwkSetWithNewKid = new JWKSet(rsaJWK).toPublicJWKSet().toString(); |
| 714 | + given(restOperations.exchange(any(RequestEntity.class), eq(String.class))) |
| 715 | + .willReturn(new ResponseEntity<>(JWK_SET, HttpStatus.OK), new ResponseEntity<>(jwkSetWithNewKid, HttpStatus.OK)); |
| 716 | + |
| 717 | + // @formatter:off |
| 718 | + NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(JWK_SET_URI) |
| 719 | + .restOperations(restOperations) |
| 720 | + .build(); |
| 721 | + // @formatter:on |
| 722 | + jwtDecoder.decode(SIGNED_JWT); |
| 723 | + |
| 724 | + // Decode JWT with new KID |
| 725 | + JWSSigner signer = new RSASSASigner(rsaJWK); |
| 726 | + JWTClaimsSet claimsSet = new JWTClaimsSet.Builder() |
| 727 | + .expirationTime(Date.from(Instant.now().plusSeconds(60))) |
| 728 | + .build(); |
| 729 | + SignedJWT signedJWT = new SignedJWT(new JWSHeader.Builder(JWSAlgorithm.RS256).keyID(rsaJWK.getKeyID()).build(), claimsSet); |
| 730 | + signedJWT.sign(signer); |
| 731 | + String token = signedJWT.serialize(); |
| 732 | + |
| 733 | + jwtDecoder.decode(token); |
| 734 | + |
| 735 | + ArgumentCaptor<RequestEntity> requestEntityCaptor = ArgumentCaptor.forClass(RequestEntity.class); |
| 736 | + verify(restOperations, times(2)).exchange(requestEntityCaptor.capture(), eq(String.class)); |
| 737 | + verifyNoMoreInteractions(restOperations); |
| 738 | + List<RequestEntity> requestEntities = requestEntityCaptor.getAllValues(); |
| 739 | + assertThat(requestEntities.get(0).getHeaders().getAccept()).contains(MediaType.APPLICATION_JSON, APPLICATION_JWK_SET_JSON); |
| 740 | + assertThat(requestEntities.get(1).getHeaders().getAccept()).contains(MediaType.APPLICATION_JSON, APPLICATION_JWK_SET_JSON); |
| 741 | + } |
| 742 | + |
663 | 743 | @Test
|
664 | 744 | public void decodeWhenCacheIsConfiguredAndValueLoaderErrorsThenThrowsJwtException() {
|
665 | 745 | Cache cache = new ConcurrentMapCache("test-jwk-set-cache");
|
|
0 commit comments