Skip to content

Commit 96cbbca

Browse files
authored
feat(client): proxy and ssl configuration support (#142)
Adding support for configuring SSL and proxies in the Confidential Client class.
1 parent 3e79ae6 commit 96cbbca

5 files changed

Lines changed: 300 additions & 45 deletions

File tree

README.md

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Add the below dependency to the project's POM:
1818
<dependency>
1919
<groupId>com.factset.sdk</groupId>
2020
<artifactId>utils</artifactId>
21-
<version>1.0.1</version>
21+
<version>1.1.0-SNAPSHOT</version>
2222
</dependency>
2323
```
2424

@@ -32,10 +32,49 @@ repositories {
3232
}
3333
3434
dependencies {
35-
implementation "com.factset.sdk:utils:1.0.1"
35+
implementation "com.factset.sdk:utils:1.1.0-SNAPSHOT"
3636
}
3737
```
3838

39+
### Snapshot Releases
40+
41+
To be able to install snapshot releases of the sdk an additional repository must be added to the maven or gradle config.
42+
43+
#### Maven Snapshot Repository
44+
45+
```xml
46+
<repositories>
47+
<repository>
48+
<id>sonatype</id>
49+
<name>sonatype-snapshot</name>
50+
<url>https://oss.sonatype.org/content/repositories/snapshots/</url>
51+
<snapshots>
52+
<enabled>true</enabled>
53+
</snapshots>
54+
<releases>
55+
<enabled>false</enabled>
56+
</releases>
57+
</repository>
58+
</repositories>
59+
```
60+
61+
#### Gradle Snapshot Repository
62+
63+
```groovy
64+
repositories {
65+
mavenCentral()
66+
maven {
67+
url = uri("https://oss.sonatype.org/content/repositories/snapshots/")
68+
mavenContent {
69+
snapshotsOnly()
70+
}
71+
}
72+
}
73+
```
74+
75+
Snapshot releases are cached by gradle for some time, for details see: [Gradle Dynamic Versions](https://docs.gradle.org/current/userguide/dynamic_versions.html#sub:declaring_dependency_with_changing_version)
76+
77+
3978
## Usage
4079

4180
This library contains multiple modules, sample usage of each module is below.
@@ -100,6 +139,45 @@ public class Console {
100139
}
101140
```
102141

142+
### Configure a Proxy
143+
144+
The Confidential Client accepts an additional optional parameter called `RequestOptions`. This can be created to specify a proxy for the client to use. Below is an example of how to do this:
145+
146+
```java
147+
Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("127.0.0.1", 8080));
148+
RequestOptions requestOptions = RequestOptions.builder().proxy(proxy).build();
149+
150+
// Pass this into client
151+
ConfidentialClient confidentialClient = new ConfidentialClient("./path/to/config.json", requestOptions);
152+
```
153+
154+
### Custom SSL Certificate
155+
156+
If you are making requests to a server which is using custom TLS certificates, you are able to verify the validity of the certificate via the `RequestOptions` configuration.
157+
158+
#### Hostname Verifier
159+
160+
You can pass in a custom hostname verifier to modify the details of the verification with a custom implementation. Otherwise, the `RequestOptions` will use the default one which checks the hostname in the certificate, located in the JRE keystore, and compares it to the hostname of the URL that is being hit by the client.
161+
162+
#### SSL Socket Factory
163+
164+
You can pass in a custom SSL Socket Factory and modify the `SSLContext` for a specific user use case. Otherwise, the `RequestOptions` uses a default `SSLSocketFactory` as described [here](https://docs.oracle.com/javase/7/docs/api/javax/net/ssl/HttpsURLConnection.html#getDefaultHostnameVerifier()).
165+
166+
#### Example
167+
168+
```java
169+
SSLContext sslContext = SSLContext.getInstance("TLS");
170+
sslContext.init(...); // Configure this based on application's needs
171+
172+
SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
173+
HostnameVerifier hostnameVerifier = ((hostname, session) -> ...); // Configure this based on application's needs
174+
175+
RequestOptions reqOpt = RequestOptions.builder()
176+
.hostnameVerifier(hostnameVerifier)
177+
.sslSocketFactory(sslSocketFactory)
178+
.build();
179+
```
180+
103181
## Modules
104182

105183
Information about the various utility modules contained in this library can be found below.

build.gradle

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ apply plugin: 'jacoco'
99
apply plugin: 'maven-publish'
1010

1111
group 'com.factset.sdk'
12-
version '1.0.1'
12+
version '1.1.0-SNAPSHOT'
1313

1414
dependencies {
1515
implementation 'org.slf4j:slf4j-api:1.7.36'
@@ -26,6 +26,11 @@ dependencies {
2626
testImplementation "org.hamcrest:hamcrest:2.2"
2727
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.10.0'
2828
testRuntimeOnly 'ch.qos.logback:logback-core:1.2.11'
29+
30+
compileOnly 'org.projectlombok:lombok:1.18.30'
31+
annotationProcessor 'org.projectlombok:lombok:1.18.30'
32+
testCompileOnly 'org.projectlombok:lombok:1.18.30'
33+
testAnnotationProcessor 'org.projectlombok:lombok:1.18.30'
2934
}
3035

3136
task sourcesJar(type: Jar) {

src/main/java/com/factset/sdk/utils/authentication/ConfidentialClient.java

Lines changed: 94 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@
2626
import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
2727
import java.io.IOException;
2828
import java.io.InputStream;
29+
import java.net.HttpURLConnection;
30+
import java.net.URL;
31+
import javax.net.ssl.HttpsURLConnection;
2932
import java.util.Date;
3033
import java.util.List;
3134
import java.util.Objects;
@@ -42,7 +45,8 @@ public class ConfidentialClient implements OAuth2Client {
4245

4346
private static final Logger LOGGER = LoggerFactory.getLogger(ConfidentialClient.class);
4447
private final Configuration config;
45-
private final OIDCProviderMetadata providerMetadata;
48+
private OIDCProviderMetadata providerMetadata;
49+
private final RequestOptions requestOptions;
4650
private TokenRequestBuilder tokenRequestBuilder;
4751
private long jwsIssuedAt;
4852
private long accessTokenExpireTime;
@@ -65,6 +69,24 @@ public ConfidentialClient(final String configPath)
6569
this(new Configuration(configPath));
6670
}
6771

72+
/**
73+
* Creates a new ConfidentialClient. When setting up the OAuth 2.0 client, this constructor reaches out to
74+
* FactSet's well-known URI to retrieve metadata about its authorization server. This information along with
75+
* information about the OAuth 2.0 client is stored and used whenever a new access token is fetched.
76+
*
77+
* @param configPath The path towards the file to pe parsed.
78+
* @param requestOptions Object that can configure options like proxy and SSL settings
79+
* @throws AuthServerMetadataContentException If Meta Issuer or Meta Token Endpoint is missing.
80+
* @throws AuthServerMetadataException If reading from URL is unsuccessful.
81+
* @throws ConfigurationException If JWK required keys are missing from the RSA or any keys with a value
82+
* that is null or an empty string.
83+
*/
84+
public ConfidentialClient(final String configPath, RequestOptions requestOptions)
85+
throws AuthServerMetadataContentException, AuthServerMetadataException,
86+
ConfigurationException {
87+
this(new Configuration(configPath), requestOptions);
88+
}
89+
6890
/**
6991
* Creates a new ConfidentialClient. When setting up the OAuth 2.0 client, this constructor reaches out to
7092
* FactSet's well-known URI to retrieve metadata about its authorization server. This information along with
@@ -77,25 +99,28 @@ public ConfidentialClient(final String configPath)
7799
*/
78100
public ConfidentialClient(final Configuration config)
79101
throws AuthServerMetadataContentException, AuthServerMetadataException {
102+
this(config, RequestOptions.builder().build());
103+
}
104+
105+
/**
106+
* Creates a new ConfidentialClient. When setting up the OAuth 2.0 client, this constructor reaches out to
107+
* FactSet's well-known URI to retrieve metadata about its authorization server. This information along with
108+
* information about the OAuth 2.0 client is stored and used whenever a new access token is fetched.
109+
*
110+
* @param config Configuration object.
111+
* @param requestOptions Object that can configure options like proxy and SSL settings
112+
* @throws AuthServerMetadataContentException If Meta Issuer or Meta Token Endpoint is missing.
113+
* @throws AuthServerMetadataException If reading from URL is unsuccessful.
114+
* @throws NullPointerException Unchecked exception, if config is null.
115+
*/
116+
public ConfidentialClient(final Configuration config, RequestOptions requestOptions)
117+
throws AuthServerMetadataContentException, AuthServerMetadataException {
80118
Objects.requireNonNull(config, "Configuration object must not be null");
81119
this.config = config;
82120
LOGGER.debug("Finished initialising configuration");
121+
this.requestOptions = requestOptions == null ? RequestOptions.builder().build() : requestOptions;
83122

84-
LOGGER.debug("Attempting to get response from Well Known URI");
85-
try (InputStream stream = config.getWellKnownUrl().openStream()) {
86-
final String providerInfo = IOUtils.readInputStreamToString(stream);
87-
this.providerMetadata = OIDCProviderMetadata.parse(providerInfo);
88-
} catch (final ParseException e) {
89-
throw new AuthServerMetadataContentException("Content of WellKnownUri has errors: " +
90-
config.getWellKnownUrl().toString(), e);
91-
} catch (final IOException e) {
92-
throw new AuthServerMetadataException("Error retrieving contents from WellKnownUri: " +
93-
config.getWellKnownUrl().toString(), e);
94-
}
95-
LOGGER.debug("Response received from Well Known URI");
96-
97-
this.tokenRequestBuilder =
98-
new TokenRequestBuilder().uri(this.providerMetadata.getTokenEndpointURI());
123+
this.requestProviderMetadata();
99124
}
100125

101126
/**
@@ -136,6 +161,25 @@ protected ConfidentialClient(final Configuration config, final TokenRequestBuild
136161
this.tokenRequestBuilder = tokReqBuilder.uri(this.providerMetadata.getTokenEndpointURI());
137162
}
138163

164+
/**
165+
* Creates a new ConfidentialClient. When setting up the OAuth 2.0 client, this constructor reaches out to
166+
* FactSet's well-known URI to retrieve metadata about its authorization server. This information along with
167+
* information about the OAuth 2.0 client is stored and used whenever a new access token is fetched.
168+
*
169+
* @param config Configuration object.
170+
* @param tokReqBuilder The TokenRequest builder, used to build custom TokenRequest instances.
171+
* @param requestOptions Object that can configure options like proxy and SSL settings
172+
* @throws AuthServerMetadataContentException If Meta Issuer or Meta Token Endpoint is missing.
173+
* @throws AuthServerMetadataException If reading from URL is unsuccessful.
174+
* @throws NullPointerException Unchecked exception, if config is null.
175+
*/
176+
protected ConfidentialClient(final Configuration config, final TokenRequestBuilder tokReqBuilder, RequestOptions requestOptions)
177+
throws AuthServerMetadataContentException,
178+
AuthServerMetadataException {
179+
this(config, requestOptions);
180+
this.tokenRequestBuilder = tokReqBuilder.uri(this.providerMetadata.getTokenEndpointURI());
181+
}
182+
139183
/**
140184
* Returns an access token that can be used for authentication. If the cache contains a valid access token,
141185
* it's returned. Otherwise, a new access token is retrieved from FactSet's authorization server. The access
@@ -156,6 +200,36 @@ public String getAccessToken() throws AccessTokenException, SigningJwsException
156200
return this.fetchAccessToken();
157201
}
158202

203+
private void requestProviderMetadata() throws AuthServerMetadataContentException, AuthServerMetadataException {
204+
LOGGER.debug("Attempting to get response from Well Known URI");
205+
URL wellKnownURL = this.config.getWellKnownUrl();
206+
InputStream stream;
207+
208+
try {
209+
HttpURLConnection conn = (HttpURLConnection) wellKnownURL.openConnection(this.requestOptions.getProxy());
210+
if (conn instanceof HttpsURLConnection) {
211+
HttpsURLConnection sslConn = (HttpsURLConnection) conn;
212+
sslConn.setHostnameVerifier(this.requestOptions.getHostnameVerifier());
213+
sslConn.setSSLSocketFactory(this.requestOptions.getSslSocketFactory());
214+
}
215+
216+
stream = conn.getInputStream();
217+
218+
final String providerInfo = IOUtils.readInputStreamToString(stream);
219+
this.providerMetadata = OIDCProviderMetadata.parse(providerInfo);
220+
} catch (final ParseException e) {
221+
throw new AuthServerMetadataContentException("Content of WellKnownUri has errors: " +
222+
this.config.getWellKnownUrl().toString(), e);
223+
} catch (final IOException e) {
224+
throw new AuthServerMetadataException("Error retrieving contents from WellKnownUri: " +
225+
this.config.getWellKnownUrl().toString(), e);
226+
}
227+
LOGGER.debug("Response received from Well Known URI");
228+
229+
this.tokenRequestBuilder =
230+
new TokenRequestBuilder().uri(this.providerMetadata.getTokenEndpointURI());
231+
}
232+
159233
private boolean isCachedTokenValid() {
160234
if (this.accessToken == null) {
161235
return false;
@@ -173,6 +247,10 @@ private String fetchAccessToken() throws AccessTokenException, SigningJwsExcepti
173247
final TokenRequest tokenRequest = this.tokenRequestBuilder.signedJwt(signedJwt).build();
174248

175249
final HTTPRequest httpRequest = tokenRequest.toHTTPRequest();
250+
httpRequest.setProxy(this.requestOptions.getProxy());
251+
httpRequest.setHostnameVerifier(this.requestOptions.getHostnameVerifier());
252+
httpRequest.setSSLSocketFactory(this.requestOptions.getSslSocketFactory());
253+
176254
logTokenRequest(httpRequest);
177255

178256
final HTTPResponse res = httpRequest.send();
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.factset.sdk.utils.authentication;
2+
3+
import lombok.Builder;
4+
import lombok.Value;
5+
6+
import javax.net.ssl.HostnameVerifier;
7+
import javax.net.ssl.HttpsURLConnection;
8+
import javax.net.ssl.SSLSocketFactory;
9+
import java.net.Proxy;
10+
11+
@Value
12+
@Builder
13+
public class RequestOptions {
14+
@Builder.Default
15+
Proxy proxy = Proxy.NO_PROXY;
16+
17+
@Builder.Default
18+
HostnameVerifier hostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier();
19+
20+
@Builder.Default
21+
SSLSocketFactory sslSocketFactory = HttpsURLConnection.getDefaultSSLSocketFactory();
22+
}

0 commit comments

Comments
 (0)