diff --git a/IRCT-CL/pom.xml b/IRCT-CL/pom.xml index fc7f00ed..5ac899fd 100644 --- a/IRCT-CL/pom.xml +++ b/IRCT-CL/pom.xml @@ -80,8 +80,21 @@ com.auth0 java-jwt - 3.2.0 + 3.4.0 + + + com.auth0 + jwks-rsa + 0.3.0 + + + + commons-codec + commons-codec + 1.11 + + org.apache.commons diff --git a/IRCT-CL/src/main/java/edu/harvard/hms/dbmi/bd2k/irct/cl/filter/CORSFilter.java b/IRCT-CL/src/main/java/edu/harvard/hms/dbmi/bd2k/irct/cl/filter/CORSFilter.java new file mode 100644 index 00000000..3feeacca --- /dev/null +++ b/IRCT-CL/src/main/java/edu/harvard/hms/dbmi/bd2k/irct/cl/filter/CORSFilter.java @@ -0,0 +1,65 @@ +package edu.harvard.hms.dbmi.bd2k.irct.cl.filter; + +import org.apache.log4j.Logger; + +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.NamingException; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerResponseContext; +import javax.ws.rs.container.ContainerResponseFilter; +import javax.ws.rs.ext.Provider; + +@Provider +/** + * Filter adding support for the Cross-Origin Resource Sharing specification (https://www.w3.org/TR/cors/). + * Configuration with: + * - global/cors_enabled (boolean true / false) + * - global/cors_allow_origin (string for "Allow Origin" value) + */ +public class CORSFilter implements ContainerResponseFilter { + + private Logger logger = Logger.getLogger(this.getClass().getName()); + + private String corsAllowOrigin; + private Boolean corsEnabled; + + /** + * Loads the CORS (Cross-Origin Resource Sharing) configuration. + */ + private void loadConfig() { + if (corsEnabled != null) { + return; + } + + try { + Context ctx = new InitialContext(); + corsEnabled = (Boolean) ctx.lookup("global/cors_enabled"); + corsAllowOrigin = (String) ctx.lookup("global/cors_allow_origin"); + ctx.close(); + } catch (NamingException e) { + logger.debug("CORS configuration NamingException, feature disabled", e); + corsEnabled = false; + } + + if (!corsEnabled) { + logger.info("CORS is not enabled"); + } else { + logger.info("CORS enabled, allow origin: " + this.corsAllowOrigin); + } + } + + @Override + public void filter(ContainerRequestContext requestContext, + ContainerResponseContext responseContext) { + + this.loadConfig(); + + if (this.corsEnabled) { + responseContext.getHeaders().add("Access-Control-Allow-Origin", this.corsAllowOrigin); + responseContext.getHeaders().add("Access-Control-Allow-Credentials", "true"); + responseContext.getHeaders().add("Access-Control-Allow-Headers", "origin, content-type, accept, authorization"); + responseContext.getHeaders().add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, HEAD"); + } + } +} diff --git a/IRCT-CL/src/main/java/edu/harvard/hms/dbmi/bd2k/irct/cl/filter/SessionFilter.java b/IRCT-CL/src/main/java/edu/harvard/hms/dbmi/bd2k/irct/cl/filter/SessionFilter.java index fb175079..8617b2e5 100644 --- a/IRCT-CL/src/main/java/edu/harvard/hms/dbmi/bd2k/irct/cl/filter/SessionFilter.java +++ b/IRCT-CL/src/main/java/edu/harvard/hms/dbmi/bd2k/irct/cl/filter/SessionFilter.java @@ -29,6 +29,9 @@ public class SessionFilter implements Filter { @Inject private IRCTApplication irctApp; + @javax.annotation.Resource(mappedName = "java:global/jwks_uri") + private String jwksUri; + @javax.annotation.Resource(mappedName = "java:global/userField") private String userField; @@ -44,8 +47,10 @@ public void doFilter(ServletRequest req, ServletResponse res, FilterChain fc) th logger.debug("doFilter() Starting"); HttpServletRequest request = (HttpServletRequest) req; - // If processing URL /securityService/*, we are creating a session/secureSession - if (request.getRequestURI().endsWith("/securityService/startSession") || request.getRequestURI().endsWith("/securityService/createKey")) { + // If processing URL /securityService/*, we are creating a session/secureSession; ignore CORS preflight calls + if ( request.getRequestURI().endsWith("/securityService/startSession") || + request.getRequestURI().endsWith("/securityService/createKey") || + request.getMethod().equals("OPTIONS")) { // Do Nothing logger.debug("doFilter() securityService URL is NOT filtered."); } else { @@ -84,7 +89,7 @@ public void doFilter(ServletRequest req, ServletResponse res, FilterChain fc) th //Get information from token introspection endpoint in 2.0 user = sc.ensureUserExists(Utilities.extractUserFromTokenIntrospection((HttpServletRequest) req, this.userField, irctApp.getToken_introspection_url(), irctApp.getToken_introspection_token())); } else{ - user = sc.ensureUserExists(Utilities.extractEmailFromJWT((HttpServletRequest) req, irctApp.getClientSecret(), this.userField)); + user = sc.ensureUserExists(Utilities.extractEmailFromJWT((HttpServletRequest) req, irctApp.getClientSecret(), this.jwksUri, this.userField)); } } @@ -107,7 +112,7 @@ public void doFilter(ServletRequest req, ServletResponse res, FilterChain fc) th // TODO DI-896 change. Since the user above gets created without an actual token, we need // to re-extract the token, from the header and parse it and place it inside the user object, // for future playtime. - if (user.getToken() == null) { + if (user.getToken() == null || !user.getToken().equals(tokenString)) { logger.debug("doFilter() No token in user object, so let's add one."); user.setToken(tokenString); sc.updateUserRecord(user); diff --git a/IRCT-CL/src/main/java/edu/harvard/hms/dbmi/bd2k/irct/cl/rest/SecurityService.java b/IRCT-CL/src/main/java/edu/harvard/hms/dbmi/bd2k/irct/cl/rest/SecurityService.java index e9bce4de..fe9f882c 100644 --- a/IRCT-CL/src/main/java/edu/harvard/hms/dbmi/bd2k/irct/cl/rest/SecurityService.java +++ b/IRCT-CL/src/main/java/edu/harvard/hms/dbmi/bd2k/irct/cl/rest/SecurityService.java @@ -58,6 +58,9 @@ public class SecurityService implements Serializable { @javax.annotation.Resource(mappedName ="java:global/client_secret") private String clientSecret; + @javax.annotation.Resource(mappedName ="java:global/jwks_uri") + private String jwksUri; + @javax.annotation.Resource(mappedName ="java:global/userField") private String userField; @@ -74,7 +77,7 @@ public Response createKey(@Context HttpServletRequest req) { try { User userObject = sc.ensureUserExists(Utilities - .extractEmailFromJWT(req, this.clientSecret, this.userField)); + .extractEmailFromJWT(req, this.clientSecret, this.jwksUri, this.userField)); logger.debug("/createKey user exists"); userObject.setToken(Utilities.extractToken(req)); diff --git a/IRCT-CL/src/main/java/edu/harvard/hms/dbmi/bd2k/irct/cl/util/Utilities.java b/IRCT-CL/src/main/java/edu/harvard/hms/dbmi/bd2k/irct/cl/util/Utilities.java index 534559bc..0aa0147c 100644 --- a/IRCT-CL/src/main/java/edu/harvard/hms/dbmi/bd2k/irct/cl/util/Utilities.java +++ b/IRCT-CL/src/main/java/edu/harvard/hms/dbmi/bd2k/irct/cl/util/Utilities.java @@ -3,6 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package edu.harvard.hms.dbmi.bd2k.irct.cl.util; +import com.auth0.jwk.*; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.JWTVerificationException; import com.auth0.jwt.interfaces.Claim; @@ -33,6 +34,9 @@ import javax.ws.rs.core.MultivaluedMap; import java.io.IOException; import java.io.UnsupportedEncodingException; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.interfaces.RSAPublicKey; import java.util.HashMap; import java.util.Map; @@ -44,6 +48,16 @@ public class Utilities { private static Logger logger = Logger.getLogger(Utilities.class); + private static JwkProvider jwkProvider = null; + private static String jwksUri = ""; + private static JwkProvider getJwkProvider(String pJwksUri) throws MalformedURLException { + if (!jwksUri.equals(pJwksUri) || jwkProvider == null) { + jwksUri = pJwksUri; + jwkProvider = new GuavaCachedJwkProvider(new UrlJwkProvider(new URL(jwksUri))); + } + return jwkProvider; + } + /** * Returns all the first values from a MultiValue Map * @@ -88,12 +102,13 @@ public static Map getFirstFromMultiMap( * extract specific user field from JWT token * @param req * @param clientSecret + * @param pJwksUri * @param userField specifies which user field is going to be extracted from JWT token * @return * @throws NotAuthorizedException */ - public static String extractEmailFromJWT(HttpServletRequest req, String clientSecret, String userField) { - logger.debug("extractEmailFromJWT() with secret:"+clientSecret); + public static String extractEmailFromJWT(HttpServletRequest req, String clientSecret, String pJwksUri, String userField) { + logger.debug("extractEmailFromJWT() with secret:"+clientSecret+" and jwks URI:" + pJwksUri); //No point in doing anything if there's no userField if (userField == null){ @@ -112,27 +127,46 @@ public static String extractEmailFromJWT(HttpServletRequest req, String clientSe DecodedJWT jwt = null; try { logger.debug("validateAuthorizationHeader() validating with un-decoded secret."); - jwt = com.auth0.jwt.JWT.require(Algorithm - .HMAC256(clientSecret - .getBytes("UTF-8"))) - .build() - .verify(tokenString); - } catch (UnsupportedEncodingException e){ - logger.error("extractEmailFromJWT() getting bytes for initialize jwt token algorithm error: " + e.getMessage()); - throw new NotAuthorizedException("Token is invalid, please request a new one"); - } catch (JWTVerificationException e) { - try{ + jwt = com.auth0.jwt.JWT.decode(tokenString); + + if (jwt.getAlgorithm().equals("RS256")) { + Jwk jwk = getJwkProvider(pJwksUri).get(jwt.getKeyId()); + RSAPublicKey signingPubKey = (RSAPublicKey) jwk.getPublicKey(); + + if (signingPubKey == null) { + throw new NotAuthorizedException("Problematic public key (null)"); + } + + jwt = com.auth0.jwt.JWT + .require(Algorithm.RSA256(signingPubKey, null)) + .build() + .verify(tokenString); + + } else if (jwt.getAlgorithm().equals("HS256")) { jwt = com.auth0.jwt.JWT.require(Algorithm - .HMAC256(Base64.decodeBase64(clientSecret - .getBytes("UTF-8")))) + .HMAC256(clientSecret + .getBytes("UTF-8"))) .build() .verify(tokenString); - } catch (UnsupportedEncodingException ex){ + } else { + throw new NotAuthorizedException("Problematic signature algorithm = " + jwt.getAlgorithm()); + } + + } catch (UnsupportedEncodingException | MalformedURLException | JwkException e){ + logger.error("extractEmailFromJWT() error decoding token: " + e.getMessage()); + throw new NotAuthorizedException("Token is invalid, please request a new one"); + } catch (JWTVerificationException e) { + try{ + if (jwt != null && jwt.getAlgorithm().equals("HS256")) { + jwt = com.auth0.jwt.JWT.require(Algorithm + .HMAC256(Base64.decodeBase64(clientSecret + .getBytes("UTF-8")))) + .build() + .verify(tokenString); + } + } catch (UnsupportedEncodingException | JWTVerificationException ex){ logger.error("extractEmailFromJWT() getting bytes for initialize jwt token algorithm error: " + e.getMessage()); throw new NotAuthorizedException("Token is invalid, please request a new one"); - } catch (JWTVerificationException ex) { - logger.error("extractEmailFromJWT() token is invalid after tried with another algorithm: " + e.getMessage()); - throw new NotAuthorizedException("Token is invalid, please request a new one"); } } diff --git a/IRCT-CL/src/main/resources/wildfly-configuration/standalone.xml b/IRCT-CL/src/main/resources/wildfly-configuration/standalone.xml index 2ac3b97d..e1535d48 100644 --- a/IRCT-CL/src/main/resources/wildfly-configuration/standalone.xml +++ b/IRCT-CL/src/main/resources/wildfly-configuration/standalone.xml @@ -366,6 +366,10 @@ + + + + diff --git a/IRCT-RI/src/main/java/edu/harvard/hms/dbmi/bd2k/irct/ri/i2b2/I2B2XMLResourceImplementation.java b/IRCT-RI/src/main/java/edu/harvard/hms/dbmi/bd2k/irct/ri/i2b2/I2B2XMLResourceImplementation.java index 50e8af30..81c91b46 100644 --- a/IRCT-RI/src/main/java/edu/harvard/hms/dbmi/bd2k/irct/ri/i2b2/I2B2XMLResourceImplementation.java +++ b/IRCT-RI/src/main/java/edu/harvard/hms/dbmi/bd2k/irct/ri/i2b2/I2B2XMLResourceImplementation.java @@ -68,6 +68,7 @@ import org.apache.log4j.Logger; import org.w3c.dom.Element; import org.w3c.dom.Node; +import org.w3c.dom.NodeList; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; @@ -108,6 +109,8 @@ public class I2B2XMLResourceImplementation protected boolean returnFullSet = true; protected List sourceWhiteList; + protected boolean useJWT; + protected ResourceState resourceState; @Override @@ -146,10 +149,13 @@ public void setup(Map parameters) throws ResourceInterfaceExcept String certificateString = parameters.get("ignoreCertificate"); logger.debug("certificateString:" + (certificateString != null ? certificateString : "NULL")); + this.useJWT = Boolean.valueOf(parameters.get("useJWT")); + logger.debug("useJWT is " + useJWT); + if (this.proxyURL == null) { this.useProxy = false; - this.userName = parameters.get("username"); - this.password = parameters.get("password"); + this.userName = this.useJWT ? "" : parameters.get("username"); + this.password = this.useJWT ? "" : parameters.get("password"); logger.debug("setup() Since no proxyURL has been specified. using username/password [" + this.userName + "/" + this.password + "]"); } else { @@ -200,7 +206,7 @@ public List getPathRelationship(Entity path, OntologyRelationship relati // If first then get projects if (pathComponents.length == 2) { logger.debug("getPathRelationship() creating PMCell."); - pmCell = createPMCell(); + pmCell = createPMCell(user.getName(), user.getToken()); ConfigureType configureType = pmCell.getUserConfiguration(client, null, new String[] { "undefined" }); @@ -220,7 +226,7 @@ public List getPathRelationship(Entity path, OntologyRelationship relati } } else { - ontCell = createOntCell(pathComponents[2]); + ontCell = createOntCell(pathComponents[2], user.getName(), user.getToken()); ConceptsType conceptsType = null; if (pathComponents.length == 3) { // If beyond second then get ontology categories @@ -251,7 +257,7 @@ public List getPathRelationship(Entity path, OntologyRelationship relati if (resourcePath.lastIndexOf('\\') != resourcePath.length() - 1) { resourcePath += '\\'; } - ontCell = createOntCell(pathComponents[2]); + ontCell = createOntCell(pathComponents[2], user.getName(), user.getToken()); ModifiersType modifiersType = ontCell.getModifiers(client, false, false, null, -1, resourcePath, false, null); entities = convertModifiersTypeToEntities(basePath, modifiersType); @@ -265,7 +271,7 @@ public List getPathRelationship(Entity path, OntologyRelationship relati if (resourcePath.lastIndexOf('\\') != resourcePath.length() - 1) { resourcePath += '\\'; } - ontCell = createOntCell(pathComponents[2]); + ontCell = createOntCell(pathComponents[2], user.getName(), user.getToken()); ConceptsType conceptsType = null; @@ -344,30 +350,30 @@ public List searchPaths(Entity path, String searchTerm, String strategy, try { if ((path == null) || (path.getPui().split("/").length <= 2)) { - pmCell = createPMCell(); + pmCell = createPMCell(user.getName(), user.getToken()); ConfigureType configureType = pmCell.getUserConfiguration(client, null, new String[] { "undefined" }); for (ProjectType pt : configureType.getUser().getProject()) { - for (ConceptType category : getCategories(client, pt.getId()).getConcept()) { + for (ConceptType category : getCategories(client, pt.getId(), user).getConcept()) { String categoryName = converti2b2Path(category.getKey()).split("/")[1]; entities.addAll(convertConceptsTypeToEntities("/" + this.resourceName + "/" + pt.getId(), - runNameSearch(client, pt.getId(), categoryName, strategy, searchTerm))); + runNameSearch(client, pt.getId(), categoryName, strategy, searchTerm, user))); } } } else { String[] pathComponents = path.getPui().split("/"); if (pathComponents.length == 3) { // Get All Categories - for (ConceptType category : getCategories(client, pathComponents[2]).getConcept()) { + for (ConceptType category : getCategories(client, pathComponents[2], user).getConcept()) { String categoryName = converti2b2Path(category.getKey()).split("/")[1]; entities.addAll(convertConceptsTypeToEntities("/" + this.resourceName + "/" + pathComponents[2], - runNameSearch(client, pathComponents[2], categoryName, strategy, searchTerm))); + runNameSearch(client, pathComponents[2], categoryName, strategy, searchTerm, user))); } } else { // Run request entities.addAll(convertConceptsTypeToEntities("/" + this.resourceName + "/" + pathComponents[2], - runNameSearch(client, pathComponents[2], pathComponents[3], strategy, searchTerm))); + runNameSearch(client, pathComponents[2], pathComponents[3], strategy, searchTerm, user))); } } } catch (JAXBException | UnsupportedOperationException | I2B2InterfaceException | IOException e) { @@ -383,23 +389,23 @@ public List searchOntology(Entity path, String ontologyType, String onto try { if ((path == null) || (path.getPui().split("/").length <= 2)) { - pmCell = createPMCell(); + pmCell = createPMCell(user.getName(), user.getToken()); ConfigureType configureType = pmCell.getUserConfiguration(client, null, new String[] { "undefined" }); for (ProjectType pt : configureType.getUser().getProject()) { entities.addAll(convertConceptsTypeToEntities("/" + this.resourceName + "/" + pt.getId(), - runCategorySearch(client, pt.getId(), null, ontologyType, ontologyTerm))); + runCategorySearch(client, pt.getId(), null, ontologyType, ontologyTerm, user))); } } else { String[] pathComponents = path.getPui().split("/"); if (pathComponents.length == 3) { // Get All Categories entities.addAll(convertConceptsTypeToEntities("/" + this.resourceName + "/" + pathComponents[2], - runCategorySearch(client, pathComponents[2], null, ontologyType, ontologyTerm))); + runCategorySearch(client, pathComponents[2], null, ontologyType, ontologyTerm, user))); } else { // Run request entities.addAll(convertConceptsTypeToEntities("/" + this.resourceName + "/" + pathComponents[2], runCategorySearch(client, pathComponents[2], pathComponents[3], ontologyType, - ontologyTerm))); + ontologyTerm, user))); } } } catch (JAXBException | UnsupportedOperationException | I2B2InterfaceException | IOException e) { @@ -536,7 +542,7 @@ public Result i2b2XMLRIRunQuery_runRequest(User user, Query query, Result result } try { - crcCell = createCRCCell(projectId, user.getName()); + crcCell = createCRCCell(projectId, user.getName(), user.getToken()); MasterInstanceResultResponseType mirrt = crcCell.runQueryInstanceFromQueryDefinition(client, null, null, "IRCT", null, "ANY", 0, roolt, panels.toArray(new PanelType[panels.size()])); @@ -588,6 +594,7 @@ public Result i2b2XMLRI_getResults(User user, Result result){ HttpClient client = createClient(user); String resultInstanceId = result.getResourceActionId(); + String projectId = resultInstanceId.split("\\|")[0]; String resultId = resultInstanceId.split("\\|")[2]; // Get PDO List @@ -600,6 +607,7 @@ public Result i2b2XMLRI_getResults(User user, Result result){ int min = 0; + crcCell = createCRCCell(projectId, user.getName(), user.getToken()); while( pdrt == null){ if (result.getMetaData().containsKey("aliasMap")) @@ -666,7 +674,7 @@ protected Result checkForResult(User user, Result result) { try { logger.debug("checkForResult() creating `CRCCell`"); - CRCCell crcCell = createCRCCell(projectId, user.getName()); + CRCCell crcCell = createCRCCell(projectId, user.getName(), user.getToken()); // Is Query Master List Complete? InstanceResponseType instanceResponse = crcCell.getQueryInstanceListFromQueryId(client, queryId); @@ -1329,55 +1337,55 @@ protected String converti2b2Path(String i2b2Path) throws UnsupportedEncodingExce } private ConceptsType runNameSearch(HttpClient client, String projectId, String category, String strategy, - String searchTerm) + String searchTerm, User user) throws UnsupportedOperationException, JAXBException, I2B2InterfaceException, IOException { - ONTCell ontCell = createOntCell(projectId); + ONTCell ontCell = createOntCell(projectId, user.getName(), user.getToken()); return ontCell.getNameInfo(client, true, category, false, strategy, searchTerm, -1, null, true, "core"); } - private ConceptsType getCategories(HttpClient client, String projectId) + private ConceptsType getCategories(HttpClient client, String projectId, User user) throws JAXBException, ClientProtocolException, IOException, I2B2InterfaceException { - ONTCell ontCell = createOntCell(projectId); + ONTCell ontCell = createOntCell(projectId, user.getName(), user.getToken()); return ontCell.getCategories(client, false, false, true, "core"); } private ConceptsType runCategorySearch(HttpClient client, String projectId, String category, String ontologyType, - String ontologyTerm) + String ontologyTerm, User user) throws UnsupportedOperationException, JAXBException, I2B2InterfaceException, IOException { - ONTCell ontCell = createOntCell(projectId); + ONTCell ontCell = createOntCell(projectId, user.getName(), user.getToken()); return ontCell.getCodeInfo(client, true, category, false, "exact", ontologyType + ":" + ontologyTerm, -1, null, true, "core"); } - private CRCCell createCRCCell(String projectId, String userName) throws JAXBException { + protected CRCCell createCRCCell(String projectId, String userName, String jwt) throws JAXBException { if (this.useProxy) { crcCell.setupConnection(this.resourceURL, this.domain, userName, "", projectId, this.useProxy, this.proxyURL + "/QueryToolService"); } else { - crcCell.setupConnection(this.resourceURL + "QueryToolService/", this.domain, this.userName, this.password, + crcCell.setupConnection(this.resourceURL + "QueryToolService/", this.domain, this.useJWT ? userName : this.userName, this.useJWT ? jwt : this.password, projectId, false, null); } return crcCell; } - private ONTCell createOntCell(String projectId) throws JAXBException { + protected ONTCell createOntCell(String projectId, String userName, String jwt) throws JAXBException { if (this.useProxy) { ontCell.setupConnection(this.resourceURL, this.domain, "", "", projectId, this.useProxy, this.proxyURL + "/OntologyService"); } else { - ontCell.setupConnection(this.resourceURL + "OntologyService/", this.domain, this.userName, this.password, + ontCell.setupConnection(this.resourceURL + "OntologyService/", this.domain, this.useJWT ? userName : this.userName, this.useJWT ? jwt : this.password, projectId, false, null); } return ontCell; } - private PMCell createPMCell() throws JAXBException { + protected PMCell createPMCell(String userName, String jwt) throws JAXBException { if (this.useProxy) { pmCell.setupConnection(this.resourceURL, this.domain, "", "", "", this.useProxy, this.proxyURL + "/PMService"); } else { - pmCell.setupConnection(this.resourceURL + "PMService/", this.domain, this.userName, this.password, "", + pmCell.setupConnection(this.resourceURL + "PMService/", this.domain, this.useJWT ? userName : this.userName, this.useJWT ? jwt : this.password, "", false, null); } return pmCell; @@ -1418,7 +1426,7 @@ protected HttpClient createClient(User user) { } protected void addAuthenticationHeader(User user, List
defaultHeaders) { - // Do nothing. + defaultHeaders.add(new BasicHeader("Authorization", "Bearer " + user.getToken())); } private HttpClientBuilder ignoreCertificate() throws NoSuchAlgorithmException, KeyManagementException {