diff --git a/pom.xml b/pom.xml
index c196b8ac..b3af6603 100644
--- a/pom.xml
+++ b/pom.xml
@@ -65,6 +65,12 @@
mssql-jdbc
+
+ com.auth0
+ java-jwt
+ 4.4.0
+
+
org.simplejavamail
@@ -356,6 +362,12 @@
${spring_boot_version}
test
+
+ org.assertj
+ assertj-core
+ 3.22.0
+ compile
+
@@ -575,6 +587,17 @@
+
+ org.springframework
+ spring-web
+ ${spring_version}
+
+
+ commons-logging
+ commons-logging
+
+
+
diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/Application.java b/src/main/java/ca/uhn/fhir/jpa/starter/Application.java
index 26b34e17..b2473f35 100644
--- a/src/main/java/ca/uhn/fhir/jpa/starter/Application.java
+++ b/src/main/java/ca/uhn/fhir/jpa/starter/Application.java
@@ -18,6 +18,7 @@
import ca.uhn.fhir.jpa.subscription.match.config.WebsocketDispatcherConfig;
import ca.uhn.fhir.jpa.subscription.submit.config.SubscriptionSubmitterConfig;
import ca.uhn.fhir.rest.server.RestfulServer;
+import ca.uhn.fhir.rest.server.interceptor.CorsInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.boot.SpringApplication;
@@ -31,8 +32,11 @@
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Import;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
+import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.servlet.DispatcherServlet;
+import java.util.Arrays;
+
@ServletComponentScan(basePackageClasses = {RestfulServer.class}) //, UnHapiServlet.class
@SpringBootApplication(exclude = {ElasticsearchRestClientAutoConfiguration.class})
@Import({
@@ -89,11 +93,42 @@ public ServletRegistrationBean hapiServletRegistration(RestfulServer restfulServ
//register FAST security interceptors
DiscoveryInterceptor securityDiscoveryInterceptor = new DiscoveryInterceptor();
IdentityMatchingAuthInterceptor authInterceptor = new IdentityMatchingAuthInterceptor(securityConfig.isEnableAuthentication(),
+ securityConfig.getIssuer(), securityConfig.getPublicKey(),
securityConfig.getIntrospectionUrl(), securityConfig.getClientId(), securityConfig.getClientSecret(),
securityConfig.getProtectedEndpoints(), securityConfig.getPublicEndpoints());
restfulServer.registerInterceptor(securityDiscoveryInterceptor);
restfulServer.registerInterceptor(authInterceptor);
+ //check if there is existing CORS configuration, if so add 'x-allow-public-access' to the allowed headers, otherwise create a new CORS interceptor
+ var existingCorsInterceptor = restfulServer.getInterceptorService().getAllRegisteredInterceptors().stream().filter(interceptor -> interceptor instanceof CorsInterceptor).findFirst().orElse(null);
+ if (existingCorsInterceptor != null) {
+ // Cast the interceptor to CorsInterceptor
+ CorsInterceptor corsInterceptor = (CorsInterceptor) existingCorsInterceptor;
+
+ // Add custom header to the existing CORS configuration
+ corsInterceptor.getConfig().addAllowedHeader("x-allow-public-access");
+ }
+ else {
+ // Define your CORS configuration
+ CorsConfiguration config = new CorsConfiguration();
+ config.addAllowedHeader("x-fhir-starter");
+ config.addAllowedHeader("Origin");
+ config.addAllowedHeader("Accept");
+ config.addAllowedHeader("X-Requested-With");
+ config.addAllowedHeader("Content-Type");
+ config.addAllowedHeader("x-allow-public-access");
+
+ config.addAllowedOrigin("*");
+
+ config.addExposedHeader("Location");
+ config.addExposedHeader("Content-Location");
+ config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"));
+
+ // Create the interceptor and register it
+ CorsInterceptor corsInterceptor = new CorsInterceptor(config);
+ restfulServer.registerInterceptor(corsInterceptor);
+ }
+
ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean();
beanFactory.autowireBean(restfulServer);
servletRegistrationBean.setServlet(restfulServer);
diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/operations/IdentityMatching.java b/src/main/java/ca/uhn/fhir/jpa/starter/operations/IdentityMatching.java
index f57b110f..8d6ebd61 100644
--- a/src/main/java/ca/uhn/fhir/jpa/starter/operations/IdentityMatching.java
+++ b/src/main/java/ca/uhn/fhir/jpa/starter/operations/IdentityMatching.java
@@ -7,7 +7,6 @@
import ca.uhn.fhir.jpa.starter.operations.models.IdentityMatchingScorer;
import ca.uhn.fhir.model.base.composite.BaseIdentifierDt;
import ca.uhn.fhir.model.dstu2.composite.IdentifierDt;
-import ca.uhn.fhir.model.valueset.BundleTypeEnum;
import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.ResourceParam;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
@@ -17,7 +16,6 @@
import ca.uhn.fhir.rest.gclient.StringClientParam;
import ca.uhn.fhir.rest.param.*;
import org.apache.commons.lang3.StringUtils;
-import org.hl7.fhir.BundleType;
import org.hl7.fhir.r4.model.*;
import java.util.ArrayList;
import java.util.HashSet;
diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/security/IdentityMatchingAuthInterceptor.java b/src/main/java/ca/uhn/fhir/jpa/starter/security/IdentityMatchingAuthInterceptor.java
index bef654dc..2208b365 100644
--- a/src/main/java/ca/uhn/fhir/jpa/starter/security/IdentityMatchingAuthInterceptor.java
+++ b/src/main/java/ca/uhn/fhir/jpa/starter/security/IdentityMatchingAuthInterceptor.java
@@ -4,9 +4,15 @@
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.server.RequestDetails;
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.JWTVerifier;
+import com.auth0.jwt.algorithms.Algorithm;
+import com.auth0.jwt.exceptions.JWTVerificationException;
+import com.auth0.jwt.interfaces.DecodedJWT;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -21,11 +27,21 @@
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.interfaces.RSAPrivateKey;
+import java.security.interfaces.RSAPublicKey;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.X509EncodedKeySpec;
import java.util.List;
@Interceptor
public class IdentityMatchingAuthInterceptor {
private boolean enableAuthentication = false;
+
+ private String issuer;
+
+ private String publicKey;
private String introspectUrl;
private String clientId;
private String clientSecret;
@@ -35,8 +51,10 @@ public class IdentityMatchingAuthInterceptor {
private final Logger _logger = LoggerFactory.getLogger(IdentityMatchingAuthInterceptor.class);
private final String allowPublicAccessHeader = "X-Allow-Public-Access";
- public IdentityMatchingAuthInterceptor(boolean enableAuthentication, String introspectUrl, String clientId, String clientSecret, List protectedEndpoints, List publicEndpoints) {
+ public IdentityMatchingAuthInterceptor(boolean enableAuthentication, String issuer, String publicKey, String introspectUrl, String clientId, String clientSecret, List protectedEndpoints, List publicEndpoints) {
this.enableAuthentication = enableAuthentication;
+ this.issuer = issuer;
+ this.publicKey = publicKey;
this.introspectUrl = introspectUrl;
this.clientId = clientId;
this.clientSecret = clientSecret;
@@ -67,16 +85,18 @@ public boolean incomingRequestPostProcessed(RequestDetails details, HttpServletR
throw new AuthenticationException("Not authorized (authorization header does not contain a bearer token)");
}
+ //if a confidential client, use the client secret and introspect endpoint to validate token
if (!StringUtils.isBlank(clientSecret) && !StringUtils.isBlank(introspectUrl)) {
authenticated = introspectionCheck(authHeader);
- } else {
- authenticated = false;
- _logger.error("Failed to provide client secret and/or introspection url.");
+ } else { //assume public client and validate using the token and supplied public key
+ authenticated = validateToken(authHeader);
}
} catch (AuthenticationException ex) {
_logger.error(ex.getMessage(), ex);
_logger.info("Failed to authenticate request");
+ } catch (NoSuchAlgorithmException | InvalidKeySpecException ex) {
+ throw new RuntimeException(ex);
}
} //otherwise, allow all
else {
@@ -85,10 +105,7 @@ public boolean incomingRequestPostProcessed(RequestDetails details, HttpServletR
}
else { //public access header detected or a public access point was requested - allow request
authenticated = true;
-
- if(publicAccessHeader != null) {
- _logger.info("The 'X-Allow-Public-Access' header was detected, ignoring security configuration.");
- }
+ _logger.info("The 'X-Allow-Public-Access' header was detected, ignoring security configuration.");
}
if(!authenticated) {
@@ -124,4 +141,28 @@ private boolean introspectionCheck(String authHeader) throws JsonProcessingExcep
return false;
}
}
+
+ private boolean validateToken(String authHeader) throws NoSuchAlgorithmException, InvalidKeySpecException {
+
+ var token = authHeader.split(" ")[1];
+
+ //current set up for RSA 256, change as necessary
+ byte[] publicBytes = Base64.decodeBase64(publicKey);
+ X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicBytes);
+ KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+ RSAPublicKey publicKey = (RSAPublicKey) keyFactory.generatePublic(keySpec);
+
+ try {
+ Algorithm algorithm = Algorithm.RSA256(publicKey, null);
+ JWTVerifier verifier = JWT.require(algorithm)
+ .withIssuer(issuer)
+ .build(); //Reusable verifier instance
+ DecodedJWT verifiedJwt = verifier.verify(token);
+
+ return verifiedJwt != null;
+
+ } catch (JWTVerificationException ex){
+ throw new JWTVerificationException(ex.getMessage());
+ }
+ }
}
diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/security/models/SecurityConfig.java b/src/main/java/ca/uhn/fhir/jpa/starter/security/models/SecurityConfig.java
index 930d7e2e..c259bc05 100644
--- a/src/main/java/ca/uhn/fhir/jpa/starter/security/models/SecurityConfig.java
+++ b/src/main/java/ca/uhn/fhir/jpa/starter/security/models/SecurityConfig.java
@@ -8,6 +8,10 @@
public class SecurityConfig {
@Value("${enable-authentication}")
boolean enableAuthentication;
+ @Value("${issuer}")
+ String issuer;
+ @Value("${public-key}")
+ String publicKey;
@Value("${introspection-url}")
String introspectionUrl;
@Value("${client-id}")
@@ -21,6 +25,10 @@ public class SecurityConfig {
public boolean isEnableAuthentication() { return enableAuthentication; }
+ public String getIssuer() { return issuer; }
+
+ public String getPublicKey() { return publicKey; }
+
public String getIntrospectionUrl() { return introspectionUrl; }
public String getClientId() { return clientId; }