Skip to content

Commit

Permalink
Fixes for azure jwks reader (#3201)
Browse files Browse the repository at this point in the history
Signed-off-by: alexandr cumarav <alexandr.cumarav@broadcom.com>
  • Loading branch information
cumarav authored Nov 15, 2023
1 parent c059322 commit f24695e
Show file tree
Hide file tree
Showing 7 changed files with 137 additions and 52 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@

package org.zowe.apiml.gateway.security.service.token;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
Expand All @@ -21,7 +20,6 @@
@Data
@AllArgsConstructor
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class JwkKeys {

private List<Key> keys;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

package org.zowe.apiml.gateway.security.service.token;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import io.jsonwebtoken.Clock;
Expand All @@ -30,6 +31,7 @@ public Clock oidcJwtClock() {
@Primary
public ObjectMapper oidcJwkMapper() {
return new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.registerModule(new JavaTimeModule());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@
import org.zowe.apiml.security.common.token.TokenNotValidException;

import javax.annotation.PostConstruct;

import java.io.IOException;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
Expand Down Expand Up @@ -105,7 +104,7 @@ void fetchJwksUrls() {
return;
}
log.debug("Refreshing JWK endpoints {}", jwksUri);
HttpGet getRequest = new HttpGet(jwksUri + "?client_id=" + clientId);
HttpGet getRequest = new HttpGet(jwksUri);
try {
CloseableHttpResponse response = httpClient.execute(getRequest);
final int statusCode = response.getStatusLine() != null ? response.getStatusLine().getStatusCode() : 0;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* This program and the accompanying materials are made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v20.html
*
* SPDX-License-Identifier: EPL-2.0
*
* Copyright Contributors to the Zowe Project.
*/

package org.zowe.apiml.gateway.security.service.token;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import org.junit.jupiter.api.Test;

import java.io.InputStream;

import static org.assertj.core.api.Assertions.assertThat;

class OIDCConfigTest {
private final ObjectMapper oidcJwkMapper = new OIDCConfig().oidcJwkMapper();

@Test
@SneakyThrows
void shouldParseJwksFormatWithExtraProperties() {
try (InputStream is = this.getClass().getResourceAsStream("/test_samples/azure_jwks.json")) {
JwkKeys jwkKeys = oidcJwkMapper.readValue(is, JwkKeys.class);

assertThat(jwkKeys.getKeys()).hasSize(1);
JwkKeys.Key key = jwkKeys.getKeys().get(0);
assertThat(key.getKid()).isEqualTo("9GmnyFPkhc3hOuR22mvSvgnLo7Y");
assertThat(key.getKty()).isEqualTo("RSA");
}
}

@Test
@SneakyThrows
void shouldParseExpectedJwksFormat() {
try (InputStream is = this.getClass().getResourceAsStream("/test_samples/okta_jwks.json")) {
JwkKeys jwkKeys = oidcJwkMapper.readValue(is, JwkKeys.class);

assertThat(jwkKeys.getKeys()).hasSize(2);
JwkKeys.Key key = jwkKeys.getKeys().get(0);
assertThat(key.getKid()).isEqualTo("Lcxckkor94qkrunxHP7Tkib547rzmkXvsYV-nc6U-N4");
assertThat(key.getKty()).isEqualTo("RSA");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpStatus;
import org.apache.http.StatusLine;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.entity.BasicHttpEntity;
import org.apache.http.impl.client.CloseableHttpClient;
Expand Down Expand Up @@ -45,56 +44,34 @@
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class OIDCTokenProviderTest {

private static final String JWKS_KEYS_BODY = "\n"
+ "{\n"
+ " \"keys\": [\n"
+ " {\n"
+ " \"kty\": \"RSA\",\n"
+ " \"alg\": \"RS256\",\n"
+ " \"kid\": \"Lcxckkor94qkrunxHP7Tkib547rzmkXvsYV-nc6U-N4\",\n"
+ " \"use\": \"sig\",\n"
+ " \"e\": \"AQAB\",\n"
+ " \"n\": \"v6wT5k7uLto_VPTV8fW9_wRqWHuqnZbyEYAwNYRdffe9WowwnzUAr0Z93-4xDvCRuVfTfvCe9orEWdjZMaYlDq_Dj5BhLAqmBAF299Kv1GymOioLRDvoVWy0aVHYXXNaqJCPsaWIDiCly-_kJBbnda_rmB28a_878TNxom0mDQ20TI5SgdebqqMBOdHEqIYH1ER9euybekeqJX24EqE9YW4Yug5BOkZ9KcUkiEsH_NPyRlozihj18Qab181PRyKHE6M40W7w67XcRq2llTy-z9RrQupcyvLD7L62KN0ey8luKWnVg4uIOldpyBYyiRX2WPM-2K00RVC0e4jQKs34Gw\"\n"
+ " },\n"
+ " {\n"
+ " \"kty\": \"RSA\",\n"
+ " \"alg\": \"RS256\",\n"
+ " \"kid\": \"-716sp3XBB_v30lGj2mu5MdXkdh8poa9zJQlAwC46n4\",\n"
+ " \"use\": \"sig\",\n"
+ " \"e\": \"AQAB\",\n"
+ " \"n\": \"5rYyqFsxel0Pv-xRDHPbg3IfumE4ks9ffLvJrfZVgrTQyiFmFfBnyD3r7y6626Yr5-68Pj0I5SHlCBPkkgTU_e9Z3tCYiegtIOeJdSdumWR2JDVAsbpwFJDG_kxP9czgX7HL0T2BPSapx7ba0ZBXd2-SfSDDL-c1Q0rJ1uQEJwDXAGZV4qy_oXuQf5DuV65Xj8y2Qn1DtVEBThxita-kis_H35CTWgW2zyyaS_08wa00R98mnQ2SHfmO5fZABITmH0DO0coDHqKZ429VNNpELLX9e95dirQ1jfngDbBCmy-XsT8yc6NpAaXmd8P2NHdsO2oK46EQEaFRyMcoDTs3-w\"\n"
+ " }\n"
+ " ]\n"
+ "}";

private static final String OKTA_JWKS_RESOURCE = "/test_samples/okta_jwks.json";
private static final String JWKS_KEYS_BODY_INVALID = "\n"
+ "{\n"
+ " \"keys\": [\n"
+ " {\n"
+ " \"kty\": \"RSA\",\n"
+ " \"alg\": \"RS256\",\n"
+ " \"kid\": \"Lcxckkor94qkrunxHP7Tkib547rzmkXvsYV-nc6U-N4\",\n"
+ " \"use\": \"sig\",\n"
+ " \"e\": \"AQAB\",\n"
+ " \"n\": \"invalid\"\n"
+ " }\n"
+ " ]\n"
+ "}";
+ "{\n"
+ " \"keys\": [\n"
+ " {\n"
+ " \"kty\": \"RSA\",\n"
+ " \"alg\": \"RS256\",\n"
+ " \"kid\": \"Lcxckkor94qkrunxHP7Tkib547rzmkXvsYV-nc6U-N4\",\n"
+ " \"use\": \"sig\",\n"
+ " \"e\": \"AQAB\",\n"
+ " \"n\": \"invalid\"\n"
+ " }\n"
+ " ]\n"
+ "}";

private static final String EXPIRED_TOKEN = "eyJraWQiOiJMY3hja2tvcjk0cWtydW54SFA3VGtpYjU0N3J6bWtYdnNZVi1uYzZVLU40IiwiYWxnIjoiUlMyNTYifQ.eyJ2ZXIiOjEsImp0aSI6IkFULlExakp2UkZ0dUhFUFpGTXNmM3A0enQ5aHBRRHZrSU1CQ3RneU9IcTdlaEkiLCJpc3MiOiJodHRwczovL2Rldi05NTcyNzY4Ni5va3RhLmNvbS9vYXV0aDIvZGVmYXVsdCIsImF1ZCI6ImFwaTovL2RlZmF1bHQiLCJpYXQiOjE2OTcwNjA3NzMsImV4cCI6MTY5NzA2NDM3MywiY2lkIjoiMG9hNmE0OG1uaVhBcUVNcng1ZDciLCJ1aWQiOiIwMHU5OTExOGgxNmtQT1dBbTVkNyIsInNjcCI6WyJvcGVuaWQiXSwiYXV0aF90aW1lIjoxNjk3MDYwMDY0LCJzdWIiOiJzajg5NTA5MkBicm9hZGNvbS5uZXQiLCJncm91cHMiOlsiRXZlcnlvbmUiXX0.Cuf1JVq_NnfBxaCwiLsR5O6DBmVV1fj9utAfKWIF1hlek2hCJsDLQM4ii_ucQ0MM1V3nVE1ZatPB-W7ImWPlGz7NeNBv7jEV9DkX70hchCjPHyYpaUhAieTG75obdufiFpI55bz3qH5cPRvsKv0OKKI9T8D7GjEWsOhv6CevJJZZvgCFLGFfnacKLOY5fEBN82bdmCulNfPVrXF23rOregFjOBJ1cKWfjmB0UGWgI8VBGGemMNm3ACX3OYpTOek2PBfoCIZWOSGnLZumFTYA0F_3DsWYhIJNoFv16_EBBJcp_C0BYE_fiuXzeB0fieNUXASsKp591XJMflDQS_Zt1g";

private static final String TOKEN = "token";

private OIDCTokenProvider oidcTokenProvider;

@Mock
private OIDCTokenProvider underTest;
@Mock
private CloseableHttpClient httpClient;
@Mock
Expand All @@ -109,7 +86,7 @@ void setup() throws CachingServiceClientException, IOException {
responseEntity = new BasicHttpEntity();
responseEntity.setContent(IOUtils.toInputStream("", StandardCharsets.UTF_8));
oidcTokenProvider = new OIDCTokenProvider(httpClient, new DefaultClock(), new ObjectMapper());
ReflectionTestUtils.setField(oidcTokenProvider, "jwkRefreshInterval",1);
ReflectionTestUtils.setField(oidcTokenProvider, "jwkRefreshInterval", 1);
ReflectionTestUtils.setField(oidcTokenProvider, "jwksUri", "https://jwksurl");
oidcTokenProvider.clientId = "client_id";
oidcTokenProvider.clientSecret = "client_secret";
Expand All @@ -119,8 +96,9 @@ void setup() throws CachingServiceClientException, IOException {
class GivenInitializationWithJwks {

@BeforeEach
void setup() throws IOException {
responseEntity.setContent(IOUtils.toInputStream(JWKS_KEYS_BODY, StandardCharsets.UTF_8));
void setup() {

responseEntity.setContent(getClass().getResourceAsStream(OKTA_JWKS_RESOURCE));
}

@Test
Expand All @@ -141,7 +119,7 @@ void initialized_thenJwksFullfilled() throws IOException {

@Test
@SuppressWarnings("unchecked")
void whenRequestFails_thenNotInitialized() throws ClientProtocolException, IOException {
void whenRequestFails_thenNotInitialized() throws IOException {
Map<String, JwkKeys> jwks = (Map<String, JwkKeys>) ReflectionTestUtils.getField(oidcTokenProvider, "jwks");
when(responseStatusLine.getStatusCode()).thenReturn(HttpStatus.SC_INTERNAL_SERVER_ERROR);
when(response.getStatusLine()).thenReturn(responseStatusLine);
Expand All @@ -162,7 +140,7 @@ void whenUriNotProvided_thenNotInitialized() {

@Test
@SuppressWarnings("unchecked")
void whenInvalidKey_thenNotInitialized() throws ClientProtocolException, IOException {
void whenInvalidKey_thenNotInitialized() throws IOException {
responseEntity.setContent(IOUtils.toInputStream(JWKS_KEYS_BODY_INVALID, StandardCharsets.UTF_8));
Map<String, JwkKeys> jwks = (Map<String, JwkKeys>) ReflectionTestUtils.getField(oidcTokenProvider, "jwks");
when(responseStatusLine.getStatusCode()).thenReturn(HttpStatus.SC_OK);
Expand All @@ -178,9 +156,9 @@ void whenInvalidKey_thenNotInitialized() throws ClientProtocolException, IOExcep
class GivenTokenForValidation {

@SuppressWarnings("unchecked")
private void initJwks() throws ClientProtocolException, IOException {
private void initJwks() throws IOException {
Map<String, JwkKeys> jwks = (Map<String, JwkKeys>) ReflectionTestUtils.getField(oidcTokenProvider, "jwks");
responseEntity.setContent(IOUtils.toInputStream(JWKS_KEYS_BODY, StandardCharsets.UTF_8));
responseEntity.setContent(getClass().getResourceAsStream(OKTA_JWKS_RESOURCE));
when(responseStatusLine.getStatusCode()).thenReturn(HttpStatus.SC_OK);
when(response.getStatusLine()).thenReturn(responseStatusLine);
when(response.getEntity()).thenReturn(responseEntity);
Expand All @@ -190,20 +168,20 @@ private void initJwks() throws ClientProtocolException, IOException {
}

@Test
void whenValidTokenExpired_thenReturnInvalid() throws ClientProtocolException, IOException {
void whenValidTokenExpired_thenReturnInvalid() throws IOException {
initJwks();
assertFalse(oidcTokenProvider.isValid(EXPIRED_TOKEN));
}

@Test
void whenValidtoken_thenReturnValid() throws ClientProtocolException, IOException {
void whenValidtoken_thenReturnValid() throws IOException {
initJwks();
ReflectionTestUtils.setField(oidcTokenProvider, "clock", new FixedClock(new Date(Instant.ofEpochSecond(1697060773 + 1000L).toEpochMilli())));
assertTrue(oidcTokenProvider.isValid(EXPIRED_TOKEN));
}

@Test
void whenInvalidToken_thenReturnInvalid() throws ClientProtocolException, IOException {
void whenInvalidToken_thenReturnInvalid() throws IOException {
initJwks();
assertFalse(oidcTokenProvider.isValid(TOKEN));
}
Expand All @@ -230,6 +208,7 @@ void whenTokenIsEmpty_thenReturnInvalid() {
assertFalse(oidcTokenProvider.isValid(""));
}
}

@Nested
class GivenInvalidConfiguration {

Expand All @@ -250,4 +229,26 @@ void whenInvalidClientSecret_thenReturnInvalid(String secret) {
}
}

@Nested
class JwksUriLoad {
@Mock
private CloseableHttpClient httpClientMock;
@Mock
CloseableHttpResponse httpResponse;

@BeforeEach
public void setUp() {
oidcTokenProvider = new OIDCTokenProvider(httpClientMock, new DefaultClock(), new ObjectMapper());
ReflectionTestUtils.setField(oidcTokenProvider, "jwksUri", "https://jwksurl");
}

@Test
void shouldNotModifyJwksUri() throws IOException {
when(httpClientMock.execute(any())).thenReturn(httpResponse);

oidcTokenProvider.fetchJwksUrls();

verify(httpClientMock).execute(argThat((getJwksRequest) -> getJwksRequest.getURI().toString().equals("https://jwksurl")));
}
}
}
16 changes: 16 additions & 0 deletions gateway-service/src/test/resources/test_samples/azure_jwks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"keys": [
{
"kty": "RSA",
"use": "sig",
"kid": "9GmnyFPkhc3hOuR22mvSvgnLo7Y",
"x5t": "9GmnyFPkhc3hOuR22mvSvgnLo7Y",
"n": "z_w-5U4eZwenXYnEgt2rCN-753YQ7RN8ykiNprNiLl4ilpwAGLWF1cssoRflsSiBVZcCSwUzUwsifG7sbRq9Vc8RFs72Gg0AUwPsJFUqNttMg3Ot-wTqsZtE5GNSBUSqnI-iWoZfjw-uLsS0u4MfzP8Fpkd-rzRlifuIAYK8Ffi1bldkszeBzQbBZbXFwiw5uTf8vEAkH_IAdB732tQAsNXpWWYDV74nKAiwLlDS5FWVs2S2T-MPNAg28MLxYfRhW2bUpd693inxI8WTSLRncouzMImJF4XeMG2ZRZ0z_KJra_uzzMCLbILtpnLA95ysxWw-4ygm3MxN2iBM2IaJeQ",
"e": "AQAB",
"x5c": [
"MIIC/jCCAeagAwIBAgIJAOCJOVRxNKcNMA0GCSqGSIb3DQEBCwUAMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwHhcNMjMwODI4MjAwMjQwWhcNMjgwODI4MjAwMjQwWjAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz/w+5U4eZwenXYnEgt2rCN+753YQ7RN8ykiNprNiLl4ilpwAGLWF1cssoRflsSiBVZcCSwUzUwsifG7sbRq9Vc8RFs72Gg0AUwPsJFUqNttMg3Ot+wTqsZtE5GNSBUSqnI+iWoZfjw+uLsS0u4MfzP8Fpkd+rzRlifuIAYK8Ffi1bldkszeBzQbBZbXFwiw5uTf8vEAkH/IAdB732tQAsNXpWWYDV74nKAiwLlDS5FWVs2S2T+MPNAg28MLxYfRhW2bUpd693inxI8WTSLRncouzMImJF4XeMG2ZRZ0z/KJra/uzzMCLbILtpnLA95ysxWw+4ygm3MxN2iBM2IaJeQIDAQABoyEwHzAdBgNVHQ4EFgQU/wzRzxsifMCz54SZ3HuF4P4jtzowDQYJKoZIhvcNAQELBQADggEBACaWlbJTObDai8+wmskHedKYb3FCfTwvH/sCRsygHIeDIi23CpoWeKt5FwXsSeqDMd0Hb6IMtYDG5rfGvhkNfunt3sutK0VpZZMNdSBmIXaUx4mBRRUsG4hpeWRrHRgTnxweDDVw4Mv+oYCmpY7eZ4SenISkSd/4qrXzFaI9NeZCY7Jg9vg1bev+NaUtD3C4As6GQ+mN8Rm2NG9vzgTDlKf4Wb5Exy7u9dMW1TChiy28ieVkETKdqwXcbhqM8GOLBUFicdmgP2y9aDGjb89BuaeoHJCGpWWCi3UZth14clVzC6p7ZD6fFx5tKMOL/hQvs3ugGtvFDWCsvcT8bB84RO8="
],
"issuer": "https://login.microsoftonline.com/d10ec64d-2698-489f-8a03-7e77cdca82d7/v2.0"
}
]
}
20 changes: 20 additions & 0 deletions gateway-service/src/test/resources/test_samples/okta_jwks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"keys": [
{
"kty": "RSA",
"alg": "RS256",
"kid": "Lcxckkor94qkrunxHP7Tkib547rzmkXvsYV-nc6U-N4",
"use": "sig",
"e": "AQAB",
"n": "v6wT5k7uLto_VPTV8fW9_wRqWHuqnZbyEYAwNYRdffe9WowwnzUAr0Z93-4xDvCRuVfTfvCe9orEWdjZMaYlDq_Dj5BhLAqmBAF299Kv1GymOioLRDvoVWy0aVHYXXNaqJCPsaWIDiCly-_kJBbnda_rmB28a_878TNxom0mDQ20TI5SgdebqqMBOdHEqIYH1ER9euybekeqJX24EqE9YW4Yug5BOkZ9KcUkiEsH_NPyRlozihj18Qab181PRyKHE6M40W7w67XcRq2llTy-z9RrQupcyvLD7L62KN0ey8luKWnVg4uIOldpyBYyiRX2WPM-2K00RVC0e4jQKs34Gw"
},
{
"kty": "RSA",
"alg": "RS256",
"kid": "-716sp3XBB_v30lGj2mu5MdXkdh8poa9zJQlAwC46n4",
"use": "sig",
"e": "AQAB",
"n": "5rYyqFsxel0Pv-xRDHPbg3IfumE4ks9ffLvJrfZVgrTQyiFmFfBnyD3r7y6626Yr5-68Pj0I5SHlCBPkkgTU_e9Z3tCYiegtIOeJdSdumWR2JDVAsbpwFJDG_kxP9czgX7HL0T2BPSapx7ba0ZBXd2-SfSDDL-c1Q0rJ1uQEJwDXAGZV4qy_oXuQf5DuV65Xj8y2Qn1DtVEBThxita-kis_H35CTWgW2zyyaS_08wa00R98mnQ2SHfmO5fZABITmH0DO0coDHqKZ429VNNpELLX9e95dirQ1jfngDbBCmy-XsT8yc6NpAaXmd8P2NHdsO2oK46EQEaFRyMcoDTs3-w"
}
]
}

0 comments on commit f24695e

Please sign in to comment.