Skip to content

Commit

Permalink
#167 Forward Authorization tokens that aren't SAML assertions
Browse files Browse the repository at this point in the history
  • Loading branch information
qligier committed Aug 30, 2024
1 parent a4c599f commit 13ab1bd
Show file tree
Hide file tree
Showing 10 changed files with 141 additions and 132 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,30 +17,14 @@
package ch.bfh.ti.i4mi.mag.mhd.iti65;

import static org.openehealth.ipf.platform.camel.ihe.fhir.core.FhirCamelTranslators.translateToFhir;
import static org.openehealth.ipf.platform.camel.ihe.fhir.core.FhirCamelValidators.itiRequestValidator;
import static org.openehealth.ipf.platform.camel.ihe.xds.XdsCamelValidators.iti41RequestValidator;
import java.util.Date;
import java.util.UUID;

import javax.xml.soap.SOAPException;

import org.apache.camel.Exchange;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.support.ExpressionAdapter;
import org.hl7.fhir.r4.model.Binary;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.DocumentManifest;
import org.hl7.fhir.r4.model.DocumentReference;
import org.hl7.fhir.r4.model.ListResource;
import org.hl7.fhir.r4.model.Resource;
import org.openehealth.ipf.commons.ihe.xds.core.ebxml.ebxml30.ProvideAndRegisterDocumentSetRequestType;
import org.openehealth.ipf.commons.ihe.xds.core.responses.QueryResponse;
import org.openehealth.ipf.commons.ihe.xds.core.responses.Response;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;

