Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 80 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Add the below dependency to the project's POM:
<dependency>
<groupId>com.factset.sdk</groupId>
<artifactId>utils</artifactId>
<version>1.0.1</version>
<version>1.1.0-SNAPSHOT</version>
</dependency>
```

Expand All @@ -32,10 +32,49 @@ repositories {
}

dependencies {
implementation "com.factset.sdk:utils:1.0.1"
implementation "com.factset.sdk:utils:1.1.0-SNAPSHOT"
}
```

### Snapshot Releases

To be able to install snapshot releases of the sdk an additional repository must be added to the maven or gradle config.

#### Maven Snapshot Repository

```xml
<repositories>
<repository>
<id>sonatype</id>
<name>sonatype-snapshot</name>
<url>https://oss.sonatype.org/content/repositories/snapshots/</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
<releases>
<enabled>false</enabled>
</releases>
</repository>
</repositories>
```

#### Gradle Snapshot Repository

```groovy
repositories {
mavenCentral()
maven {
url = uri("https://oss.sonatype.org/content/repositories/snapshots/")
mavenContent {
snapshotsOnly()
}
}
}
```

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)


## Usage

This library contains multiple modules, sample usage of each module is below.
Expand Down Expand Up @@ -100,6 +139,45 @@ public class Console {
}
```

### Configure a Proxy

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:

```java
Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("127.0.0.1", 8080));
RequestOptions requestOptions = RequestOptions.builder().proxy(proxy).build();

// Pass this into client
ConfidentialClient confidentialClient = new ConfidentialClient("./path/to/config.json", requestOptions);
```

Comment thread
granitdula marked this conversation as resolved.
### Custom SSL Certificate

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.

#### Hostname Verifier

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.

#### SSL Socket Factory

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()).

#### Example

```java
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(...); // Configure this based on application's needs

SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
HostnameVerifier hostnameVerifier = ((hostname, session) -> ...); // Configure this based on application's needs

