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 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
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,5 +67,6 @@
},
"files.eol": "\r\n",
"editor.formatOnSave": true,
"thunder-client.saveToWorkspace": true
"thunder-client.saveToWorkspace": true,
"java.jdt.ls.vmargs": "-noverify -Xmx2G -XX:+UseG1GC -XX:+UseStringDeduplication"
}
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.basic_auth.enabled=false"
})
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.basic_auth.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,14 +18,22 @@ public SecurityProperties OAuthProperties() {
return new SecurityProperties();
}

@Bean
@ConditionalOnProperty(prefix = "hapi.fhir.security.basic_auth", name = "enabled", havingValue = "true", matchIfMissing = false)
public AuthenticationInterceptor authenticationInterceptor() {
return new AuthenticationInterceptor();
}

@Bean
@Conditional(OnR4Condition.class)
@ConditionalOnProperty(prefix = "hapi.fhir.security.oauth", name = "enabled", havingValue = "true")
public MetadataExtender<org.hl7.fhir.r4.model.CapabilityStatement> oAuthProviderR4() {
return new org.opencds.cqf.ruler.security.r4.OAuthProvider();
}

@Bean
@Conditional(OnDSTU3Condition.class)
@ConditionalOnProperty(prefix = "hapi.fhir.security.oauth", name = "enabled", havingValue = "true")
public MetadataExtender<org.hl7.fhir.dstu3.model.CapabilityStatement> oAuthProviderDstu3() {
return new org.opencds.cqf.ruler.security.dstu3.OAuthProvider();
}
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 BasicAuth basic_auth;

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

public BasicAuth getBasicAuth() {
return this.basic_auth;
}

public void setBasicAuth(BasicAuth basic_auth) {
this.basic_auth = basic_auth;
}

private OAuth oAuth = new OAuth();

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

public static class BasicAuth {

private boolean enabled = false;
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;
private boolean enabled = false;

public boolean getEnabled() {
return enabled;
}

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

public boolean getSecurityCors() {
return securityCors;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package org.opencds.cqf.ruler.security.interceptor;

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

import java.util.Base64;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

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 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;

@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.getBasicAuth().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("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("Missing or invalid Authorization header");
}

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

if (!StringUtils.equals(username.trim(), securityProperties.getBasicAuth().getUsername().trim()) ||
!StringUtils.equals(password.trim(), securityProperties.getBasicAuth().getPassword().trim())) {
throw new AuthenticationException("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
basic_auth:
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
Expand Up @@ -12,7 +12,7 @@

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = { OAuthProviderIT.class,
SecurityConfig.class }, properties = {
"hapi.fhir.fhir_version=dstu3"
"hapi.fhir.fhir_version=dstu3", "hapi.fhir.security.oauth.enabled=true"
})
public class OAuthProviderIT extends RestIntegrationTest {
@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package org.opencds.cqf.ruler.security.interceptor;

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

import org.hl7.fhir.r4.model.Bundle;
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.basic_auth.enabled=true",
"hapi.fhir.security.basic_auth.username=someuser",
"hapi.fhir.security.basic_auth.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("test-bundle.json");
Bundle bundle = (Bundle) getFhirContext().newJsonParser().parseResource(bundleAsText);
getClient().transaction().withBundle(bundle)
.withAdditionalHeader("Authorization", "Basic c29tZXVzZXI6dGhlcGFzc3dvcmQ=")
.execute();

}

@Test
public void testMeasureEvaluateAuthExceptionWithoutHeader() throws Exception {
String bundleAsText = stringFromResource("test-bundle.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("test-bundle.json");
Bundle bundle = (Bundle) getFhirContext().newJsonParser().parseResource(bundleAsText);

assertThrows(AuthenticationException.class, () -> {
getClient().transaction().withBundle(bundle)
.withAdditionalHeader("Authorization", "Basic blahblah")
.execute();
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = { OAuthProviderIT.class,
SecurityConfig.class }, properties = {
"hapi.fhir.fhir_version=r4"
"hapi.fhir.fhir_version=r4", "hapi.fhir.security.oauth.enabled=true"
})
public class OAuthProviderIT extends RestIntegrationTest {
@Test
Expand Down
Loading