import ch.bfh.ti.i4mi.mag.Config;
import ch.bfh.ti.i4mi.mag.mhd.BaseResponseConverter;
import ch.bfh.ti.i4mi.mag.mhd.Utils;
import ch.bfh.ti.i4mi.mag.xua.AuthTokenConverter;
import lombok.extern.slf4j.Slf4j;
Expand Down Expand Up @@ -81,7 +65,7 @@ public void configure() throws Exception {
// pass back errors to the endpoint
.errorHandler(noErrorHandler())
//.process(itiRequestValidator())
.process(AuthTokenConverter.addWsHeader())
.process(AuthTokenConverter.forwardAuthToken())
// translate, forward, translate back
.process(Utils.keepBody())
.process(Utils.storeBodyToHeader("BundleRequest"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ public void configure() throws Exception {
from("mhd-iti66-v401:translation?audit=true&auditContext=#myAuditContext").routeId("mdh-documentmanifest-adapter")
// pass back errors to the endpoint
.errorHandler(noErrorHandler())
.process(AuthTokenConverter.addWsHeader()).choice()
.process(AuthTokenConverter.forwardAuthToken()).choice()
.when(header(Constants.FHIR_REQUEST_PARAMETERS).isNotNull())
.bean(Utils.class,"searchParameterToBody")
.bean(Iti66RequestConverter.class).endChoice()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ public void configure() throws Exception {
from("mhd-iti67-v401:translation?audit=true&auditContext=#myAuditContext").routeId("mdh-documentreference-adapter")
// pass back errors to the endpoint
.errorHandler(noErrorHandler())
.process(AuthTokenConverter.addWsHeader())
.process(AuthTokenConverter.forwardAuthToken())
.choice()
.when(header(Constants.FHIR_REQUEST_PARAMETERS).isNotNull())
.bean(Utils.class,"searchParameterToBody")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
import org.springframework.stereotype.Component;

import ch.bfh.ti.i4mi.mag.Config;
import ch.bfh.ti.i4mi.mag.mhd.Utils;
import ch.bfh.ti.i4mi.mag.xua.AuthTokenConverter;
import lombok.extern.slf4j.Slf4j;

Expand Down Expand Up @@ -62,7 +61,7 @@ public void configure() throws Exception {
from("mhd-iti68:camel/xdsretrieve?audit=true&auditContext=#myAuditContext").routeId("ddh-retrievedoc-adapter")
// pass back errors to the endpoint
.errorHandler(noErrorHandler())
.process(AuthTokenConverter.addWsHeader())
.process(AuthTokenConverter.forwardAuthToken())

// translate, forward, translate back
.bean(Iti68RequestConverter.class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public void configure() throws Exception {
from("mhd-pharm5:translation?audit=true&auditContext=#myAuditContext").routeId("mdh-documentreference-findmedicationlist-adapter")
// pass back errors to the endpoint
.errorHandler(noErrorHandler())
.process(AuthTokenConverter.addWsHeader())
.process(AuthTokenConverter.forwardAuthToken())
.bean(Pharm5RequestConverter.class)
.to(endpoint)
.process(translateToFhir(new Iti67ResponseConverter(config) , QueryResponse.class));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,11 @@
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;

import ca.uhn.fhir.rest.api.MethodOutcome;
import ch.bfh.ti.i4mi.mag.Config;
import ch.bfh.ti.i4mi.mag.mhd.BaseResponseConverter;
import ch.bfh.ti.i4mi.mag.mhd.Utils;
import ch.bfh.ti.i4mi.mag.pmir.iti78.Iti78RequestConverter;
import ch.bfh.ti.i4mi.mag.pmir.iti78.Iti78ResponseConverter;
import ch.bfh.ti.i4mi.mag.pmir.iti83.Iti83ResponseConverter;
import ch.bfh.ti.i4mi.mag.xua.AuthTokenConverter;
import lombok.extern.slf4j.Slf4j;

Expand Down Expand Up @@ -88,7 +86,7 @@ public void configure() throws Exception {
from("pmir-iti104:stub?audit=true&auditContext=#myAuditContext").routeId("iti104-feed")
// pass back errors to the endpoint
.errorHandler(noErrorHandler())
.process(AuthTokenConverter.addWsHeader())
.process(AuthTokenConverter.forwardAuthToken())
.process(Utils.keepBody())
.bean(Iti104RequestConverter.class)
.doTry()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public void configure() throws Exception {
from("pdqm-iti78:translation?audit=true&auditContext=#myAuditContext").routeId("pdqm-adapter")
// pass back errors to the endpoint
.errorHandler(noErrorHandler())
.process(AuthTokenConverter.addWsHeader())
.process(AuthTokenConverter.forwardAuthToken())
.choice()
.when(header(Constants.FHIR_REQUEST_PARAMETERS).isNotNull())
.bean(Iti78RequestConverter.class, "iti78ToIti47Converter")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public void configure() throws Exception {
from("pixm-iti83:translation?audit=true&auditContext=#myAuditContext").routeId("pixm-adapter")
// pass back errors to the endpoint
.errorHandler(noErrorHandler())
.process(AuthTokenConverter.addWsHeader())
.process(AuthTokenConverter.forwardAuthToken())
.process(Utils.keepBody())
.bean(Iti83RequestConverter.class)
.doTry()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ public void configure() throws Exception {
from("pmir-iti93:stub?audit=true&auditContext=#myAuditContext").routeId("pmir-feed")
// pass back errors to the endpoint
.errorHandler(noErrorHandler())
.process(AuthTokenConverter.addWsHeader())
.process(AuthTokenConverter.forwardAuthToken())
.process(Utils.keepBody())
.bean(Iti93RequestConverter.class)
.doTry()
Expand Down
236 changes: 132 additions & 104 deletions src/main/java/ch/bfh/ti/i4mi/mag/xua/AuthTokenConverter.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,128 +16,156 @@

package ch.bfh.ti.i4mi.mag.xua;

import static ch.bfh.ti.i4mi.mag.xua.XuaUtils.OASIS_WSSECURITY_NS;
import static org.openehealth.ipf.platform.camel.ihe.ws.AbstractWsEndpoint.OUTGOING_SOAP_HEADERS;
import static org.opensaml.common.xml.SAMLConstants.SAML20_NS;

import java.io.StringReader;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Map;

import javax.xml.namespace.QName;

import org.apache.camel.Processor;
import org.apache.camel.util.CastUtils;
import org.apache.cxf.binding.soap.SoapHeader;
import org.apache.cxf.headers.Header;
import org.apache.cxf.headers.Header.Direction;
import org.apache.cxf.staxutils.StaxUtils;
import org.openehealth.ipf.commons.ihe.fhir.Constants;
import org.openehealth.ipf.platform.camel.ihe.ws.AbstractWsEndpoint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import javax.xml.namespace.QName;
import java.io.StringReader;
import java.util.*;

import static ch.bfh.ti.i4mi.mag.xua.XuaUtils.OASIS_WSSECURITY_NS;
import static org.openehealth.ipf.platform.camel.ihe.ws.AbstractWsEndpoint.OUTGOING_SOAP_HEADERS;
import static org.opensaml.common.xml.SAMLConstants.SAML20_NS;


/**
* Use IHE-SAML Header from request as SOAP wsse Security Header
* @author alexander kreutz
*
* @author alexander kreutz
*/
public class AuthTokenConverter {

private static final String AUTHORIZATION_HEADER = "Authorization";

/**
* This class is not instantiable.
*/
private AuthTokenConverter() {
}

public static String convert(final String token) {
final String base64Token;
if (token != null && token.startsWith("IHE-SAML ")) {
base64Token = token.substring("IHE-SAML ".length());
} else if (token != null && token.startsWith("Bearer ")) {
base64Token = token.substring("Bearer ".length());
} else {
return null;
}
return new String(Base64.getDecoder().decode(base64Token));
}

private static String getNodeValue(final Element in,
final String ns,
final String element) {
final NodeList lst = in.getElementsByTagNameNS(ns, element);
if (lst.getLength() == 0) {
return "";
}
return lst.item(0).getTextContent();

}

private static String getAttrValue(final Element in,
final String ns,
final String element,
final String attribute) {
final NodeList lst = in.getElementsByTagNameNS(ns, element);
if (lst.getLength() == 0) {
return "";
}
final Node attr = lst.item(0).getAttributes().getNamedItem(attribute);
return attr != null ? attr.getTextContent() : "";
}

public static Processor addWsHeader() {
return exchange -> {

Map<String, List<String>> httpHeaders =
CastUtils.cast((Map<?, ?>) exchange.getMessage().getHeader("FhirHttpHeaders"));

String converted = null;

if (httpHeaders != null) {
final List<String> header = httpHeaders.get(AUTHORIZATION_HEADER);
if (header != null) {
converted = convert(header.get(0));
httpHeaders.remove(AUTHORIZATION_HEADER);
}
} else {
final Object authHeader = exchange.getMessage().getHeader(AUTHORIZATION_HEADER);
if (authHeader != null) {
exchange.getMessage().removeHeader(AUTHORIZATION_HEADER);
converted = convert(authHeader.toString());
}
}
if (converted != null) {
if (converted.startsWith("<?xml")) {
converted = converted.substring(converted.indexOf(">") + 1);
}
converted = String.format("<wsse:Security xmlns:wsse=\"%s\">%s</wsse:Security>", OASIS_WSSECURITY_NS, converted);

List<SoapHeader> soapHeaders =
CastUtils.cast((List<?>) exchange.getIn().getHeader(Header.HEADER_LIST));

if (soapHeaders == null) {
soapHeaders = new ArrayList<>(1);
}
private static final Logger log = LoggerFactory.getLogger(AuthTokenConverter.class);

private static final String AUTHORIZATION_HEADER = "Authorization";

/**
* This class is not instantiable.
*/
private AuthTokenConverter() {
}

private static String getNodeValue(final Element in,
final String ns,
final String element) {
final NodeList lst = in.getElementsByTagNameNS(ns, element);
if (lst.getLength() == 0) {
return "";
}
return lst.item(0).getTextContent();

}

private static String getAttrValue(final Element in,
final String ns,
final String element,
final String attribute) {
final NodeList lst = in.getElementsByTagNameNS(ns, element);
if (lst.getLength() == 0) {
return "";
}
final Node attr = lst.item(0).getAttributes().getNamedItem(attribute);
return attr != null ? attr.getTextContent() : "";
}

/**
* Forwards the Authorization header to the next hop.
* <p>
* If the Authorization header contains a base64-encoded SAML assertion, it is decoded and
* a WS-Security header is created from it.
* </p>
* <p>
* If the Authorization header is a JWT or something else, it is forwarded as is.
* </p>
*/
public static Processor forwardAuthToken() {
return exchange -> {
Map<String, List<String>> httpHeaders =
CastUtils.cast((Map<?, ?>) exchange.getMessage().getHeader(Constants.HTTP_INCOMING_HEADERS));

// Find the Authorization header in the HTTP headers
String authorizationHeader = null;
if (httpHeaders != null) {
final List<String> header = httpHeaders.get(AUTHORIZATION_HEADER);
if (header != null) {
authorizationHeader = header.get(0);
httpHeaders.remove(AUTHORIZATION_HEADER);
}
} else {
final Object authHeader = exchange.getMessage().getHeader(AUTHORIZATION_HEADER);
if (authHeader != null) {
authorizationHeader = authHeader.toString();
exchange.getMessage().removeHeader(AUTHORIZATION_HEADER);
}
}

if (authorizationHeader == null) {
return;
}

// Extract the payload from the Authorization header
final String payload;
if (authorizationHeader.startsWith("Bearer ")) {
payload = authorizationHeader.substring("Bearer ".length());
} else if (authorizationHeader.startsWith("IHE-SAML ")) {
payload = authorizationHeader.substring("IHE-SAML ".length());
} else {
return;
}

if (payload.startsWith("PHNhbWwyOkFzc2") || payload.startsWith("PD94bW")) {
// It is an encoded SAML assertion, convert it to a WS-Security header
log.debug("Converting encoded SAML assertion to WS-Security header");
String converted = new String(Base64.getDecoder().decode(payload));
if (converted.startsWith("<?xml")) {
converted = converted.substring(converted.indexOf(">") + 1);
}
converted = String.format("<wsse:Security xmlns:wsse=\"%s\">%s</wsse:Security>",
OASIS_WSSECURITY_NS,
converted);

List<SoapHeader> soapHeaders =
CastUtils.cast((List<?>) exchange.getIn().getHeader(Header.HEADER_LIST));

if (soapHeaders == null) {
soapHeaders = new ArrayList<>(1);
}
final Element headerDocument = StaxUtils.read(new StringReader(converted)).getDocumentElement();

String alias = getAttrValue(headerDocument, SAML20_NS, "NameID", "SPProvidedID");
String user = getNodeValue(headerDocument, SAML20_NS, "NameID");
String issuer = getNodeValue(headerDocument, SAML20_NS, "Issuer");

String userName = alias+"<"+user+"@"+issuer+">";

final var newHeader = new SoapHeader(new QName(OASIS_WSSECURITY_NS, "Security"), headerDocument);
newHeader.setDirection(Direction.DIRECTION_OUT);

soapHeaders.add(newHeader);

exchange.getMessage().setHeader(OUTGOING_SOAP_HEADERS, soapHeaders);
exchange.setProperty("UserName", userName);
}
};
}

String userName = alias + "<" + user + "@" + issuer + ">";

final var newHeader = new SoapHeader(new QName(OASIS_WSSECURITY_NS, "Security"), headerDocument);
newHeader.setDirection(Direction.DIRECTION_OUT);

soapHeaders.add(newHeader);

exchange.getMessage().setHeader(OUTGOING_SOAP_HEADERS, soapHeaders);
exchange.setProperty("UserName", userName);
} else {
// It is a JWT or something else, just forward it
log.debug("Forwarding Authorization header: {}", authorizationHeader);
final Map<String, String> outgoingHttpHeaders =
CastUtils.cast(exchange.getMessage().getHeader(AbstractWsEndpoint.OUTGOING_HTTP_HEADERS,
HashMap::new, Map.class));

outgoingHttpHeaders.put(AUTHORIZATION_HEADER, authorizationHeader);


}
};
}
}

0 comments on commit 13ab1bd

Please sign in to comment.