Skip to content

Commit

Permalink
OIDC UserInfo Endpoint
Browse files Browse the repository at this point in the history
Signed-off-by: Stephen Crawford <[email protected]>
  • Loading branch information
stephen-crawford committed Aug 27, 2024
1 parent 2d8445f commit 84732b8
Show file tree
Hide file tree
Showing 5 changed files with 318 additions and 354 deletions.
5 changes: 5 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,11 @@ dependencies {
implementation "org.opensaml:opensaml-storage-api:${open_saml_version}"

implementation "com.nulab-inc:zxcvbn:1.9.0"
implementation 'com.nimbusds:oauth2-oidc-sdk:11.18'
implementation 'net.minidev:json-smart:2.5.1'
implementation 'com.nimbusds:content-type:2.3'

testImplementation 'org.apache.camel:camel-xmlsecurity:3.22.2'

runtimeOnly 'com.google.guava:failureaccess:1.0.2'
runtimeOnly 'org.apache.commons:commons-text:1.11.0'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,23 @@

package com.amazon.dlic.auth.http.jwt.keybyoidc;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.text.ParseException;
import java.util.Map;
import java.util.Optional;

import org.apache.http.HttpEntity;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import com.amazon.dlic.auth.http.jwt.AbstractHTTPJwtAuthenticator;
import com.amazon.dlic.util.SettingsBasedSSLConfigurator;
import com.nimbusds.common.contenttype.ContentType;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import com.nimbusds.oauth2.sdk.http.HTTPRequest;
import com.nimbusds.oauth2.sdk.http.HTTPResponse;
import com.nimbusds.oauth2.sdk.token.AccessToken;
import com.nimbusds.oauth2.sdk.token.AccessTokenType;
import com.nimbusds.openid.connect.sdk.UserInfoRequest;
import com.nimbusds.openid.connect.sdk.UserInfoResponse;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import org.opensearch.OpenSearchSecurityException;
import org.opensearch.common.settings.Settings;
import org.opensearch.common.util.concurrent.ThreadContext;
Expand All @@ -38,17 +36,29 @@
import org.opensearch.security.filter.SecurityResponse;
import org.opensearch.security.user.AuthCredentials;

import com.amazon.dlic.auth.http.jwt.AbstractHTTPJwtAuthenticator;
import com.amazon.dlic.util.SettingsBasedSSLConfigurator;
import com.nimbusds.common.contenttype.ContentType;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import com.nimbusds.oauth2.sdk.http.HTTPRequest;
import com.nimbusds.oauth2.sdk.http.HTTPResponse;
import com.nimbusds.oauth2.sdk.token.AccessToken;
import com.nimbusds.oauth2.sdk.token.AccessTokenType;
import com.nimbusds.oauth2.sdk.util.StringUtils;
import com.nimbusds.openid.connect.sdk.UserInfoRequest;
import com.nimbusds.openid.connect.sdk.UserInfoResponse;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Path;
import java.text.ParseException;
import java.util.Map;
import java.util.Optional;

import static org.apache.http.HttpHeaders.AUTHORIZATION;
import static org.apache.http.entity.ContentType.APPLICATION_JSON;
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.OpenIdConstants.SUB_CLAIM;
import static org.apache.http.HttpHeaders.AUTHORIZATION;

public class HTTPOpenIdAuthenticator implements HTTPAuthenticator {

Expand Down Expand Up @@ -100,18 +110,6 @@ public Optional<SecurityResponse> reRequestAuthentication(SecurityRequest reques
return Optional.empty();
}

// Public for testing
public CloseableHttpClient createHttpClient() {
HttpClientBuilder builder;
builder = HttpClients.custom();
builder.useSystemProperties();
if (sslConfig != null) {
builder.setSSLSocketFactory(sslConfig.toSSLConnectionSocketFactory());
}

return builder.build();
}

/**
* This method performs the logic required for making use of the userinfo_endpoint OIDC feature.
* Per the spec: https://openid.net/specs/openid-connect-core-1_0.html#UserInfo there are 10 verification steps we must perform
Expand All @@ -132,74 +130,65 @@ public CloseableHttpClient createHttpClient() {
*/
public AuthCredentials extractCredentials0(SecurityRequest request, ThreadContext context) throws OpenSearchSecurityException {

try (CloseableHttpClient httpClient = createHttpClient()) {

HttpGet httpGet = new HttpGet(this.userInfoEndpoint);
try {

RequestConfig requestConfig = RequestConfig.custom()
.setConnectionRequestTimeout(requestTimeoutMs)
.setConnectTimeout(requestTimeoutMs)
.build();
URI userInfoEndpointURI = new URI(this.userInfoEndpoint);

httpGet.setConfig(requestConfig);
httpGet.addHeader(AUTHORIZATION, request.getHeaders().get(AUTHORIZATION).get(0));
String bearerHeader = request.getHeaders().get(AUTHORIZATION).getFirst();
if (!StringUtils.isBlank(bearerHeader)) {
if (bearerHeader.contains("Bearer ")) {
bearerHeader = bearerHeader.substring(7);
}
}

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

if (response.getStatusLine().getStatusCode() < 200 || response.getStatusLine().getStatusCode() >= 300) {
throw new AuthenticatorUnavailableException(
"Error while getting " + this.userInfoEndpoint + ": Invalid status code " + response.getStatusLine().getStatusCode()
);
AccessToken accessToken = new AccessToken(AccessTokenType.BEARER, finalBearerHeader) {
@Override
public String toAuthorizationHeader() {
return "Bearer " + finalBearerHeader;
}
};

HttpEntity httpEntity = response.getEntity();
UserInfoRequest userInfoRequest = new UserInfoRequest(userInfoEndpointURI, accessToken);

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

String contentType = httpEntity.getContentType().getValue();
if (!contentType.contains(APPLICATION_JSON.getMimeType()) && !contentType.contains(APPLICATION_JWT)) {
throw new AuthenticatorUnavailableException(
"Error while getting " + this.userInfoEndpoint + ": Invalid content type in response"
);
}
HTTPResponse httpResponse = httpRequest.send();
if (httpResponse.getStatusCode() < 200 || httpResponse.getStatusCode() >= 300) {
throw new AuthenticatorUnavailableException(
"Error while getting " + this.userInfoEndpoint + ": " + httpResponse.getStatusMessage()
);
}

String userinfoContent;

try (
// got this from ChatGpt & Amazon Q
InputStream inputStream = httpEntity.getContent();
InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8)
) {
StringBuilder content = new StringBuilder();
char[] buffer = new char[8192];
int bytesRead;
while ((bytesRead = reader.read(buffer)) != -1) {
content.append(buffer, 0, bytesRead);
}
userinfoContent = content.toString();
} catch (IOException e) {
try {

UserInfoResponse userInfoResponse = UserInfoResponse.parse(httpResponse);

if (!userInfoResponse.indicatesSuccess()) {
throw new AuthenticatorUnavailableException(
"Error while getting " + this.userInfoEndpoint + ": Unable to read response content"
"Error while getting " + this.userInfoEndpoint + ": " + userInfoResponse.toErrorResponse()
);
}

String contentType = String.valueOf(httpResponse.getHeaderValues("content-type"));

JWTClaimsSet claims;
boolean isSigned = contentType.contains(APPLICATION_JWT);
if (contentType.contains(APPLICATION_JWT)) { // We don't need the userinfo_encrypted_response_alg since the
boolean isSigned = contentType.contains(ContentType.APPLICATION_JWT.toString());
if (isSigned) { // We don't need the userinfo_encrypted_response_alg since the
// selfRefreshingKeyProvider has access to the keys
claims = openIdJwtAuthenticator.getJwtClaimsSetFromInfoContent(userinfoContent);
claims = openIdJwtAuthenticator.getJwtClaimsSetFromInfoContent(
userInfoResponse.toSuccessResponse().getUserInfoJWT().getParsedString()
);
} else {
claims = JWTClaimsSet.parse(userinfoContent);
claims = JWTClaimsSet.parse(userInfoResponse.toSuccessResponse().getUserInfo().toString());
}

String id = openIdJwtAuthenticator.getJwtClaimsSet(request).getSubject();
String missing = validateResponseClaims(claims, id, isSigned);
if (!missing.isBlank()) {
throw new AuthenticatorUnavailableException(
"Error while getting " + this.userInfoEndpoint + ": Missing or invalid required claims in response: " + missing
"Error while getting " + this.userInfoEndpoint + ": Missing or invalid required claims in response: " + missing
);
}

Expand All @@ -221,7 +210,7 @@ public AuthCredentials extractCredentials0(SecurityRequest request, ThreadContex
} catch (ParseException e) {
throw new RuntimeException(e);
}
} catch (IOException e) {
} catch (IOException | URISyntaxException | com.nimbusds.oauth2.sdk.ParseException e) {
throw new AuthenticatorUnavailableException("Error while getting " + this.userInfoEndpoint + ": " + e, e);
}
}
Expand All @@ -239,8 +228,8 @@ private String validateResponseClaims(JWTClaimsSet claims, String id, boolean is
missing = missing.concat("iss");
}
if (claims.getAudience() == null
|| claims.getAudience().toString().isBlank()
|| !claims.getAudience().contains(settings.get(CLIENT_ID))) {
|| claims.getAudience().toString().isBlank()
|| !claims.getAudience().contains(settings.get(CLIENT_ID))) {
missing = missing.concat("aud");
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@

public class OpenIdConstants {

public static final String APPLICATION_JWT = "application/jwt";
public static final String CLIENT_ID = "client_id";
public static final String ISSUER_ID_URL = "issuer_id_url";
public static final String SUB_CLAIM = "sub";
Expand Down
Loading

0 comments on commit 84732b8

Please sign in to comment.