Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add OpenID Connect authentication for connecting to OAuth secured FHIR stores #113

Merged
merged 1 commit into from
May 29, 2024
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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/mii-process-feasibility/target/
/mii-process-feasibility/src/test/resources/de/medizininformatik_initiative/feasibility_dsf_process/client/store/certs
/mii-process-feasibility/src/test/resources/de/medizininformatik_initiative/process/feasibility/client/certs

/mii-process-feasibility-docker-test-setup/certs/*.pem
/mii-process-feasibility-docker-test-setup/certs/*.p12
Expand Down
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
Loading