diff --git a/components/org.wso2.carbon.identity.application.authenticator.oidc/pom.xml b/components/org.wso2.carbon.identity.application.authenticator.oidc/pom.xml index e61bf2ae..4ed080e9 100644 --- a/components/org.wso2.carbon.identity.application.authenticator.oidc/pom.xml +++ b/components/org.wso2.carbon.identity.application.authenticator.oidc/pom.xml @@ -155,6 +155,7 @@ org.apache.oltu.oauth2.common.exception; version="${oltu.package.import.version.range}", org.apache.oltu.oauth2.common.message.types; version="${oltu.package.import.version.range}", + org.apache.http.*; version="${http.package.import.version.range}", org.apache.oltu.oauth2.common.utils; version="${oltu.package.import.version.range}", org.wso2.carbon.utils.*; version="${carbon.kernel.package.import.version.range}", org.wso2.carbon.identity.oauth.common.*; @@ -187,6 +188,7 @@ version="${identity.framework.package.import.version.range}", org.wso2.carbon.identity.central.log.mgt.utils; version="${identity.framework.package.import.version.range}", + org.wso2.carbon.base; !org.wso2.carbon.identity.application.authenticator.oidc.internal, diff --git a/components/org.wso2.carbon.identity.application.authenticator.oidc/src/main/java/org/wso2/carbon/identity/application/authenticator/oidc/CustomURLConnectionClient.java b/components/org.wso2.carbon.identity.application.authenticator.oidc/src/main/java/org/wso2/carbon/identity/application/authenticator/oidc/CustomURLConnectionClient.java new file mode 100644 index 00000000..3fb835e3 --- /dev/null +++ b/components/org.wso2.carbon.identity.application.authenticator.oidc/src/main/java/org/wso2/carbon/identity/application/authenticator/oidc/CustomURLConnectionClient.java @@ -0,0 +1,218 @@ +/** + * Copyright (c) 2024, WSO2 Inc. (https://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.wso2.carbon.identity.application.authenticator.oidc; + +import org.apache.commons.lang3.StringUtils; +import org.apache.http.HttpHost; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.CredentialsProvider; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.config.RegistryBuilder; +import org.apache.http.conn.socket.ConnectionSocketFactory; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.conn.ssl.SSLContexts; +import org.apache.http.conn.ssl.SSLSocketFactory; +import org.apache.http.conn.ssl.X509HostnameVerifier; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.impl.conn.DefaultProxyRoutePlanner; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.apache.http.util.EntityUtils; +import org.apache.oltu.oauth2.client.HttpClient; +import org.apache.oltu.oauth2.client.request.OAuthClientRequest; +import org.apache.oltu.oauth2.client.response.OAuthClientResponse; +import org.apache.oltu.oauth2.client.response.OAuthClientResponseFactory; +import org.apache.oltu.oauth2.common.exception.OAuthProblemException; +import org.apache.oltu.oauth2.common.exception.OAuthSystemException; +import org.wso2.carbon.identity.application.authenticator.oidc.util.ExtendedProxyRoutePlanner; +import org.wso2.carbon.identity.core.util.IdentityUtil; +import org.wso2.carbon.utils.CarbonUtils; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSession; +import java.io.FileInputStream; +import java.io.IOException; +import java.net.MalformedURLException; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.util.Arrays; +import java.util.Map; + + +public class CustomURLConnectionClient implements HttpClient { + @Override + public T execute(OAuthClientRequest request, Map headers, String s, Class responseClass) throws OAuthSystemException, OAuthProblemException { + + org.apache.http.client.HttpClient httpClient = getHttpClient(); + try { + HttpPost httpPost = new HttpPost(request.getLocationUri()); + if (headers != null && !headers.isEmpty()) { + for (Map.Entry header : headers.entrySet()) { + httpPost.setHeader(header.getKey(), header.getValue()); + } + } + if (request.getHeaders() != null) { + for (Map.Entry header : request.getHeaders().entrySet()) { + httpPost.setHeader(header.getKey(), header.getValue()); + } + } + + String requestBody = request.getBody(); + StringEntity requestEntity = new StringEntity(requestBody, ContentType.APPLICATION_JSON); + httpPost.setEntity(requestEntity); + + HttpResponse response = httpClient.execute(httpPost); + if (HttpStatus.SC_OK == response.getStatusLine().getStatusCode()) { + String responseString = EntityUtils.toString(response.getEntity(), "UTF-8"); + return OAuthClientResponseFactory + .createCustomResponse(responseString, requestEntity.getContentType().toString(), + response.getStatusLine().getStatusCode(), responseClass); + } else { + throw new OAuthSystemException("Error while obtaining the access token through the proxy " + + EntityUtils.toString(response.getEntity())); + } + } catch (IOException e) { + throw new OAuthSystemException(e); + } + } + + @Override + public void shutdown() { + // Nothing to do here + } + + public static org.apache.http.client.HttpClient getHttpClient() throws OAuthSystemException { + Boolean proxyEnabled = Boolean.parseBoolean(IdentityUtil.getProperty( + OIDCAuthenticatorConstants.Proxy.proxyEnable)); + String proxyProtocol = IdentityUtil.getProperty(OIDCAuthenticatorConstants.Proxy.proxyProtocol); + String proxyUsername = IdentityUtil.getProperty(OIDCAuthenticatorConstants.Proxy.proxyUsername); + String proxyPassword = IdentityUtil.getProperty(OIDCAuthenticatorConstants.Proxy.proxyPassword); + String proxyHost = IdentityUtil.getProperty(OIDCAuthenticatorConstants.Proxy.proxyHost); + String proxyPort = IdentityUtil.getProperty(OIDCAuthenticatorConstants.Proxy.proxyPort); + String nonProxyHosts = IdentityUtil.getProperty(OIDCAuthenticatorConstants.Proxy.proxyPort); + + PoolingHttpClientConnectionManager pool = null; + try { + pool = getPoolingHttpClientConnectionManager(proxyProtocol); + } catch (Exception e) { + throw new OAuthSystemException(e); + } + + RequestConfig params = RequestConfig.custom().build(); + HttpClientBuilder clientBuilder = HttpClients.custom().setConnectionManager(pool) + .setDefaultRequestConfig(params); + + HttpHost host = null; + if (proxyEnabled) { + host = new HttpHost(proxyHost, Integer.parseInt(proxyPort), proxyProtocol); + clientBuilder.setDefaultRequestConfig(RequestConfig.custom().setProxy(host).build()); + DefaultProxyRoutePlanner routePlanner; + if (!StringUtils.isBlank(nonProxyHosts)) { + routePlanner = new ExtendedProxyRoutePlanner(host, nonProxyHosts, proxyHost, proxyPort, proxyProtocol); + } else { + routePlanner = new DefaultProxyRoutePlanner(host); + } + clientBuilder = clientBuilder.setRoutePlanner(routePlanner); + if (!StringUtils.isBlank(proxyUsername) && !StringUtils.isBlank(proxyPassword)) { + CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); + credentialsProvider.setCredentials(new AuthScope(proxyHost, Integer.parseInt(proxyPort)), + new UsernamePasswordCredentials(proxyUsername, proxyPassword)); + clientBuilder = clientBuilder.setDefaultCredentialsProvider(credentialsProvider); + } + } + return clientBuilder.build(); + } + + /** + * Return a PoolingHttpClientConnectionManager instance + * + * @param protocol- service endpoint protocol. It can be http/https + * @return PoolManager + */ + private static PoolingHttpClientConnectionManager getPoolingHttpClientConnectionManager(String protocol) throws Exception { + + PoolingHttpClientConnectionManager poolManager; + if (OIDCAuthenticatorConstants.Proxy.HTTPS.equals(protocol)) { + SSLConnectionSocketFactory socketFactory = createSocketFactory(); + org.apache.http.config.Registry socketFactoryRegistry = + RegistryBuilder.create() + .register(OIDCAuthenticatorConstants.Proxy.HTTPS, socketFactory).build(); + poolManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry); + } else { + poolManager = new PoolingHttpClientConnectionManager(); + } + return poolManager; + } + + private static SSLConnectionSocketFactory createSocketFactory() throws OAuthSystemException { + SSLContext sslContext = null; + HostnameVerifier hostnameVerifier = null; + String keyStorePath = CarbonUtils.getServerConfiguration() + .getFirstProperty(OIDCAuthenticatorConstants.Proxy.trustStoreLocation); + String keyStorePassword = CarbonUtils.getServerConfiguration() + .getFirstProperty(OIDCAuthenticatorConstants.Proxy.trustStorePassword); + try { + KeyStore trustStore = KeyStore.getInstance("JKS"); + trustStore.load(new FileInputStream(keyStorePath), keyStorePassword.toCharArray()); + sslContext = SSLContexts.custom().loadTrustMaterial(trustStore).build(); + + String hostnameVerifierOption = System.getProperty(OIDCAuthenticatorConstants.Proxy.hostNameVerifierSysEnv); + + if (OIDCAuthenticatorConstants.Proxy.ALLOW_ALL_HOSTNAME_VERIFIER.equalsIgnoreCase(hostnameVerifierOption)) { + hostnameVerifier = SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER; + } else if (OIDCAuthenticatorConstants.Proxy.STRICT_HOSTNAME_VERIFIER.equalsIgnoreCase(hostnameVerifierOption)) { + hostnameVerifier = SSLSocketFactory.STRICT_HOSTNAME_VERIFIER; + } else if (OIDCAuthenticatorConstants.Proxy.DEFAULT_HOSTNAME_VERIFIER.equalsIgnoreCase(hostnameVerifierOption)) { + hostnameVerifier = new HostnameVerifier() { + final String[] localhosts = {"::1", "127.0.0.1", "localhost", "localhost.localdomain"}; + + @Override + public boolean verify(String urlHostName, SSLSession session) { + return SSLSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER.verify(urlHostName, session) + || Arrays.asList(localhosts).contains(urlHostName); + } + }; + } else { + hostnameVerifier = SSLSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER; + } + return new SSLConnectionSocketFactory(sslContext, (X509HostnameVerifier) hostnameVerifier); + + } catch (KeyStoreException e) { + throw new OAuthSystemException(e); + } catch (IOException e) { + throw new OAuthSystemException(e); + } catch (CertificateException e) { + throw new OAuthSystemException(e); + } catch (NoSuchAlgorithmException e) { + throw new OAuthSystemException(e); + } catch (KeyManagementException e) { + throw new OAuthSystemException(e); + } + } +} diff --git a/components/org.wso2.carbon.identity.application.authenticator.oidc/src/main/java/org/wso2/carbon/identity/application/authenticator/oidc/OIDCAuthenticatorConstants.java b/components/org.wso2.carbon.identity.application.authenticator.oidc/src/main/java/org/wso2/carbon/identity/application/authenticator/oidc/OIDCAuthenticatorConstants.java index 48f6ae18..5432a1f9 100644 --- a/components/org.wso2.carbon.identity.application.authenticator.oidc/src/main/java/org/wso2/carbon/identity/application/authenticator/oidc/OIDCAuthenticatorConstants.java +++ b/components/org.wso2.carbon.identity.application.authenticator.oidc/src/main/java/org/wso2/carbon/identity/application/authenticator/oidc/OIDCAuthenticatorConstants.java @@ -193,4 +193,25 @@ public static class ActionIDs { public static final String INITIATE_OUTBOUND_AUTH_REQUEST = "initiate-outbound-auth-oidc-request"; } } + + public class Proxy { + private Proxy() { + + } + public static final String proxyEnable = "ProxyConfig.Enable"; + public static final String proxyHost = "ProxyConfig.Host"; + public static final String proxyUsername = "ProxyConfig.Username" ; + public static final String proxyPassword = "ProxyConfig.Password"; + public static final String proxyPort = "ProxyConfig.Port"; + public static final String proxyProtocol = "ProxyConfig.Protocol"; + public static final String nonProxyHosts = "ProxyConfig.NonProxyHosts"; + + public static final String HTTPS = "https"; + public static final String trustStoreLocation = "Security.TrustStore.Location"; + public static final String trustStorePassword = "Security.TrustStore.Password"; + public static final String hostNameVerifierSysEnv = "httpclient.hostnameVerifier"; + public static final String ALLOW_ALL_HOSTNAME_VERIFIER = "AllowAll"; + public static final String STRICT_HOSTNAME_VERIFIER = "Strict"; + public static final String DEFAULT_HOSTNAME_VERIFIER = "DefaultAndLocalhost"; + } } diff --git a/components/org.wso2.carbon.identity.application.authenticator.oidc/src/main/java/org/wso2/carbon/identity/application/authenticator/oidc/OpenIDConnectAuthenticator.java b/components/org.wso2.carbon.identity.application.authenticator.oidc/src/main/java/org/wso2/carbon/identity/application/authenticator/oidc/OpenIDConnectAuthenticator.java index 684cbc38..ebde530f 100644 --- a/components/org.wso2.carbon.identity.application.authenticator.oidc/src/main/java/org/wso2/carbon/identity/application/authenticator/oidc/OpenIDConnectAuthenticator.java +++ b/components/org.wso2.carbon.identity.application.authenticator.oidc/src/main/java/org/wso2/carbon/identity/application/authenticator/oidc/OpenIDConnectAuthenticator.java @@ -1245,7 +1245,12 @@ protected OAuthClientResponse requestAccessToken(HttpServletRequest request, Aut OAuthClientRequest accessTokenRequest = getAccessTokenRequest(context, authzResponse); // Create OAuth client that uses custom http client under the hood. - OAuthClient oAuthClient = new OAuthClient(new URLConnectionClient()); + OAuthClient oAuthClient; + if (Boolean.parseBoolean(IdentityUtil.getProperty(OIDCAuthenticatorConstants.Proxy.proxyEnable))) { + oAuthClient = new OAuthClient(new CustomURLConnectionClient()); + } else { + oAuthClient = new OAuthClient(new URLConnectionClient()); + } oAuthResponse = getOauthResponse(oAuthClient, accessTokenRequest); if (oAuthResponse != null) { processAuthenticatedUserScopes(context, oAuthResponse.getParam(OAuthConstants.OAuth20Params.SCOPE)); diff --git a/components/org.wso2.carbon.identity.application.authenticator.oidc/src/main/java/org/wso2/carbon/identity/application/authenticator/oidc/util/ExtendedProxyRoutePlanner.java b/components/org.wso2.carbon.identity.application.authenticator.oidc/src/main/java/org/wso2/carbon/identity/application/authenticator/oidc/util/ExtendedProxyRoutePlanner.java new file mode 100644 index 00000000..41a12174 --- /dev/null +++ b/components/org.wso2.carbon.identity.application.authenticator.oidc/src/main/java/org/wso2/carbon/identity/application/authenticator/oidc/util/ExtendedProxyRoutePlanner.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2024, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.wso2.carbon.identity.application.authenticator.oidc.util; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.http.HttpException; +import org.apache.http.HttpHost; +import org.apache.http.HttpRequest; +import org.apache.http.impl.conn.DefaultProxyRoutePlanner; +import org.apache.http.protocol.HttpContext; + + +/** + * Extended ProxyRoutePlanner class to handle non proxy hosts implementation + */ +public class ExtendedProxyRoutePlanner extends DefaultProxyRoutePlanner { + private static final Log log = LogFactory.getLog(ExtendedProxyRoutePlanner.class); + String nonProxyHosts; + String proxyHost; + String proxyPort; + String protocol; + + public ExtendedProxyRoutePlanner(HttpHost host, String nonProxyHosts, String proxyHost, String proxyPort, + String protocol ) { + super(host); + this.nonProxyHosts = nonProxyHosts; + this.proxyHost = proxyHost; + this.proxyPort = proxyPort; + this.protocol = protocol; + } + + private HttpHost getProxy(String scheme) { + log.debug("Get proxy for scheme: " + scheme); + String proto = scheme; + + String protoProxyHost = proxyHost; + if (protoProxyHost == null) { + return null; + } + String proxyPortStr = proxyPort; + if (proxyPortStr == null) { + return null; + } + int protoProxyPort = -1; + if (proxyPortStr != null) { + try { + protoProxyPort = Integer.valueOf(proxyPortStr); + } catch (NumberFormatException nfe) { + log.warn("invalid proxy port: " + proxyPortStr + ". proxy will be ignored"); + return null; + } + } + if (protoProxyPort < 1) { + return null; + } + log.debug("set " + proto + " proxy '" + protoProxyHost + ":" + protoProxyPort + "'"); + return new HttpHost(protoProxyHost, protoProxyPort, scheme); + } + + private String[] getNonProxyHosts() { + String nonProxyHosts = this.nonProxyHosts; + if (nonProxyHosts == null) { + return null; + } + return nonProxyHosts.split("\\|"); + } + + private boolean doesTargetMatchNonProxy(HttpHost target) { + String uriHost = target.getHostName(); + String uriScheme = target.getSchemeName(); + String[] nonProxyHosts = getNonProxyHosts(); + int nphLength = nonProxyHosts != null ? nonProxyHosts.length : 0; + if (nonProxyHosts == null || nphLength < 1) { + log.debug("sheme:'" + uriScheme + "', host:'" + uriHost + "' : DEFAULT (0 non proxy host)"); + return false; + } + for (String nonProxyHost : nonProxyHosts) { + if (uriHost.matches(nonProxyHost)) { + log.debug("sheme:'" + uriScheme + "', host:'" + uriHost + "' matches nonProxyHost '" + nonProxyHost + "' : NO PROXY"); + return true; + } + } + log.debug("sheme:'" + uriScheme + "', host:'" + uriHost + "' : DEFAULT (no match of " + nphLength + " non proxy host)"); + return false; + } + + @Override + protected HttpHost determineProxy(HttpHost target, final HttpRequest request, final HttpContext context) + throws HttpException { + + if (doesTargetMatchNonProxy(target)) { + return null; + } + if (StringUtils.isNotEmpty(protocol)) { + return getProxy(protocol); + } else { + return getProxy(target.getSchemeName()); + } + } +} diff --git a/pom.xml b/pom.xml index f5e71607..1d6d5063 100644 --- a/pom.xml +++ b/pom.xml @@ -336,6 +336,7 @@ [4.4.0, 5.0.0) [2.3.0, 3.0.0) [1.0.0, 2.0.0) + [4.4.0, 5.0.0) [2.6.0, 3.0.0) [1.0.1, 2.0.0) [1.2,2.0)