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

Authentication interceptor #528

Merged
merged 17 commits into from
Jun 6, 2022
Merged
Show file tree
Hide file tree
Changes from 12 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
27 changes: 14 additions & 13 deletions core/src/main/java/org/opencds/cqf/ruler/Server.java
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,20 @@ protected void initialize() throws ServletException {
// >=5.7.0 we need to remove this
this.registerProvider(valueSetOperationProvider);

log.info("Loading operation providers from plugins");
Map<String, OperationProvider> providers = applicationContext.getBeansOfType(OperationProvider.class);
for (OperationProvider o : providers.values()) {
log.info("Registering {}", o.getClass().getName());
this.registerProvider(o);
}

log.info("Loading interceptors from plugins");
Map<String, Interceptor> interceptors = applicationContext.getBeansOfType(Interceptor.class);
for (Interceptor o : interceptors.values()) {
log.info("Registering {} interceptor", o.getClass().getName());
this.registerInterceptor(o);
}

log.info("Loading metadata extenders from plugins");
Map<String, MetadataExtender> extenders = applicationContext.getBeansOfType(MetadataExtender.class);
for (MetadataExtender o : extenders.values()) {
Expand Down Expand Up @@ -120,18 +134,5 @@ protected void initialize() throws ServletException {
}
}

log.info("Loading operation providers from plugins");
Map<String, OperationProvider> providers = applicationContext.getBeansOfType(OperationProvider.class);
for (OperationProvider o : providers.values()) {
log.info("Registering {}", o.getClass().getName());
this.registerProvider(o);
}

log.info("Loading interceptors from plugins");
Map<String, Interceptor> interceptors = applicationContext.getBeansOfType(Interceptor.class);
for (Interceptor o : interceptors.values()) {
log.info("Registering {} interceptor", o.getClass().getName());
this.registerInterceptor(o);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = { Application.class,
CdsHooksConfig.class }, properties = {
"hapi.fhir.fhir_version=dstu3",
"hapi.fhir.fhir_version=dstu3", "hapi.fhir.security.security_configuration.enabled=false"
JPercival marked this conversation as resolved.
Show resolved Hide resolved
})
public class CdsHooksServletIT extends RestIntegrationTest {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = { Application.class,
CdsHooksConfig.class }, properties = {
"hapi.fhir.fhir_version=r4",
"hapi.fhir.fhir_version=r4", "hapi.fhir.security.security_configuration.enabled=false"
})
public class CdsHooksServletIT extends RestIntegrationTest {
String ourCdsBase;
Expand Down
20 changes: 20 additions & 0 deletions plugin/security/pom.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<dependencies>
<dependency>
<groupId>org.opencds.cqf.ruler</groupId>
<artifactId>cqf-ruler-dev-tools</artifactId>
<version>0.5.0-SNAPSHOT</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.opencds.cqf.ruler</groupId>
<artifactId>cqf-ruler-cql</artifactId>
<version>0.5.0-SNAPSHOT</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.opencds.cqf.ruler</groupId>
<artifactId>cqf-ruler-cr</artifactId>
<version>0.5.0-SNAPSHOT</version>
<scope>test</scope>
</dependency>
</dependencies>
<parent>
<groupId>org.opencds.cqf.ruler</groupId>
<artifactId>cqf-ruler-plugin</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import org.opencds.cqf.ruler.api.MetadataExtender;
import org.opencds.cqf.ruler.external.annotations.OnDSTU3Condition;
import org.opencds.cqf.ruler.external.annotations.OnR4Condition;
import org.opencds.cqf.ruler.security.interceptor.AuthenticationInterceptor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
Expand All @@ -17,6 +18,9 @@ public SecurityProperties OAuthProperties() {
return new SecurityProperties();
}

@Bean
public AuthenticationInterceptor authenticationInterceptor() { return new AuthenticationInterceptor(); }

@Bean
@Conditional(OnR4Condition.class)
public MetadataExtender<org.hl7.fhir.r4.model.CapabilityStatement> oAuthProviderR4() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ public class SecurityProperties {

private boolean enabled = false;

private SecurityConfiguration security_configuration;

public boolean getEnabled() {
return enabled;
}
Expand All @@ -15,6 +17,14 @@ public void setEnabled(boolean enabled) {
this.enabled = enabled;
}

public SecurityConfiguration getSecurityConfiguration() {
return this.security_configuration;
}

public void setSecurityConfiguration(SecurityConfiguration security_configuration) {
this.security_configuration = security_configuration;
}

private OAuth oAuth = new OAuth();

public OAuth getOAuth() {
Expand All @@ -25,6 +35,33 @@ public void setOAuth(OAuth oAuth) {
this.oAuth = oAuth;
}

public static class SecurityConfiguration {

private boolean enabled;
private String username;
private String password;

public boolean getEnabled() {
return enabled;
}

public void setEnabled(boolean enabled) {
this.enabled = enabled;
}

public String getUsername() {
return this.username;
}

public void setUsername(String username) { this.username = username; }

public String getPassword() {
return this.password;
}

public void setPassword(String password) { this.password = password; }
}

public class OAuth {
private boolean securityCors = true;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package org.opencds.cqf.ruler.security.interceptor;

import ca.uhn.fhir.interceptor.api.Hook;
import ca.uhn.fhir.interceptor.api.Interceptor;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.exceptions.AuthenticationException;
import org.apache.commons.lang3.StringUtils;
import org.opencds.cqf.ruler.security.SecurityProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Base64;

import static java.nio.charset.StandardCharsets.UTF_8;


@Interceptor
public class AuthenticationInterceptor implements org.opencds.cqf.ruler.api.Interceptor {
private final Logger myLog = LoggerFactory.getLogger(AuthenticationInterceptor.class);

@Autowired
private SecurityProperties securityProperties;

@Hook(Pointcut.SERVER_INCOMING_REQUEST_POST_PROCESSED)
public boolean incomingRequestPostProcessed(RequestDetails theRequestDetails, HttpServletRequest theRequest,
HttpServletResponse theResponse) throws AuthenticationException {
if(securityProperties.getSecurityConfiguration().getEnabled()) {
String authHeader = theRequest.getHeader("Authorization");

myLog.info("incoming request intercepted");
// The format of the header must be:
// Authorization: Basic [base64 of username:password]
if (authHeader == null || !authHeader.startsWith("Basic ")) {
throw new AuthenticationException(642 + "Missing or invalid Authorization header");
}

String base64 = authHeader.substring("Basic ".length());
String base64decoded = new String(Base64.getDecoder().decode(base64), UTF_8);
String[] parts = base64decoded.split(":");

if(parts.length <= 1) {
throw new AuthenticationException(642 + "Missing or invalid Authorization header");
}

String username = parts[0];
String password = parts[1];

/*
* Here we test for a hardcoded username & password. This is

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should remove this comment.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

* not typically how you would implement this in a production
* system of course..
*/

if (!StringUtils.equals(username.trim(), securityProperties.getSecurityConfiguration().getUsername().trim()) ||
!StringUtils.equals(password.trim(), securityProperties.getSecurityConfiguration().getPassword().trim())) {
throw new AuthenticationException(643 + "Invalid username or password");
}

myLog.info("Authorization successful");
}

// Return true to allow the request to proceed
return true;
}

}
5 changes: 5 additions & 0 deletions plugin/security/src/main/resources/application.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
hapi:
fhir:
security:
enabled: true
security_configuration:
JPercival marked this conversation as resolved.
Show resolved Hide resolved
enabled: false
username: someuser
password: thepassword
oauth:
securityCors: true
securityUrl: http://fhir-registry.smarthealthit.org/StructureDefinition/oauth-uris
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package org.opencds.cqf.ruler.security.interceptor;

import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;

import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.MeasureReport;
import org.hl7.fhir.r4.model.Parameters;
import org.hl7.fhir.r4.model.StringType;
import org.junit.jupiter.api.Test;
import org.opencds.cqf.ruler.cql.CqlConfig;
import org.opencds.cqf.ruler.cr.CrConfig;
import org.opencds.cqf.ruler.devtools.DevToolsConfig;
import org.opencds.cqf.ruler.security.SecurityConfig;
import org.opencds.cqf.ruler.test.RestIntegrationTest;
import org.springframework.boot.test.context.SpringBootTest;

import ca.uhn.fhir.rest.server.exceptions.AuthenticationException;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = {AuthenticatorInterceptorIT.class,
SecurityConfig.class, CrConfig.class, CqlConfig.class, DevToolsConfig.class }, properties = {
"hapi.fhir.fhir_version=r4", "hapi.fhir.security.security_configuration.enabled=true",
"hapi.fhir.security.security_configuration.username=someuser",
"hapi.fhir.security.security_configuration.password=thepassword"
})
public class AuthenticatorInterceptorIT extends RestIntegrationTest {

@Test
public void testMeasureEvaluateAuth() throws Exception {
JPercival marked this conversation as resolved.
Show resolved Hide resolved
String bundleAsText = stringFromResource("Exm104FhirR4MeasureBundle.json");
Bundle bundle = (Bundle) getFhirContext().newJsonParser().parseResource(bundleAsText);
getClient().transaction().withBundle(bundle)
.withAdditionalHeader("Authorization", "Basic c29tZXVzZXI6dGhlcGFzc3dvcmQ=")
.execute();

Parameters params = new Parameters();
params.addParameter().setName("periodStart").setValue(new StringType("2019-01-01"));
params.addParameter().setName("periodEnd").setValue(new StringType("2020-01-01"));
params.addParameter().setName("reportType").setValue(new StringType("individual"));
params.addParameter().setName("subject").setValue(new StringType("Patient/numer-EXM104"));
params.addParameter().setName("lastReceivedOn").setValue(new StringType("2019-12-12"));

MeasureReport returnMeasureReport = getClient().operation()
.onInstance(new IdType("Measure", "measure-EXM104-8.2.000"))
.named("$evaluate-measure")
.withParameters(params)
.withAdditionalHeader("Authorization", "Basic c29tZXVzZXI6dGhlcGFzc3dvcmQ=")
.returnResourceType(MeasureReport.class)
.execute();

assertNotNull(returnMeasureReport);
}

@Test
public void testMeasureEvaluateAuthExceptionWithoutHeader() throws Exception {
String bundleAsText = stringFromResource("Exm104FhirR4MeasureBundle.json");
Bundle bundle = (Bundle) getFhirContext().newJsonParser().parseResource(bundleAsText);

assertThrows(AuthenticationException.class, () -> {
getClient().transaction().withBundle(bundle).execute();
});
}

@Test
public void testMeasureEvaluateAuthExceptionWrongAuthInfo() throws Exception {
String bundleAsText = stringFromResource("Exm104FhirR4MeasureBundle.json");
Bundle bundle = (Bundle) getFhirContext().newJsonParser().parseResource(bundleAsText);

assertThrows(AuthenticationException.class, () -> {
getClient().transaction().withBundle(bundle)
.withAdditionalHeader("Authorization", "Basic blahblah")
.execute();
});
}
}
Loading