diff --git a/src/main/java/com/amazon/dlic/auth/http/jwt/keybyoidc/HTTPOpenIdAuthenticator.java b/src/main/java/com/amazon/dlic/auth/http/jwt/keybyoidc/HTTPOpenIdAuthenticator.java index a25b8d284d..222a144a9a 100644 --- a/src/main/java/com/amazon/dlic/auth/http/jwt/keybyoidc/HTTPOpenIdAuthenticator.java +++ b/src/main/java/com/amazon/dlic/auth/http/jwt/keybyoidc/HTTPOpenIdAuthenticator.java @@ -11,9 +11,13 @@ package com.amazon.dlic.auth.http.jwt.keybyoidc; +import java.io.BufferedReader; import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; import java.nio.file.Path; import java.text.ParseException; +import java.util.Map; import java.util.Optional; import java.util.concurrent.TimeUnit; @@ -148,7 +152,6 @@ public AuthCredentials extractCredentials0(SecurityRequest request, ThreadContex httpGet.addHeader(AUTHORIZATION_HEADER, request.getHeaders().get(AUTHORIZATION_HEADER)); // HTTPGet should internally verify the appropriate TLS cert. - try (CloseableHttpResponse response = httpClient.execute(httpGet)) { if (response.getCode() < 200 || response.getCode() >= 300) { @@ -166,25 +169,41 @@ public AuthCredentials extractCredentials0(SecurityRequest request, ThreadContex } String contentType = httpEntity.getContentType(); - - if (!contentType.equals(APPLICATION_JSON) && !contentType.equals(APPLICATION_JWT)) { + if (!contentType.contains(APPLICATION_JSON) && !contentType.contains(APPLICATION_JWT)) { throw new AuthenticatorUnavailableException( "Error while getting " + this.userinfo_endpoint + ": Invalid content type in response" ); } - String userinfoContent = httpEntity.getContent().toString(); + String userinfoContent; + + try ( + InputStream inputStream = httpEntity.getContent(); + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)) + ) { + + StringBuilder content = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + content.append(line); + } + userinfoContent = content.toString(); + } catch (IOException e) { + throw new AuthenticatorUnavailableException( + "Error while getting " + this.userinfo_endpoint + ": Unable to read response content" + ); + } + JWTClaimsSet claims; - boolean isSigned = contentType.equals(APPLICATION_JWT); - if (contentType.equals(APPLICATION_JWT)) { // We don't need the userinfo_encrypted_response_alg since the - // selfRefreshingKeyProvider has access to the keys + boolean isSigned = contentType.contains(APPLICATION_JWT); + if (contentType.contains(APPLICATION_JWT)) { // We don't need the userinfo_encrypted_response_alg since the + // selfRefreshingKeyProvider has access to the keys claims = openIdJwtAuthenticator.getJwtClaimsSetFromInfoContent(userinfoContent); } else { claims = JWTClaimsSet.parse(userinfoContent); } String id = openIdJwtAuthenticator.getJwtClaimsSet(request).getSubject(); - String missing = validateResponseClaims(claims, id, isSigned); if (!missing.isBlank()) { throw new AuthenticatorUnavailableException( @@ -199,7 +218,14 @@ public AuthCredentials extractCredentials0(SecurityRequest request, ThreadContex } final String[] roles = openIdJwtAuthenticator.extractRoles(claims); - return new AuthCredentials(subject, roles).markComplete(); + + AuthCredentials ac = new AuthCredentials(subject, roles); + + for (Map.Entry claim : claims.getClaims().entrySet()) { + ac.addAttribute("attr.jwt." + claim.getKey(), String.valueOf(claim.getValue())); + } + + return ac.markComplete(); } catch (ParseException e) { throw new RuntimeException(e); } @@ -217,14 +243,12 @@ private String validateResponseClaims(JWTClaimsSet claims, String id, boolean is } if (isSigned) { - if (claims.getClaim("iss") == null - || claims.getClaim("iss").toString().isBlank() - || !claims.getClaim("iss").toString().equals(settings.get(ISSUER_ID_URL))) { + if (claims.getIssuer() == null || claims.getIssuer().isBlank() || !claims.getIssuer().equals(settings.get(ISSUER_ID_URL))) { missing = missing.concat("iss"); } - if (claims.getClaim("aud") == null - || claims.getClaim("aud").toString().isBlank() - || !claims.getClaim("aud").toString().equals(settings.get(CLIENT_ID))) { + if (claims.getAudience() == null + || claims.getAudience().toString().isBlank() + || !claims.getAudience().contains(settings.get(CLIENT_ID))) { missing = missing.concat("aud"); } } diff --git a/src/test/java/com/amazon/dlic/auth/http/jwt/keybyoidc/HTTPOpenIdAuthenticatorTests.java b/src/test/java/com/amazon/dlic/auth/http/jwt/keybyoidc/HTTPOpenIdAuthenticatorTests.java index c560ce0437..3e834c1b41 100644 --- a/src/test/java/com/amazon/dlic/auth/http/jwt/keybyoidc/HTTPOpenIdAuthenticatorTests.java +++ b/src/test/java/com/amazon/dlic/auth/http/jwt/keybyoidc/HTTPOpenIdAuthenticatorTests.java @@ -10,18 +10,10 @@ */ package com.amazon.dlic.auth.http.jwt.keybyoidc; -import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.List; import com.google.common.collect.ImmutableMap; -import com.nimbusds.jwt.JWTClaimsSet; -import org.apache.hc.client5.http.classic.methods.HttpGet; -import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; -import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; -import org.apache.hc.core5.http.ContentType; -import org.apache.hc.core5.http.HttpEntity; -import org.apache.hc.core5.http.io.entity.StringEntity; import org.junit.AfterClass; import org.junit.Assert; import org.junit.BeforeClass; @@ -32,55 +24,22 @@ import org.opensearch.security.user.AuthCredentials; import org.opensearch.security.util.FakeRestRequest; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static com.amazon.dlic.auth.http.jwt.keybyoidc.OpenIdConstants.APPLICATION_JWT; import static com.amazon.dlic.auth.http.jwt.keybyoidc.OpenIdConstants.CLIENT_ID; import static com.amazon.dlic.auth.http.jwt.keybyoidc.OpenIdConstants.ISSUER_ID_URL; import static com.amazon.dlic.auth.http.jwt.keybyoidc.TestJwts.MCCOY_SUBJECT; +import static com.amazon.dlic.auth.http.jwt.keybyoidc.TestJwts.OIDC_TEST_AUD; +import static com.amazon.dlic.auth.http.jwt.keybyoidc.TestJwts.OIDC_TEST_ISS; import static com.amazon.dlic.auth.http.jwt.keybyoidc.TestJwts.ROLES_CLAIM; -import static com.amazon.dlic.auth.http.jwt.keybyoidc.TestJwts.TEST_ROLES_STRING; -import static com.amazon.dlic.auth.http.jwt.keybyoidc.TestJwts.createSigned; -import static java.nio.charset.StandardCharsets.UTF_8; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.when; public class HTTPOpenIdAuthenticatorTests { protected static MockIpdServer mockIdpServer; - private HttpEntity getEntity(String type, String content) { - - HttpEntity testingEntity; - switch (type) { - case "json": - testingEntity = new StringEntity(content, ContentType.APPLICATION_JSON, UTF_8.displayName(), false); - break; - case "jwt": // There is no APPLICATION_JWT content type so have to make plain text and then convert - testingEntity = new StringEntity(content, ContentType.TEXT_PLAIN, UTF_8.displayName(), false); - break; - default: - testingEntity = new StringEntity(content, UTF_8); - break; - } - System.out.println("Created mock entity is: " + testingEntity); - return testingEntity; - } - - private String getMockedEntityContent(String sub, String roles, String iss, String aud, boolean isJwt) { - String content; - JWTClaimsSet claims = new JWTClaimsSet.Builder().claim("sub", sub).claim("roles", roles).claim("iss", iss).claim("aud", aud).build(); - if (isJwt) { - content = createSigned(claims, TestJwk.OCT_1); - } else { - content = claims.getClaims().toString(); - } - System.out.println("Created mock content is: " + content); - return content; - } - @BeforeClass public static void setUp() throws Exception { mockIdpServer = new MockIpdServer(TestJwk.Jwks.ALL); @@ -466,227 +425,269 @@ public void testPeculiarJsonEscaping() throws Exception { @Test public void userinfoEndpointReturnsJwtWithAllRequirementsTest() throws Exception { Settings settings = Settings.builder() - .put("openid_connect_url", mockIdpServer.getDiscoverUri()) - .put("userinfo_endpoint", mockIdpServer.getUserinfoUri()) - .put(CLIENT_ID, "testClient") - .put(ISSUER_ID_URL, "http://www.example.com") - .put("required_issuer", TestJwts.TEST_ISSUER) - .put("required_audience", TestJwts.TEST_AUDIENCE + ",another_audience") - .build(); - - CloseableHttpClient mockedHttpClient = mock(CloseableHttpClient.class); - CloseableHttpResponse mockedHttpResponse = spy(CloseableHttpResponse.class); + .put("openid_connect_url", mockIdpServer.getDiscoverUri()) + .put("userinfo_endpoint", mockIdpServer.getUserinfoSignedUri()) + .put(CLIENT_ID, OIDC_TEST_AUD) + .put(ISSUER_ID_URL, OIDC_TEST_ISS) + .build(); HTTPOpenIdAuthenticator openIdAuthenticator = spy(new HTTPOpenIdAuthenticator(settings, null)); - doReturn(mockedHttpClient).when(openIdAuthenticator.createHttpClient()); - when(mockedHttpClient.execute(any(HttpGet.class))).thenReturn(mockedHttpResponse); - doReturn(200).when(mockedHttpResponse.getCode()); - doReturn(getEntity("jwt", getMockedEntityContent(MCCOY_SUBJECT, TEST_ROLES_STRING, "http://www.example.com", "testClient", true))).when(mockedHttpResponse.getEntity()); - AuthCredentials creds = openIdAuthenticator.extractCredentials( - new FakeRestRequest(ImmutableMap.of("Authorization", TestJwts.MC_COY_SIGNED_OCT_1), new HashMap<>()).asSecurityRequest(), - null + new FakeRestRequest( + ImmutableMap.of("Authorization", "Bearer " + TestJwts.MC_COY_SIGNED_OCT_1, "Content-Type", APPLICATION_JWT), + new HashMap<>() + ).asSecurityRequest(), + null ); - System.out.println("Created creds are: " + creds); Assert.assertNotNull(creds); assertThat(creds.getUsername(), is(MCCOY_SUBJECT)); - assertThat(creds.getAttributes().get("attr.jwt.aud"), is(List.of( "testClient").toString())); - assertThat(creds.getAttributes().get("attr.jwt.iss"), is(List.of( "http://www.example.com").toString())); assertThat(creds.getBackendRoles().size(), is(0)); assertThat(creds.getAttributes().size(), is(4)); } @Test - public void userinfoEndpointReturnsJwtMissingIssuerTest() throws Exception { + public void userinfoEndpointReturnsJwtWithRequiredAudIssFailsTest() throws Exception { // Setting a required issuer or audience + // alongside userinfoendpoint settings causes + // failures in signed response cases Settings settings = Settings.builder() - .put("openid_connect_url", mockIdpServer.getDiscoverUri()) - .put("userinfo_endpoint", mockIdpServer.getUserinfoUri()) - .put(CLIENT_ID, "testClient") - .put(ISSUER_ID_URL, "http://www.example.com") - .put("required_issuer", TestJwts.TEST_ISSUER) - .put("required_audience", TestJwts.TEST_AUDIENCE + ",another_audience") - .build(); + .put("openid_connect_url", mockIdpServer.getDiscoverUri()) + .put("userinfo_endpoint", mockIdpServer.getUserinfoSignedUri()) + .put(CLIENT_ID, OIDC_TEST_AUD) + .put(ISSUER_ID_URL, OIDC_TEST_ISS) + .put("required_issuer", TestJwts.TEST_ISSUER) + .put("required_audience", TestJwts.TEST_AUDIENCE) + .build(); HTTPOpenIdAuthenticator openIdAuthenticator = new HTTPOpenIdAuthenticator(settings, null); - AuthCredentials creds = openIdAuthenticator.extractCredentials( - new FakeRestRequest(ImmutableMap.of("Authorization", TestJwts.MC_COY_SIGNED_OCT_1), new HashMap<>()).asSecurityRequest(), + AuthCredentials creds = null; + String message = ""; + try { + creds = openIdAuthenticator.extractCredentials( + new FakeRestRequest( + ImmutableMap.of("Authorization", "Bearer " + TestJwts.MC_COY_SIGNED_OCT_1, "Content-Type", APPLICATION_JWT), + new HashMap<>() + ).asSecurityRequest(), null - ); - - Assert.assertNotNull(creds); - assertThat(creds.getUsername(), is(MCCOY_SUBJECT)); - assertThat(creds.getAttributes().get("attr.jwt.aud"), is(List.of(TestJwts.TEST_AUDIENCE).toString())); - assertThat(creds.getBackendRoles().size(), is(0)); - assertThat(creds.getAttributes().size(), is(4)); + ); + } catch (RuntimeException e) { + message = e.getMessage(); + } + Assert.assertNull(creds); + assertTrue(message.contains("JWT audience rejected")); } @Test - public void userinfoEndpointReturnsJwtMissingAudienceTest() throws Exception { + public void userinfoEndpointReturnsJwtWithMatchingRequiredAudIssPassesTest() throws Exception { Settings settings = Settings.builder() - .put("openid_connect_url", mockIdpServer.getDiscoverUri()) - .put("userinfo_endpoint", mockIdpServer.getUserinfoUri()) - .put(CLIENT_ID, "testClient") - .put(ISSUER_ID_URL, "http://www.example.com") - .put("required_issuer", TestJwts.TEST_ISSUER) - .put("required_audience", TestJwts.TEST_AUDIENCE + ",another_audience") - .build(); + .put("openid_connect_url", mockIdpServer.getDiscoverUri()) + .put("userinfo_endpoint", mockIdpServer.getUserinfoSignedUri()) + .put(CLIENT_ID, OIDC_TEST_AUD) + .put(ISSUER_ID_URL, OIDC_TEST_ISS) + .put("required_issuer", OIDC_TEST_ISS) + .put("required_audience", OIDC_TEST_AUD) + .build(); HTTPOpenIdAuthenticator openIdAuthenticator = new HTTPOpenIdAuthenticator(settings, null); AuthCredentials creds = openIdAuthenticator.extractCredentials( - new FakeRestRequest(ImmutableMap.of("Authorization", TestJwts.MC_COY_SIGNED_OCT_1), new HashMap<>()).asSecurityRequest(), - null + new FakeRestRequest( + ImmutableMap.of("Authorization", "Bearer " + TestJwts.MC_COY_SIGNED_OCT_1_OIDC, "Content-Type", APPLICATION_JWT), + new HashMap<>() + ).asSecurityRequest(), + null ); - Assert.assertNotNull(creds); assertThat(creds.getUsername(), is(MCCOY_SUBJECT)); - assertThat(creds.getAttributes().get("attr.jwt.aud"), is(List.of(TestJwts.TEST_AUDIENCE).toString())); assertThat(creds.getBackendRoles().size(), is(0)); assertThat(creds.getAttributes().size(), is(4)); } @Test - public void userinfoEndpointReturnsJwtMismatchedSubTest() throws Exception { + public void userinfoEndpointReturnsJwtMissingIssuerTest() throws Exception { Settings settings = Settings.builder() - .put("openid_connect_url", mockIdpServer.getDiscoverUri()) - .put("userinfo_endpoint", mockIdpServer.getUserinfoUri()) - .put(CLIENT_ID, "testClient") - .put(ISSUER_ID_URL, "http://www.example.com") - .put("required_issuer", TestJwts.TEST_ISSUER) - .put("required_audience", TestJwts.TEST_AUDIENCE + ",another_audience") - .build(); + .put("openid_connect_url", mockIdpServer.getDiscoverUri()) + .put("userinfo_endpoint", mockIdpServer.getUserinfoSignedUri()) + .put(CLIENT_ID, OIDC_TEST_AUD) + .put(ISSUER_ID_URL, "http://www.differentexample.com") + .build(); HTTPOpenIdAuthenticator openIdAuthenticator = new HTTPOpenIdAuthenticator(settings, null); - AuthCredentials creds = openIdAuthenticator.extractCredentials( - new FakeRestRequest(ImmutableMap.of("Authorization", TestJwts.MC_COY_SIGNED_OCT_1), new HashMap<>()).asSecurityRequest(), + AuthCredentials creds = null; + String message = ""; + try { + creds = openIdAuthenticator.extractCredentials( + new FakeRestRequest( + ImmutableMap.of("Authorization", "Bearer " + TestJwts.MC_COY_SIGNED_OCT_1, "Content-Type", APPLICATION_JWT), + new HashMap<>() + ).asSecurityRequest(), null - ); - - Assert.assertNotNull(creds); - assertThat(creds.getUsername(), is(MCCOY_SUBJECT)); - assertThat(creds.getAttributes().get("attr.jwt.aud"), is(List.of(TestJwts.TEST_AUDIENCE).toString())); - assertThat(creds.getBackendRoles().size(), is(0)); - assertThat(creds.getAttributes().size(), is(4)); + ); + } catch (AuthenticatorUnavailableException e) { + message = e.getMessage(); + } + Assert.assertNull(creds); + assertTrue(message.contains("Missing or invalid required claims in response: iss")); } @Test - public void userinfoEndpointReturnsJwtInvalidAlgTest() throws Exception { + public void userinfoEndpointReturnsJwtMissingAudienceTest() throws Exception { Settings settings = Settings.builder() - .put("openid_connect_url", mockIdpServer.getDiscoverUri()) - .put("userinfo_endpoint", mockIdpServer.getUserinfoUri()) - .put(CLIENT_ID, "testClient") - .put(ISSUER_ID_URL, "http://www.example.com") - .put("required_issuer", TestJwts.TEST_ISSUER) - .put("required_audience", TestJwts.TEST_AUDIENCE + ",another_audience") - .build(); + .put("openid_connect_url", mockIdpServer.getDiscoverUri()) + .put("userinfo_endpoint", mockIdpServer.getUserinfoSignedUri()) + .put(CLIENT_ID, "aDifferentTestClient") + .put(ISSUER_ID_URL, "http://www.example.com") + .build(); HTTPOpenIdAuthenticator openIdAuthenticator = new HTTPOpenIdAuthenticator(settings, null); - AuthCredentials creds = openIdAuthenticator.extractCredentials( - new FakeRestRequest(ImmutableMap.of("Authorization", TestJwts.MC_COY_SIGNED_OCT_1), new HashMap<>()).asSecurityRequest(), + AuthCredentials creds = null; + String message = ""; + try { + creds = openIdAuthenticator.extractCredentials( + new FakeRestRequest( + ImmutableMap.of("Authorization", "Bearer " + TestJwts.MC_COY_SIGNED_OCT_1, "Content-Type", APPLICATION_JWT), + new HashMap<>() + ).asSecurityRequest(), null - ); + ); + } catch (AuthenticatorUnavailableException e) { + message = e.getMessage(); + } + Assert.assertNull(creds); + assertTrue(message.contains("Missing or invalid required claims in response: aud")); + } - Assert.assertNotNull(creds); - assertThat(creds.getUsername(), is(MCCOY_SUBJECT)); - assertThat(creds.getAttributes().get("attr.jwt.aud"), is(List.of(TestJwts.TEST_AUDIENCE).toString())); - assertThat(creds.getBackendRoles().size(), is(0)); - assertThat(creds.getAttributes().size(), is(4)); + @Test + public void userinfoEndpointReturnsJwtMismatchedSubTest() throws Exception { + Settings settings = Settings.builder() + .put("openid_connect_url", mockIdpServer.getDiscoverUri()) + .put("userinfo_endpoint", mockIdpServer.getUserinfoSignedUri()) + .put(CLIENT_ID, "testClient") + .put(ISSUER_ID_URL, "http://www.example.com") + .build(); + + HTTPOpenIdAuthenticator openIdAuthenticator = new HTTPOpenIdAuthenticator(settings, null); + AuthCredentials creds = null; + String message = ""; + try { + creds = openIdAuthenticator.extractCredentials( + new FakeRestRequest( + ImmutableMap.of("Authorization", "Bearer " + TestJwts.STEPHEN_RSA_1, "Content-Type", APPLICATION_JWT), + new HashMap<>() + ).asSecurityRequest(), + null + ); + } catch (AuthenticatorUnavailableException e) { + message = e.getMessage(); + } + Assert.assertNull(creds); + assertTrue(message.contains("Missing or invalid required claims in response: sub")); } @Test public void userinfoEndpointReturnsJsonWithAllRequirementsTest() throws Exception { Settings settings = Settings.builder() - .put("openid_connect_url", mockIdpServer.getDiscoverUri()) - .put("userinfo_endpoint", mockIdpServer.getUserinfoUri()) - .put("required_issuer", TestJwts.TEST_ISSUER) - .put("required_audience", TestJwts.TEST_AUDIENCE + ",another_audience") - .build(); + .put("openid_connect_url", mockIdpServer.getDiscoverUri()) + .put("userinfo_endpoint", mockIdpServer.getUserinfoUri()) + .put(CLIENT_ID, "testClient") + .put(ISSUER_ID_URL, "http://www.example.com") + .build(); - HTTPOpenIdAuthenticator openIdAuthenticator = new HTTPOpenIdAuthenticator(settings, null); + HTTPOpenIdAuthenticator openIdAuthenticator = spy(new HTTPOpenIdAuthenticator(settings, null)); AuthCredentials creds = openIdAuthenticator.extractCredentials( - new FakeRestRequest(ImmutableMap.of("Authorization", TestJwts.MC_COY_SIGNED_OCT_1), new HashMap<>()).asSecurityRequest(), - null + new FakeRestRequest( + ImmutableMap.of("Authorization", "Bearer " + TestJwts.MC_COY_SIGNED_OCT_1, "Content-Type", APPLICATION_JWT), + new HashMap<>() + ).asSecurityRequest(), + null ); - Assert.assertNotNull(creds); assertThat(creds.getUsername(), is(MCCOY_SUBJECT)); - assertThat(creds.getAttributes().get("attr.jwt.aud"), is(List.of(TestJwts.TEST_AUDIENCE).toString())); assertThat(creds.getBackendRoles().size(), is(0)); - assertThat(creds.getAttributes().size(), is(4)); + assertThat(creds.getAttributes().size(), is(2)); } @Test public void userinfoEndpointReturnsJsonMismatchedSubTest() throws Exception { Settings settings = Settings.builder() - .put("openid_connect_url", mockIdpServer.getDiscoverUri()) - .put("userinfo_endpoint", mockIdpServer.getUserinfoUri()) - .put("required_issuer", TestJwts.TEST_ISSUER) - .put("required_audience", TestJwts.TEST_AUDIENCE + ",another_audience") - .build(); + .put("openid_connect_url", mockIdpServer.getDiscoverUri()) + .put("userinfo_endpoint", mockIdpServer.getUserinfoUri()) + .put(CLIENT_ID, "testClient") + .put(ISSUER_ID_URL, "http://www.example.com") + .build(); HTTPOpenIdAuthenticator openIdAuthenticator = new HTTPOpenIdAuthenticator(settings, null); - - AuthCredentials creds = openIdAuthenticator.extractCredentials( - new FakeRestRequest(ImmutableMap.of("Authorization", TestJwts.MC_COY_SIGNED_OCT_1), new HashMap<>()).asSecurityRequest(), + AuthCredentials creds = null; + String message = ""; + try { + creds = openIdAuthenticator.extractCredentials( + new FakeRestRequest( + ImmutableMap.of("Authorization", "Bearer " + TestJwts.STEPHEN_RSA_1, "Content-Type", APPLICATION_JWT), + new HashMap<>() + ).asSecurityRequest(), null - ); - - Assert.assertNotNull(creds); - assertThat(creds.getUsername(), is(MCCOY_SUBJECT)); - assertThat(creds.getAttributes().get("attr.jwt.aud"), is(List.of(TestJwts.TEST_AUDIENCE).toString())); - assertThat(creds.getBackendRoles().size(), is(0)); - assertThat(creds.getAttributes().size(), is(4)); + ); + } catch (AuthenticatorUnavailableException e) { + message = e.getMessage(); + } + Assert.assertNull(creds); + assertTrue(message.contains("Missing or invalid required claims in response: sub")); } @Test public void userinfoEndpointReturnsResponseNot2xxTest() throws Exception { Settings settings = Settings.builder() - .put("openid_connect_url", mockIdpServer.getDiscoverUri()) - .put("userinfo_endpoint", mockIdpServer.getUserinfoUri()) - .put("required_issuer", TestJwts.TEST_ISSUER) - .put("required_audience", TestJwts.TEST_AUDIENCE + ",another_audience") - .build(); + .put("openid_connect_url", mockIdpServer.getDiscoverUri()) + .put("userinfo_endpoint", mockIdpServer.getUserinfoUri()) + .put("required_issuer", TestJwts.TEST_ISSUER) + .put("required_audience", TestJwts.TEST_AUDIENCE + ",another_audience") + .build(); HTTPOpenIdAuthenticator openIdAuthenticator = new HTTPOpenIdAuthenticator(settings, null); - - AuthCredentials creds = openIdAuthenticator.extractCredentials( - new FakeRestRequest(ImmutableMap.of("Authorization", TestJwts.MC_COY_SIGNED_OCT_1), new HashMap<>()).asSecurityRequest(), + AuthCredentials creds = null; + String message = ""; + try { + creds = openIdAuthenticator.extractCredentials( + new FakeRestRequest( + ImmutableMap.of("Authorization", TestJwts.MC_COY_SIGNED_OCT_1, "Content-Type", APPLICATION_JWT), + new HashMap<>() + ).asSecurityRequest(), null - ); - - Assert.assertNotNull(creds); - assertThat(creds.getUsername(), is(MCCOY_SUBJECT)); - assertThat(creds.getAttributes().get("attr.jwt.aud"), is(List.of(TestJwts.TEST_AUDIENCE).toString())); - assertThat(creds.getBackendRoles().size(), is(0)); - assertThat(creds.getAttributes().size(), is(4)); + ); + } catch (AuthenticatorUnavailableException e) { + message = e.getMessage(); + } + Assert.assertNull(creds); + assertTrue(message.contains("Error while getting")); } @Test - public void userinfoEndpointReturnsRequestNot2xxTest() throws Exception { + public void userinfoEndpointReturnsJsonWithRequiredAudIssPassesTest() throws Exception { Settings settings = Settings.builder() - .put("openid_connect_url", mockIdpServer.getDiscoverUri()) - .put("userinfo_endpoint", mockIdpServer.getUserinfoUri()) - .put("required_issuer", TestJwts.TEST_ISSUER) - .put("required_audience", TestJwts.TEST_AUDIENCE + ",another_audience") - .build(); + .put("openid_connect_url", mockIdpServer.getDiscoverUri()) + .put("userinfo_endpoint", mockIdpServer.getUserinfoUri()) + .put(CLIENT_ID, "testClient") + .put(ISSUER_ID_URL, "http://www.example.com") + .put("required_issuer", TestJwts.TEST_ISSUER) + .put("required_audience", TestJwts.TEST_AUDIENCE) + .build(); HTTPOpenIdAuthenticator openIdAuthenticator = new HTTPOpenIdAuthenticator(settings, null); AuthCredentials creds = openIdAuthenticator.extractCredentials( - new FakeRestRequest(ImmutableMap.of("Authorization", TestJwts.MC_COY_SIGNED_OCT_1), new HashMap<>()).asSecurityRequest(), - null + new FakeRestRequest( + ImmutableMap.of("Authorization", "Bearer " + TestJwts.MC_COY_SIGNED_OCT_1, "Content-Type", APPLICATION_JWT), + new HashMap<>() + ).asSecurityRequest(), + null ); - Assert.assertNotNull(creds); assertThat(creds.getUsername(), is(MCCOY_SUBJECT)); - assertThat(creds.getAttributes().get("attr.jwt.aud"), is(List.of(TestJwts.TEST_AUDIENCE).toString())); assertThat(creds.getBackendRoles().size(), is(0)); - assertThat(creds.getAttributes().size(), is(4)); + assertThat(creds.getAttributes().size(), is(2)); } - } diff --git a/src/test/java/com/amazon/dlic/auth/http/jwt/keybyoidc/MockIpdServer.java b/src/test/java/com/amazon/dlic/auth/http/jwt/keybyoidc/MockIpdServer.java index 94a0914d23..220005380c 100644 --- a/src/test/java/com/amazon/dlic/auth/http/jwt/keybyoidc/MockIpdServer.java +++ b/src/test/java/com/amazon/dlic/auth/http/jwt/keybyoidc/MockIpdServer.java @@ -18,19 +18,15 @@ import java.net.Socket; import java.security.GeneralSecurityException; import java.security.KeyStore; -import java.text.ParseException; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLParameters; import javax.net.ssl.TrustManagerFactory; -import com.nimbusds.jwt.JWTClaimsSet; import org.apache.hc.core5.function.Callback; import org.apache.hc.core5.http.ClassicHttpRequest; import org.apache.hc.core5.http.ClassicHttpResponse; -import org.apache.hc.core5.http.ContentType; import org.apache.hc.core5.http.Header; -import org.apache.hc.core5.http.HttpEntity; import org.apache.hc.core5.http.HttpException; import org.apache.hc.core5.http.HttpRequest; import org.apache.hc.core5.http.config.Http1Config; @@ -46,13 +42,17 @@ import org.opensearch.security.test.helper.network.SocketUtils; import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jwt.JWTClaimsSet; +import static com.amazon.dlic.auth.http.jwt.keybyoidc.OpenIdConstants.APPLICATION_JSON; +import static com.amazon.dlic.auth.http.jwt.keybyoidc.OpenIdConstants.APPLICATION_JWT; import static com.amazon.dlic.auth.http.jwt.keybyoidc.TestJwts.MCCOY_SUBJECT; import static com.amazon.dlic.auth.http.jwt.keybyoidc.TestJwts.TEST_ROLES_STRING; -import static org.mockito.Mockito.when; +import static com.amazon.dlic.auth.http.jwt.keybyoidc.TestJwts.createSigned; class MockIpdServer implements Closeable { final static String CTX_DISCOVER = "/discover"; + final static String CTX_USERINFO_SIGNED = "/api/oauth/userinfo/signed"; final static String CTX_USERINFO = "/api/oauth/userinfo"; final static String CTX_KEYS = "/api/oauth/keys"; @@ -94,9 +94,17 @@ public void handle(ClassicHttpRequest request, ClassicHttpResponse response, Htt @Override public void handle(ClassicHttpRequest request, ClassicHttpResponse response, HttpContext context) throws HttpException, - IOException { + IOException { handleUserinfoRequest(request, response, context); } + }) + .register(CTX_USERINFO_SIGNED, new HttpRequestHandler() { + + @Override + public void handle(ClassicHttpRequest request, ClassicHttpResponse response, HttpContext context) throws HttpException, + IOException { + handleUserinfoRequestSigned(request, response, context); + } }); if (ssl) { @@ -141,6 +149,10 @@ public String getUserinfoUri() { return uri + CTX_USERINFO; } + public String getUserinfoSignedUri() { + return uri + CTX_USERINFO_SIGNED; + } + public String getJwksUri() { return uri + CTX_KEYS; } @@ -160,32 +172,53 @@ protected void handleDiscoverRequest(HttpRequest request, ClassicHttpResponse re ); } - protected void handleUserinfoRequestUnencrypted(HttpRequest request, ClassicHttpResponse response, HttpContext context) throws HttpException, - IOException, ParseException { + protected void handleUserinfoRequestSigned(HttpRequest request, ClassicHttpResponse response, HttpContext context) throws HttpException, + IOException { + + Header headers = request.getHeader("Authorization"); + String requestToken; + + String authHeaderValue = headers.getValue(); + if (authHeaderValue.startsWith("[Bearer")) { + requestToken = authHeaderValue.substring(7).trim(); + } else { + response.setCode(401); + return; + } + + response.setCode(200); + response.setHeader("content-type", APPLICATION_JWT); + + // We have to manually form the response content since we don't want to need to pass settings info into the test class + JWTClaimsSet claims = new JWTClaimsSet.Builder().claim("sub", MCCOY_SUBJECT) + .claim("roles", TEST_ROLES_STRING) + .claim("iss", "http://www.example.com") + .claim("aud", "testClient") + .build(); + String content = createSigned(claims, TestJwk.OCT_1); + + response.setEntity(new StringEntity(content)); + } - Header[] headers = request.getHeaders("Authorization"); + protected void handleUserinfoRequest(HttpRequest request, ClassicHttpResponse response, HttpContext context) throws HttpException, + IOException { + Header headers = request.getHeader("Authorization"); String requestToken; - // Check if the "Authorization" header is present - if (headers.length > 0) { - // Parse the "Authorization" header value - String authHeaderValue = headers[0].getValue(); - if (authHeaderValue.startsWith("Bearer")) { - requestToken = authHeaderValue.substring(7).trim(); - } - else { - response.setCode(401); - return; - } + + String authHeaderValue = headers.getValue(); + if (authHeaderValue.startsWith("[Bearer")) { + requestToken = authHeaderValue.substring(7).trim(); } else { - response.setCode(401); - return; + response.setCode(401); + return; } - JWTClaimsSet claims = JWTClaimsSet.parse(requestToken); response.setCode(200); - response.setHeader("content-type", ContentType.APPLICATION_JSON); - response.setEntity(new StringEntity()); + response.setHeader("content-type", APPLICATION_JSON); + // We have to manually form the response content since we don't want to need to pass settings info into the test class + JWTClaimsSet claims = new JWTClaimsSet.Builder().claim("sub", MCCOY_SUBJECT).claim("roles", TEST_ROLES_STRING).build(); + response.setEntity(new StringEntity(claims.toString())); } protected void handleKeysRequest(HttpRequest request, ClassicHttpResponse response, HttpContext context) throws HttpException, diff --git a/src/test/java/com/amazon/dlic/auth/http/jwt/keybyoidc/TestJwts.java b/src/test/java/com/amazon/dlic/auth/http/jwt/keybyoidc/TestJwts.java index 4a6d5f97e9..aa6d53f696 100644 --- a/src/test/java/com/amazon/dlic/auth/http/jwt/keybyoidc/TestJwts.java +++ b/src/test/java/com/amazon/dlic/auth/http/jwt/keybyoidc/TestJwts.java @@ -33,14 +33,24 @@ class TestJwts { static final Set TEST_ROLES = ImmutableSet.of("role1", "role2"); static final String TEST_ROLES_STRING = Strings.join(TEST_ROLES, ','); + static final String OIDC_TEST_AUD = "testClient"; + + static final String OIDC_TEST_ISS = "http://www.example.com"; + static final String TEST_AUDIENCE = "TestAudience"; static final String MCCOY_SUBJECT = "Leonard McCoy"; + static final String STEPHEN_SUBJECT = "Stephen Crawford"; + static final String TEST_ISSUER = "TestIssuer"; + static final JWTClaimsSet STEPHEN = create(STEPHEN_SUBJECT, TEST_AUDIENCE, TEST_ISSUER, ROLES_CLAIM, TEST_ROLES_STRING); + static final JWTClaimsSet MC_COY = create(MCCOY_SUBJECT, TEST_AUDIENCE, TEST_ISSUER, ROLES_CLAIM, TEST_ROLES_STRING); + static final JWTClaimsSet MC_COY_OIDC = create(MCCOY_SUBJECT, OIDC_TEST_AUD, OIDC_TEST_ISS, ROLES_CLAIM, TEST_ROLES_STRING); + static final JWTClaimsSet MC_COY_2 = create(MCCOY_SUBJECT, TEST_AUDIENCE, TEST_ISSUER, ROLES_CLAIM, TEST_ROLES_STRING); static final JWTClaimsSet MC_COY_NO_AUDIENCE = create(MCCOY_SUBJECT, null, TEST_ISSUER, ROLES_CLAIM, TEST_ROLES_STRING); @@ -59,6 +69,8 @@ class TestJwts { static final String MC_COY_SIGNED_OCT_1 = createSigned(MC_COY, TestJwk.OCT_1); + static final String MC_COY_SIGNED_OCT_1_OIDC = createSigned(MC_COY_OIDC, TestJwk.OCT_1); + static final String MC_COY_SIGNED_OCT_2 = createSigned(MC_COY_2, TestJwk.OCT_2); static final String MC_COY_SIGNED_NO_AUDIENCE_OCT_1 = createSigned(MC_COY_NO_AUDIENCE, TestJwk.OCT_1); @@ -68,6 +80,8 @@ class TestJwts { static final String MC_COY_SIGNED_RSA_1 = createSigned(MC_COY, TestJwk.RSA_1); + static final String STEPHEN_RSA_1 = createSigned(STEPHEN, TestJwk.RSA_1); + static final String MC_COY_SIGNED_RSA_X = createSigned(MC_COY, TestJwk.RSA_X); static final String MC_COY_EXPIRED_SIGNED_OCT_1 = createSigned(MC_COY_EXPIRED, TestJwk.OCT_1);