RequestOptions reqOpt = RequestOptions.builder()
.hostnameVerifier(hostnameVerifier)
.sslSocketFactory(sslSocketFactory)
.build();
```

## Modules

Information about the various utility modules contained in this library can be found below.
Expand Down
7 changes: 6 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ apply plugin: 'jacoco'
apply plugin: 'maven-publish'

group 'com.factset.sdk'
version '1.0.1'
version '1.1.0-SNAPSHOT'

dependencies {
implementation 'org.slf4j:slf4j-api:1.7.36'
Expand All @@ -26,6 +26,11 @@ dependencies {
testImplementation "org.hamcrest:hamcrest:2.2"
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.10.0'
testRuntimeOnly 'ch.qos.logback:logback-core:1.2.11'

compileOnly 'org.projectlombok:lombok:1.18.30'
annotationProcessor 'org.projectlombok:lombok:1.18.30'
testCompileOnly 'org.projectlombok:lombok:1.18.30'
testAnnotationProcessor 'org.projectlombok:lombok:1.18.30'
}

task sourcesJar(type: Jar) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@
import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import javax.net.ssl.HttpsURLConnection;
import java.util.Date;
import java.util.List;
import java.util.Objects;
Expand All @@ -42,7 +45,8 @@ public class ConfidentialClient implements OAuth2Client {

private static final Logger LOGGER = LoggerFactory.getLogger(ConfidentialClient.class);
private final Configuration config;
private final OIDCProviderMetadata providerMetadata;
private OIDCProviderMetadata providerMetadata;
private final RequestOptions requestOptions;
private TokenRequestBuilder tokenRequestBuilder;
private long jwsIssuedAt;
private long accessTokenExpireTime;
Expand All @@ -65,6 +69,24 @@ public ConfidentialClient(final String configPath)
this(new Configuration(configPath));
}

/**
* Creates a new ConfidentialClient. When setting up the OAuth 2.0 client, this constructor reaches out to
* FactSet's well-known URI to retrieve metadata about its authorization server. This information along with
* information about the OAuth 2.0 client is stored and used whenever a new access token is fetched.
*
* @param configPath The path towards the file to pe parsed.
* @param requestOptions Object that can configure options like proxy and SSL settings
* @throws AuthServerMetadataContentException If Meta Issuer or Meta Token Endpoint is missing.
* @throws AuthServerMetadataException If reading from URL is unsuccessful.
* @throws ConfigurationException If JWK required keys are missing from the RSA or any keys with a value
* that is null or an empty string.
*/
public ConfidentialClient(final String configPath, RequestOptions requestOptions)
throws AuthServerMetadataContentException, AuthServerMetadataException,
ConfigurationException {
this(new Configuration(configPath), requestOptions);
}

/**
* Creates a new ConfidentialClient. When setting up the OAuth 2.0 client, this constructor reaches out to
* FactSet's well-known URI to retrieve metadata about its authorization server. This information along with
Expand All @@ -77,25 +99,28 @@ public ConfidentialClient(final String configPath)
*/
public ConfidentialClient(final Configuration config)
throws AuthServerMetadataContentException, AuthServerMetadataException {
this(config, RequestOptions.builder().build());
}

/**
* Creates a new ConfidentialClient. When setting up the OAuth 2.0 client, this constructor reaches out to
* FactSet's well-known URI to retrieve metadata about its authorization server. This information along with
* information about the OAuth 2.0 client is stored and used whenever a new access token is fetched.
*
* @param config Configuration object.
* @param requestOptions Object that can configure options like proxy and SSL settings
* @throws AuthServerMetadataContentException If Meta Issuer or Meta Token Endpoint is missing.
* @throws AuthServerMetadataException If reading from URL is unsuccessful.
* @throws NullPointerException Unchecked exception, if config is null.
*/
public ConfidentialClient(final Configuration config, RequestOptions requestOptions)
Comment thread
granitdula marked this conversation as resolved.
throws AuthServerMetadataContentException, AuthServerMetadataException {
Objects.requireNonNull(config, "Configuration object must not be null");
Comment thread
granitdula marked this conversation as resolved.
this.config = config;
LOGGER.debug("Finished initialising configuration");
this.requestOptions = requestOptions == null ? RequestOptions.builder().build() : requestOptions;

LOGGER.debug("Attempting to get response from Well Known URI");
try (InputStream stream = config.getWellKnownUrl().openStream()) {
final String providerInfo = IOUtils.readInputStreamToString(stream);
this.providerMetadata = OIDCProviderMetadata.parse(providerInfo);
} catch (final ParseException e) {
throw new AuthServerMetadataContentException("Content of WellKnownUri has errors: " +
config.getWellKnownUrl().toString(), e);
} catch (final IOException e) {
throw new AuthServerMetadataException("Error retrieving contents from WellKnownUri: " +
config.getWellKnownUrl().toString(), e);
}
LOGGER.debug("Response received from Well Known URI");

this.tokenRequestBuilder =
new TokenRequestBuilder().uri(this.providerMetadata.getTokenEndpointURI());
this.requestProviderMetadata();
}

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

/**
* Creates a new ConfidentialClient. When setting up the OAuth 2.0 client, this constructor reaches out to
* FactSet's well-known URI to retrieve metadata about its authorization server. This information along with
* information about the OAuth 2.0 client is stored and used whenever a new access token is fetched.
*
* @param config Configuration object.
* @param tokReqBuilder The TokenRequest builder, used to build custom TokenRequest instances.
* @param requestOptions Object that can configure options like proxy and SSL settings
* @throws AuthServerMetadataContentException If Meta Issuer or Meta Token Endpoint is missing.
* @throws AuthServerMetadataException If reading from URL is unsuccessful.
* @throws NullPointerException Unchecked exception, if config is null.
*/
protected ConfidentialClient(final Configuration config, final TokenRequestBuilder tokReqBuilder, RequestOptions requestOptions)
throws AuthServerMetadataContentException,
AuthServerMetadataException {
this(config, requestOptions);
this.tokenRequestBuilder = tokReqBuilder.uri(this.providerMetadata.getTokenEndpointURI());
}

/**
* Returns an access token that can be used for authentication. If the cache contains a valid access token,
* it's returned. Otherwise, a new access token is retrieved from FactSet's authorization server. The access
Expand All @@ -156,6 +200,36 @@ public String getAccessToken() throws AccessTokenException, SigningJwsException
return this.fetchAccessToken();
}

private void requestProviderMetadata() throws AuthServerMetadataContentException, AuthServerMetadataException {
LOGGER.debug("Attempting to get response from Well Known URI");
URL wellKnownURL = this.config.getWellKnownUrl();
InputStream stream;

try {
HttpURLConnection conn = (HttpURLConnection) wellKnownURL.openConnection(this.requestOptions.getProxy());
if (conn instanceof HttpsURLConnection) {
HttpsURLConnection sslConn = (HttpsURLConnection) conn;
sslConn.setHostnameVerifier(this.requestOptions.getHostnameVerifier());
sslConn.setSSLSocketFactory(this.requestOptions.getSslSocketFactory());
}

stream = conn.getInputStream();

final String providerInfo = IOUtils.readInputStreamToString(stream);
this.providerMetadata = OIDCProviderMetadata.parse(providerInfo);
} catch (final ParseException e) {
throw new AuthServerMetadataContentException("Content of WellKnownUri has errors: " +
this.config.getWellKnownUrl().toString(), e);
} catch (final IOException e) {
throw new AuthServerMetadataException("Error retrieving contents from WellKnownUri: " +
this.config.getWellKnownUrl().toString(), e);
}
LOGGER.debug("Response received from Well Known URI");

this.tokenRequestBuilder =
new TokenRequestBuilder().uri(this.providerMetadata.getTokenEndpointURI());
}

private boolean isCachedTokenValid() {
if (this.accessToken == null) {
return false;
Expand All @@ -173,6 +247,10 @@ private String fetchAccessToken() throws AccessTokenException, SigningJwsExcepti
final TokenRequest tokenRequest = this.tokenRequestBuilder.signedJwt(signedJwt).build();

final HTTPRequest httpRequest = tokenRequest.toHTTPRequest();
httpRequest.setProxy(this.requestOptions.getProxy());
httpRequest.setHostnameVerifier(this.requestOptions.getHostnameVerifier());
httpRequest.setSSLSocketFactory(this.requestOptions.getSslSocketFactory());

logTokenRequest(httpRequest);

final HTTPResponse res = httpRequest.send();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.factset.sdk.utils.authentication;

import lombok.Builder;
import lombok.Value;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSocketFactory;
import java.net.Proxy;

@Value
@Builder
public class RequestOptions {
@Builder.Default
Proxy proxy = Proxy.NO_PROXY;

@Builder.Default
HostnameVerifier hostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier();

@Builder.Default
SSLSocketFactory sslSocketFactory = HttpsURLConnection.getDefaultSSLSocketFactory();
}
Loading