Skip to content

Commit c837dcf

Browse files
OIDC UserInfo Endpoint
Signed-off-by: Stephen Crawford <[email protected]>
1 parent b601a92 commit c837dcf

File tree

5 files changed

+137
-80
lines changed

5 files changed

+137
-80
lines changed

build.gradle

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -607,9 +607,12 @@ dependencies {
607607
runtimeOnly 'jakarta.xml.bind:jakarta.xml.bind-api:4.0.2'
608608
runtimeOnly 'org.ow2.asm:asm:9.7'
609609

610+
implementation 'com.nimbusds:oauth2-oidc-sdk:11.18'
611+
implementation 'net.minidev:json-smart:2.5.1'
612+
implementation 'com.nimbusds:content-type:2.3'
613+
610614
testImplementation 'org.apache.camel:camel-xmlsecurity:3.22.2'
611615
testImplementation 'org.mockito:mockito-inline:5.2.0'
612-
//testImplementation 'org.mockito:mockito-inline:2.13.0'
613616
//OpenSAML
614617
implementation 'net.shibboleth.utilities:java-support:8.4.2'
615618
runtimeOnly "io.dropwizard.metrics:metrics-core:4.2.27"

src/main/java/com/amazon/dlic/auth/http/jwt/keybyoidc/HTTPOpenIdAuthenticator.java

Lines changed: 48 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -12,24 +12,18 @@
1212
package com.amazon.dlic.auth.http.jwt.keybyoidc;
1313

1414
import java.io.IOException;
15-
import java.io.InputStream;
16-
import java.io.InputStreamReader;
17-
import java.nio.charset.StandardCharsets;
15+
import java.net.URI;
16+
import java.net.URISyntaxException;
1817
import java.nio.file.Path;
1918
import java.text.ParseException;
2019
import java.util.Map;
2120
import java.util.Optional;
22-
import java.util.concurrent.TimeUnit;
2321

24-
import org.apache.hc.client5.http.classic.methods.HttpGet;
25-
import org.apache.hc.client5.http.config.RequestConfig;
2622
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
27-
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
2823
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
2924
import org.apache.hc.client5.http.impl.classic.HttpClients;
3025
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
3126
import org.apache.hc.client5.http.io.HttpClientConnectionManager;
32-
import org.apache.hc.core5.http.HttpEntity;
3327
import org.apache.logging.log4j.LogManager;
3428
import org.apache.logging.log4j.Logger;
3529

@@ -43,12 +37,18 @@
4337

4438
import com.amazon.dlic.auth.http.jwt.AbstractHTTPJwtAuthenticator;
4539
import com.amazon.dlic.util.SettingsBasedSSLConfigurator;
40+
import com.nimbusds.common.contenttype.ContentType;
4641
import com.nimbusds.jwt.JWTClaimsSet;
4742
import com.nimbusds.jwt.SignedJWT;
43+
import com.nimbusds.oauth2.sdk.http.HTTPRequest;
44+
import com.nimbusds.oauth2.sdk.http.HTTPResponse;
45+
import com.nimbusds.oauth2.sdk.token.AccessToken;
46+
import com.nimbusds.oauth2.sdk.token.AccessTokenType;
47+
import com.nimbusds.oauth2.sdk.util.StringUtils;
48+
import com.nimbusds.openid.connect.sdk.UserInfoRequest;
49+
import com.nimbusds.openid.connect.sdk.UserInfoResponse;
4850

49-
import static org.apache.hc.core5.http.ContentType.APPLICATION_JSON;
5051
import static org.apache.hc.core5.http.HttpHeaders.AUTHORIZATION;
51-
import static com.amazon.dlic.auth.http.jwt.keybyoidc.OpenIdConstants.APPLICATION_JWT;
5252
import static com.amazon.dlic.auth.http.jwt.keybyoidc.OpenIdConstants.CLIENT_ID;
5353
import static com.amazon.dlic.auth.http.jwt.keybyoidc.OpenIdConstants.ISSUER_ID_URL;
5454
import static com.amazon.dlic.auth.http.jwt.keybyoidc.OpenIdConstants.SUB_CLAIM;
@@ -139,67 +139,58 @@ public CloseableHttpClient createHttpClient() {
139139
*/
140140
public AuthCredentials extractCredentials0(SecurityRequest request, ThreadContext context) throws OpenSearchSecurityException {
141141

142-
try (CloseableHttpClient httpClient = createHttpClient()) {
142+
try {
143143

144-
HttpGet httpGet = new HttpGet(this.userInfoEndpoint);
144+
URI userInfoEndpointURI = new URI(this.userInfoEndpoint);
145145

146-
RequestConfig requestConfig = RequestConfig.custom()
147-
.setConnectionRequestTimeout(requestTimeoutMs, TimeUnit.MILLISECONDS)
148-
.setConnectTimeout(requestTimeoutMs, TimeUnit.MILLISECONDS)
149-
.build();
150-
151-
httpGet.setConfig(requestConfig);
152-
httpGet.addHeader(AUTHORIZATION, request.getHeaders().get(AUTHORIZATION));
146+
String bearerHeader = request.getHeaders().get(AUTHORIZATION).getFirst();
147+
if (!StringUtils.isBlank(bearerHeader)) {
148+
if (bearerHeader.contains("Bearer ")) {
149+
bearerHeader = bearerHeader.substring(7);
150+
}
151+
}
153152

154-
// HTTPGet should internally verify the appropriate TLS cert.
155-
try (CloseableHttpResponse response = httpClient.execute(httpGet)) {
153+
String finalBearerHeader = bearerHeader;
156154

157-
if (response.getCode() < 200 || response.getCode() >= 300) {
158-
throw new AuthenticatorUnavailableException(
159-
"Error while getting " + this.userInfoEndpoint + ": " + response.getReasonPhrase()
160-
);
155+
AccessToken accessToken = new AccessToken(AccessTokenType.BEARER, finalBearerHeader) {
156+
@Override
157+
public String toAuthorizationHeader() {
158+
return "Bearer " + finalBearerHeader;
161159
}
160+
};
162161

163-
HttpEntity httpEntity = response.getEntity();
162+
UserInfoRequest userInfoRequest = new UserInfoRequest(userInfoEndpointURI, accessToken);
164163

165-
if (httpEntity == null) {
166-
throw new AuthenticatorUnavailableException("Error while getting " + this.userInfoEndpoint + ": Empty response entity");
167-
}
164+
HTTPRequest httpRequest = userInfoRequest.toHTTPRequest();
168165

169-
String contentType = httpEntity.getContentType();
170-
if (!contentType.contains(APPLICATION_JSON.getMimeType()) && !contentType.contains(APPLICATION_JWT)) {
171-
throw new AuthenticatorUnavailableException(
172-
"Error while getting " + this.userInfoEndpoint + ": Invalid content type in response"
173-
);
174-
}
166+
HTTPResponse httpResponse = httpRequest.send();
167+
if (httpResponse.getStatusCode() < 200 || httpResponse.getStatusCode() >= 300) {
168+
throw new AuthenticatorUnavailableException(
169+
"Error while getting " + this.userInfoEndpoint + ": " + httpResponse.getStatusMessage()
170+
);
171+
}
172+
173+
try {
174+
175+
UserInfoResponse userInfoResponse = UserInfoResponse.parse(httpResponse);
175176

176-
String userinfoContent;
177-
178-
try (
179-
// got this from ChatGpt & Amazon Q
180-
InputStream inputStream = httpEntity.getContent();
181-
InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8)
182-
) {
183-
StringBuilder content = new StringBuilder();
184-
char[] buffer = new char[8192];
185-
int bytesRead;
186-
while ((bytesRead = reader.read(buffer)) != -1) {
187-
content.append(buffer, 0, bytesRead);
188-
}
189-
userinfoContent = content.toString();
190-
} catch (IOException e) {
177+
if (!userInfoResponse.indicatesSuccess()) {
191178
throw new AuthenticatorUnavailableException(
192-
"Error while getting " + this.userInfoEndpoint + ": Unable to read response content"
179+
"Error while getting " + this.userInfoEndpoint + ": " + userInfoResponse.toErrorResponse()
193180
);
194181
}
195182

183+
String contentType = String.valueOf(httpResponse.getHeaderValues("content-type"));
184+
196185
JWTClaimsSet claims;
197-
boolean isSigned = contentType.contains(APPLICATION_JWT);
198-
if (contentType.contains(APPLICATION_JWT)) { // We don't need the userinfo_encrypted_response_alg since the
199-
// selfRefreshingKeyProvider has access to the keys
200-
claims = openIdJwtAuthenticator.getJwtClaimsSetFromInfoContent(userinfoContent);
186+
boolean isSigned = contentType.contains(ContentType.APPLICATION_JWT.toString());
187+
if (isSigned) { // We don't need the userinfo_encrypted_response_alg since the
188+
// selfRefreshingKeyProvider has access to the keys
189+
claims = openIdJwtAuthenticator.getJwtClaimsSetFromInfoContent(
190+
userInfoResponse.toSuccessResponse().getUserInfoJWT().getParsedString()
191+
);
201192
} else {
202-
claims = JWTClaimsSet.parse(userinfoContent);
193+
claims = JWTClaimsSet.parse(userInfoResponse.toSuccessResponse().getUserInfo().toString());
203194
}
204195

205196
String id = openIdJwtAuthenticator.getJwtClaimsSet(request).getSubject();
@@ -228,7 +219,7 @@ public AuthCredentials extractCredentials0(SecurityRequest request, ThreadContex
228219
} catch (ParseException e) {
229220
throw new RuntimeException(e);
230221
}
231-
} catch (IOException e) {
222+
} catch (IOException | URISyntaxException | com.nimbusds.oauth2.sdk.ParseException e) {
232223
throw new AuthenticatorUnavailableException("Error while getting " + this.userInfoEndpoint + ": " + e, e);
233224
}
234225
}

src/main/java/com/amazon/dlic/auth/http/jwt/keybyoidc/OpenIdConstants.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313

1414
public class OpenIdConstants {
1515

16-
public static final String APPLICATION_JWT = "application/jwt";
1716
public static final String CLIENT_ID = "client_id";
1817
public static final String ISSUER_ID_URL = "issuer_id_url";
1918
public static final String SUB_CLAIM = "sub";

src/test/java/com/amazon/dlic/auth/http/jwt/keybyoidc/HTTPOpenIdAuthenticatorTests.java

Lines changed: 61 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,17 @@
2424
import org.opensearch.security.user.AuthCredentials;
2525
import org.opensearch.security.util.FakeRestRequest;
2626

27+
import com.nimbusds.common.contenttype.ContentType;
28+
2729
import static org.hamcrest.MatcherAssert.assertThat;
2830
import static org.hamcrest.Matchers.is;
29-
import static com.amazon.dlic.auth.http.jwt.keybyoidc.OpenIdConstants.APPLICATION_JWT;
3031
import static com.amazon.dlic.auth.http.jwt.keybyoidc.OpenIdConstants.CLIENT_ID;
3132
import static com.amazon.dlic.auth.http.jwt.keybyoidc.OpenIdConstants.ISSUER_ID_URL;
3233
import static com.amazon.dlic.auth.http.jwt.keybyoidc.TestJwts.MCCOY_SUBJECT;
3334
import static com.amazon.dlic.auth.http.jwt.keybyoidc.TestJwts.OIDC_TEST_AUD;
3435
import static com.amazon.dlic.auth.http.jwt.keybyoidc.TestJwts.OIDC_TEST_ISS;
3536
import static com.amazon.dlic.auth.http.jwt.keybyoidc.TestJwts.ROLES_CLAIM;
37+
import static com.amazon.dlic.auth.http.jwt.keybyoidc.TestJwts.STEPHEN_SUBJECT;
3638
import static org.junit.Assert.assertTrue;
3739
import static org.mockito.Mockito.spy;
3840

@@ -435,7 +437,12 @@ public void userinfoEndpointReturnsJwtWithAllRequirementsTest() throws Exception
435437

436438
AuthCredentials creds = openIdAuthenticator.extractCredentials(
437439
new FakeRestRequest(
438-
ImmutableMap.of("Authorization", "Bearer " + TestJwts.MC_COY_SIGNED_OCT_1, "Content-Type", APPLICATION_JWT),
440+
ImmutableMap.of(
441+
"Authorization",
442+
"Bearer " + TestJwts.MC_COY_SIGNED_OCT_1,
443+
"Content-Type",
444+
ContentType.APPLICATION_JWT.toString()
445+
),
439446
new HashMap<>()
440447
).asSecurityRequest(),
441448
null
@@ -448,8 +455,8 @@ public void userinfoEndpointReturnsJwtWithAllRequirementsTest() throws Exception
448455

449456
@Test
450457
public void userinfoEndpointReturnsJwtWithRequiredAudIssFailsTest() throws Exception { // Setting a required issuer or audience
451-
// alongside userinfo endpoint settings causes
452-
// failures in signed response cases
458+
// alongside userinfo endpoint settings causes
459+
// failures in signed response cases
453460
Settings settings = Settings.builder()
454461
.put("openid_connect_url", mockIdpServer.getDiscoverUri())
455462
.put("userinfo_endpoint", mockIdpServer.getUserinfoSignedUri())
@@ -466,7 +473,12 @@ public void userinfoEndpointReturnsJwtWithRequiredAudIssFailsTest() throws Excep
466473
try {
467474
creds = openIdAuthenticator.extractCredentials(
468475
new FakeRestRequest(
469-
ImmutableMap.of("Authorization", "Bearer " + TestJwts.MC_COY_SIGNED_OCT_1, "Content-Type", APPLICATION_JWT),
476+
ImmutableMap.of(
477+
"Authorization",
478+
"Bearer " + TestJwts.MC_COY_SIGNED_OCT_1,
479+
"Content-Type",
480+
ContentType.APPLICATION_JWT.toString()
481+
),
470482
new HashMap<>()
471483
).asSecurityRequest(),
472484
null
@@ -493,7 +505,12 @@ public void userinfoEndpointReturnsJwtWithMatchingRequiredAudIssPassesTest() thr
493505

494506
AuthCredentials creds = openIdAuthenticator.extractCredentials(
495507
new FakeRestRequest(
496-
ImmutableMap.of("Authorization", "Bearer " + TestJwts.MC_COY_SIGNED_OCT_1_OIDC, "Content-Type", APPLICATION_JWT),
508+
ImmutableMap.of(
509+
"Authorization",
510+
"Bearer " + TestJwts.MC_COY_SIGNED_OCT_1_OIDC,
511+
"Content-Type",
512+
ContentType.APPLICATION_JWT.toString()
513+
),
497514
new HashMap<>()
498515
).asSecurityRequest(),
499516
null
@@ -520,7 +537,12 @@ public void userinfoEndpointReturnsJwtMissingIssuerTest() throws Exception {
520537
try {
521538
creds = openIdAuthenticator.extractCredentials(
522539
new FakeRestRequest(
523-
ImmutableMap.of("Authorization", "Bearer " + TestJwts.MC_COY_SIGNED_OCT_1, "Content-Type", APPLICATION_JWT),
540+
ImmutableMap.of(
541+
"Authorization",
542+
"Bearer " + TestJwts.MC_COY_SIGNED_OCT_1,
543+
"Content-Type",
544+
ContentType.APPLICATION_JWT.toString()
545+
),
524546
new HashMap<>()
525547
).asSecurityRequest(),
526548
null
@@ -548,7 +570,12 @@ public void userinfoEndpointReturnsJwtMissingAudienceTest() throws Exception {
548570
try {
549571
creds = openIdAuthenticator.extractCredentials(
550572
new FakeRestRequest(
551-
ImmutableMap.of("Authorization", "Bearer " + TestJwts.MC_COY_SIGNED_OCT_1, "Content-Type", APPLICATION_JWT),
573+
ImmutableMap.of(
574+
"Authorization",
575+
"Bearer " + TestJwts.MC_COY_SIGNED_OCT_1,
576+
"Content-Type",
577+
ContentType.APPLICATION_JWT.toString()
578+
),
552579
new HashMap<>()
553580
).asSecurityRequest(),
554581
null
@@ -575,7 +602,12 @@ public void userinfoEndpointReturnsJwtMismatchedSubTest() throws Exception {
575602
try {
576603
creds = openIdAuthenticator.extractCredentials(
577604
new FakeRestRequest(
578-
ImmutableMap.of("Authorization", "Bearer " + TestJwts.STEPHEN_RSA_1, "Content-Type", APPLICATION_JWT),
605+
ImmutableMap.of(
606+
"Authorization",
607+
"Bearer " + TestJwts.STEPHEN_RSA_1,
608+
"Content-Type",
609+
ContentType.APPLICATION_JWT.toString()
610+
),
579611
new HashMap<>()
580612
).asSecurityRequest(),
581613
null
@@ -600,7 +632,12 @@ public void userinfoEndpointReturnsJsonWithAllRequirementsTest() throws Exceptio
600632

601633
AuthCredentials creds = openIdAuthenticator.extractCredentials(
602634
new FakeRestRequest(
603-
ImmutableMap.of("Authorization", "Bearer " + TestJwts.MC_COY_SIGNED_OCT_1, "Content-Type", APPLICATION_JWT),
635+
ImmutableMap.of(
636+
"Authorization",
637+
"Bearer " + TestJwts.MC_COY_SIGNED_OCT_1,
638+
"Content-Type",
639+
ContentType.APPLICATION_JWT.toString()
640+
),
604641
new HashMap<>()
605642
).asSecurityRequest(),
606643
null
@@ -626,7 +663,12 @@ public void userinfoEndpointReturnsJsonMismatchedSubTest() throws Exception {
626663
try {
627664
creds = openIdAuthenticator.extractCredentials(
628665
new FakeRestRequest(
629-
ImmutableMap.of("Authorization", "Bearer " + TestJwts.STEPHEN_RSA_1, "Content-Type", APPLICATION_JWT),
666+
ImmutableMap.of(
667+
"Authorization",
668+
"Bearer " + TestJwts.STEPHEN_RSA_1,
669+
"Content-Type",
670+
ContentType.APPLICATION_JWT.toString()
671+
),
630672
new HashMap<>()
631673
).asSecurityRequest(),
632674
null
@@ -642,7 +684,7 @@ public void userinfoEndpointReturnsJsonMismatchedSubTest() throws Exception {
642684
public void userinfoEndpointReturnsResponseNot2xxTest() throws Exception {
643685
Settings settings = Settings.builder()
644686
.put("openid_connect_url", mockIdpServer.getDiscoverUri())
645-
.put("userinfo_endpoint", mockIdpServer.getUserinfoUri())
687+
.put("userinfo_endpoint", mockIdpServer.getBadUserInfoUri())
646688
.put("required_issuer", TestJwts.TEST_ISSUER)
647689
.put("required_audience", TestJwts.TEST_AUDIENCE + ",another_audience")
648690
.build();
@@ -653,7 +695,7 @@ public void userinfoEndpointReturnsResponseNot2xxTest() throws Exception {
653695
try {
654696
creds = openIdAuthenticator.extractCredentials(
655697
new FakeRestRequest(
656-
ImmutableMap.of("Authorization", TestJwts.MC_COY_SIGNED_OCT_1, "Content-Type", APPLICATION_JWT),
698+
ImmutableMap.of("Authorization", STEPHEN_SUBJECT, "Content-Type", ContentType.APPLICATION_JWT.toString()),
657699
new HashMap<>()
658700
).asSecurityRequest(),
659701
null
@@ -680,7 +722,12 @@ public void userinfoEndpointReturnsJsonWithRequiredAudIssPassesTest() throws Exc
680722

681723
AuthCredentials creds = openIdAuthenticator.extractCredentials(
682724
new FakeRestRequest(
683-
ImmutableMap.of("Authorization", "Bearer " + TestJwts.MC_COY_SIGNED_OCT_1, "Content-Type", APPLICATION_JWT),
725+
ImmutableMap.of(
726+
"Authorization",
727+
"Bearer " + TestJwts.MC_COY_SIGNED_OCT_1,
728+
"Content-Type",
729+
ContentType.APPLICATION_JWT.toString()
730+
),
684731
new HashMap<>()
685732
).asSecurityRequest(),
686733
null

0 commit comments

Comments
 (0)