Skip to content

Commit

Permalink
Merge pull request #113 from medizininformatik-initiative/feat/add-op…
Browse files Browse the repository at this point in the history
…enid-connect

Add OpenID Connect authentication for connecting to OAuth secured FHIR stores
  • Loading branch information
EmteZogaf authored May 29, 2024
2 parents 6d7069b + 7141015 commit 8567d7b
Show file tree
Hide file tree
Showing 10 changed files with 2,342 additions and 28 deletions.
56 changes: 31 additions & 25 deletions mii-process-feasibility/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,31 +104,37 @@ Besides the [common DSF settings controlled by different environment variables][

**All of them share the same prefix `DE_MEDIZININFORMATIK_INITIATIVE_FEASIBILITY_DSF_PROCESS_`:**

| EnvVar | Description | Default |
|--------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|
| CLIENT_STORE_PROXY_HOST | Forward proxy host. | `null` |
| CLIENT_STORE_PROXY_PORT | Forward proxy port. | `` |
| CLIENT_STORE_PROXY_USERNAME | Username for a forward proxy if it requires one. | `null` |
| CLIENT_STORE_PROXY_PASSWORD | Password for a forward proxy if it requires one. | `null` |
| CLIENT_STORE_AUTH_BEARER_TOKEN | Bearer token used for authentication against a client target. Do not prefix this with `Bearer `! | `null` |
| CLIENT_STORE_AUTH_BASIC_USERNAME | Username for basic authentication against a FHIR server client target. | `null` |
| CLIENT_STORE_AUTH_BASIC_PASSWORD | Password for basic authentication against a FHIR server client target. | `null` |
| CLIENT_STORE_TIMEOUT_CONNECT | Timeout for establishing a connection to a FHIR server client target in `ms`. | `2000` |
| CLIENT_STORE_TIMEOUT_CONNECT_REQUEST | Timeout for requesting a connection to a FHIR server client target in `ms`. | `20000` |
| CLIENT_STORE_TIMEOUT_SOCKET | Timeout for blocking a read / write network operation to a FHIR server without failing in `ms`. | `300000` |
| CLIENT_STORE_TRUST_STORE_PATH | Path to a trust store used for connecting to a FHIR server. Necessary when using self-signed certificates. | `null` |
| CLIENT_STORE_TRUST_STORE_PASSWORD | Password for opening the trust store used for connecting to a FHIR server. | `null` |
| CLIENT_STORE_KEY_STORE_PATH | Path to a key store used for authenticating against a FHIR server or proxy using a client certificate. | `null` |
| CLIENT_STORE_KEY_STORE_PASSWORD | Password for opening the key store used for authenticating against a FHIR server or proxy. | `null` |
| CLIENT_STORE_BASE_URL | Base URL to a FHIR server or proxy for feasibility evaluation. This can also be the base URL of a reverse proxy if used. Only required if evaluation strategy is set to `cql`. | `` |
| CLIENT_FLARE_BASE_URL | Base URL to a FLARE instance. Only required if evaluation strategy is set to `structured-query`. | `` |
| CLIENT_FLARE_TIMEOUT_CONNECT | Timeout for establishing a connection to a FLARE client target in `ms`. | `300000` |
| EVALUATION_STRATEGY | Defines whether the feasibility shall be evaluated using `cql` or `structured-query`. Using the latter requires a FLARE instance. | `cql` |
| EVALUATION_OBFUSCATE | Defines whether the feasibility evaluation result shall be obfuscated. | `true` |
| EVALUATION_OBFUSCATION_SENSITIVITY | Sets the sensitivity of the Laplace distribution function used for obfuscating the result. | `1.0` |
| EVALUATION_OBFUSCATION_EPSILON | Sets the epsilon value of the Laplace distribution function used for obfuscating the result. | `0.5` |
| RATE_LIMIT_COUNT | Sets the hard limit for the maximum allowed number of requests during the configured rate limit interval after no further requests will be processed | `999` |
| RATE_LIMIT_INTERVAL_DURATION | Sets the size of the time window used for calculating the request rate. The value is required to be given in the [ISO 8601 format][10] (e.g. "PT1H30M10S"). | `PT1H` |
| EnvVar | Description | Default |
|-----------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|
| CLIENT_STORE_PROXY_HOST | Forward proxy host. | `null` |
| CLIENT_STORE_PROXY_PORT | Forward proxy port. | `` |
| CLIENT_STORE_PROXY_USERNAME | Username for a forward proxy if it requires one. | `null` |
| CLIENT_STORE_PROXY_PASSWORD | Password for a forward proxy if it requires one. | `null` |
| CLIENT_STORE_AUTH_BEARER_TOKEN | Bearer token used for authentication against a client target. Do not prefix this with `Bearer `! | `null` |
| CLIENT_STORE_AUTH_BASIC_USERNAME | Username for basic authentication against a FHIR server client target. | `null` |
| CLIENT_STORE_AUTH_BASIC_PASSWORD | Password for basic authentication against a FHIR server client target. | `null` |
| CLIENT_STORE_AUTH_OAUTH_CLIENT_ID | Client ID for authentication against a OpenID Connect provider to gain access token for FHIR server client target. | `null` |
| CLIENT_STORE_AUTH_OAUTH_CLIENT_PASSWORD | Client Password for authentication against a OpenID Connect provider to gain access token for FHIR server client target. | `null` |
| CLIENT_STORE_AUTH_OAUTH_PROXY_HOST | Forward proxy host for connecting to OpenID Connect provider. | `null` |
| CLIENT_STORE_AUTH_OAUTH_PROXY_PORT | Forward proxy port for connecting to OpenID Connect provider. | `` |
| CLIENT_STORE_AUTH_OAUTH_PROXY_USERNAME | Username for a forward proxy for connecting to OpenID Connect provider if it requires one. | `null` |
| CLIENT_STORE_AUTH_OAUTH_PROXY_PASSWORD | Password for a forward proxy for connecting to OpenID Connect provider if it requires one. | `null` |
| CLIENT_STORE_TIMEOUT_CONNECT | Timeout for establishing a connection to a FHIR server client target in `ms`. | `2000` |
| CLIENT_STORE_TIMEOUT_CONNECT_REQUEST | Timeout for requesting a connection to a FHIR server client target in `ms`. | `20000` |
| CLIENT_STORE_TIMEOUT_SOCKET | Timeout for blocking a read / write network operation to a FHIR server without failing in `ms`. | `300000` |
| CLIENT_STORE_TRUST_STORE_PATH | Path to a trust store used for connecting to a FHIR server. Necessary when using self-signed certificates. | `null` |
| CLIENT_STORE_TRUST_STORE_PASSWORD | Password for opening the trust store used for connecting to a FHIR server. | `null` |
| CLIENT_STORE_KEY_STORE_PATH | Path to a key store used for authenticating against a FHIR server or proxy using a client certificate. | `null` |
| CLIENT_STORE_KEY_STORE_PASSWORD | Password for opening the key store used for authenticating against a FHIR server or proxy. | `null` |
| CLIENT_STORE_BASE_URL | Base URL to a FHIR server or proxy for feasibility evaluation. This can also be the base URL of a reverse proxy if used. Only required if evaluation strategy is set to `cql`. | `` |
| CLIENT_FLARE_BASE_URL | Base URL to a FLARE instance. Only required if evaluation strategy is set to `structured-query`. | `` |
| CLIENT_FLARE_TIMEOUT_CONNECT | Timeout for establishing a connection to a FLARE client target in `ms`. | `300000` |
| EVALUATION_STRATEGY | Defines whether the feasibility shall be evaluated using `cql` or `structured-query`. Using the latter requires a FLARE instance. | `cql` |
| EVALUATION_OBFUSCATE | Defines whether the feasibility evaluation result shall be obfuscated. | `true` |
| EVALUATION_OBFUSCATION_SENSITIVITY | Sets the sensitivity of the Laplace distribution function used for obfuscating the result. | `1.0` |
| EVALUATION_OBFUSCATION_EPSILON | Sets the epsilon value of the Laplace distribution function used for obfuscating the result. | `0.5` |
| RATE_LIMIT_COUNT | Sets the hard limit for the maximum allowed number of requests during the configured rate limit interval after no further requests will be processed | `999` |
| RATE_LIMIT_INTERVAL_DURATION | Sets the size of the time window used for calculating the request rate. The value is required to be given in the [ISO 8601 format][10] (e.g. "PT1H30M10S"). | `PT1H` |

## Compatibility

Expand Down
15 changes: 13 additions & 2 deletions mii-process-feasibility/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -73,15 +73,20 @@
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>3.1.1</version>
<version>3.2.5</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>6.0.12</version>
<version>6.1.6</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>oauth2-oidc-sdk</artifactId>
<version>11.10.1</version>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
Expand All @@ -106,6 +111,12 @@
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.dasniko</groupId>
<artifactId>testcontainers-keycloak</artifactId>
<version>3.3.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>dev.dsf</groupId>
<artifactId>dsf-fhir-validation</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package de.medizininformatik_initiative.process.feasibility.client.store;

public class OAuth2ClientException extends RuntimeException {

private static final long serialVersionUID = -5840162115734733430L;

public OAuth2ClientException(String message) {
super(message);
}

public OAuth2ClientException(String message, Exception cause) {
super(message, cause);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package de.medizininformatik_initiative.process.feasibility.client.store;

import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.client.api.IClientInterceptor;
import ca.uhn.fhir.rest.client.api.IHttpRequest;
import ca.uhn.fhir.rest.client.api.IHttpResponse;
import com.nimbusds.oauth2.sdk.AccessTokenResponse;
import com.nimbusds.oauth2.sdk.ClientCredentialsGrant;
import com.nimbusds.oauth2.sdk.ParseException;
import com.nimbusds.oauth2.sdk.TokenErrorResponse;
import com.nimbusds.oauth2.sdk.TokenRequest;
import com.nimbusds.oauth2.sdk.TokenResponse;
import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic;
import com.nimbusds.oauth2.sdk.auth.Secret;
import com.nimbusds.oauth2.sdk.http.HTTPRequest;
import com.nimbusds.oauth2.sdk.id.ClientID;
import com.nimbusds.oauth2.sdk.token.AccessToken;
import org.joda.time.DateTime;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.Proxy.Type;
import java.net.URI;
import java.util.Base64;
import java.util.Optional;

final class OAuthInterceptor implements IClientInterceptor {

private static final String HEADER_PROXY_AUTHORIZATION = "Proxy-Authorization";
private static final int TOKEN_EXPIRY_THRESHOLD = 10000;
private HTTPRequest tokenRequest;
private AccessToken token;
private DateTime tokenExpiry;

public OAuthInterceptor(String oauthClientId, String oauthClientSecret, String oauthTokenUrl,
Optional<String> proxyHost, Optional<Integer> proxyPort, Optional<String> proxyUsername,
Optional<String> proxyPassword) {
super();
ClientSecretBasic clientAuth = new ClientSecretBasic(new ClientID(oauthClientId),
new Secret(oauthClientSecret));
HTTPRequest request = new TokenRequest(URI.create(oauthTokenUrl), clientAuth, new ClientCredentialsGrant())
.toHTTPRequest();

if (proxyHost.isPresent() && proxyPort.isPresent()) {
Proxy proxy = new Proxy(Type.HTTP,
InetSocketAddress.createUnresolved(proxyHost.get(), proxyPort.get()));
request.setProxy(proxy);

if (proxyUsername.isPresent() && proxyPassword.isPresent()) {
request.setHeader(HEADER_PROXY_AUTHORIZATION,
generateBasicAuthHeader(proxyUsername.get(), proxyPassword.get()));
}
}
tokenRequest = request;
}

private String generateBasicAuthHeader(String username, String password) {
return Constants.HEADER_AUTHORIZATION_VALPREFIX_BASIC
+ Base64.getEncoder().encodeToString((username + ":" + password).getBytes(Constants.CHARSET_US_ASCII));
}

public String getToken() {
if (token == null || tokenExpiry == null || tokenExpiry.isBefore(DateTime.now().plus(TOKEN_EXPIRY_THRESHOLD))) {
try {
TokenResponse response = TokenResponse.parse(tokenRequest.send());
if (!response.indicatesSuccess()) {
TokenErrorResponse errorResponse = response.toErrorResponse();
throw new OAuth2ClientException(errorResponse.getErrorObject().getCode() + " - "
+ errorResponse.getErrorObject().getDescription());
}
AccessTokenResponse successResponse = response.toSuccessResponse();

token = successResponse.getTokens().getAccessToken();
tokenExpiry = DateTime.now().plus(token.getLifetime() * 1000);
} catch (ParseException | IOException e) {
throw new OAuth2ClientException("OAuth2 access token tokenRequest failed", e);
}
}
return token.getValue();
}

@Override
public void interceptRequest(IHttpRequest theRequest) {
theRequest.addHeader(Constants.HEADER_AUTHORIZATION,
Constants.HEADER_AUTHORIZATION_VALPREFIX_BEARER + getToken());
}

@Override
public void interceptResponse(IHttpResponse theResponse) throws IOException {
}
}
Loading

0 comments on commit 8567d7b

Please sign in to comment.