Skip to content

Commit

Permalink
Added the ability to validate a jwt token from a public client using …
Browse files Browse the repository at this point in the history
…environment varaibles for a public key and issuer
  • Loading branch information
amphillipsLGC committed Jun 13, 2023
1 parent 00420db commit 5761dd5
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 10 deletions.
23 changes: 23 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@
<artifactId>mssql-jdbc</artifactId>
</dependency>

<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version>
</dependency>

<!-- Needed for Email subscriptions -->
<dependency>
<groupId>org.simplejavamail</groupId>
Expand Down Expand Up @@ -356,6 +362,12 @@
<version>${spring_boot_version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.22.0</version>
<scope>compile</scope>
</dependency>

</dependencies>

Expand Down Expand Up @@ -575,6 +587,17 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>${spring_version}</version>
<exclusions>
<exclusion>
<artifactId>commons-logging</artifactId>
<groupId>commons-logging</groupId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</profile>
<profile>
Expand Down
35 changes: 35 additions & 0 deletions src/main/java/ca/uhn/fhir/jpa/starter/Application.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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({
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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<String> protectedEndpoints, List<String> publicEndpoints) {
public IdentityMatchingAuthInterceptor(boolean enableAuthentication, String issuer, String publicKey, String introspectUrl, String clientId, String clientSecret, List<String> protectedEndpoints, List<String> publicEndpoints) {
this.enableAuthentication = enableAuthentication;
this.issuer = issuer;
this.publicKey = publicKey;
this.introspectUrl = introspectUrl;
this.clientId = clientId;
this.clientSecret = clientSecret;
Expand Down Expand Up @@ -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 {
Expand All @@ -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) {
Expand Down Expand Up @@ -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());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
Expand All @@ -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; }
Expand Down

0 comments on commit 5761dd5

Please sign in to comment.