Question
· May 22, 2023

Unlocking the Secrets: How to Extract and Send SAML Assertion in SOAP Web Services for Seamless Integration

Good afternoon,

first thank you from heart and mind, mind and heart; for reading, thinking, reflecting, responding, and above all explaining a possible solution and/or documentation to address this doubt.

We would need a way to get inside a SOAP Web Service the SAML Assertion, and then, send it directly to the endpoint throught a SOAP Operation.

Currently we have researched and developed how to get the SAML Assertion with the following code:

Class Servicios.RESNS.ConsultaRecetas Extends EnsLib.SOAP.Service [ ProcedureBlock ]
{

/// This is the namespace used by the Service
Parameter NAMESPACE = "urn:ihe:iti:xds-b:2007";
/// Use xsi:type attribute for literal types.
Parameter OUTPUTTYPEATTRIBUTE = 0;
/// Determines handling of Security header.
Parameter SECURITYIN = "IGNORE";
/// This is the name of the Service
Parameter SERVICENAME = "RespondingGateway_Service";
/// This is the SOAP version supported by the service.
Parameter SOAPVERSION = 1.2;
/// Namespaces of referenced classes will be used in the WSDL.
Parameter USECLASSNAMESPACES = 1;
Property sns As %String;
Parameter SETTINGS = "sns";
/// Default URL for invoking the WebService.
/// The URL may be absolute or relative to the WSDL request URL..
Method RespondingGatewayCrossGatewayQuery(RequestSlotList As EsquemasDatos.RESNS.v02r00.rim.SlotListType, id As %xsd.anyURI(XMLPROJECTION="attribute"), comment As %String(XMLPROJECTION="attribute"), ResponseOption As EsquemasDatos.RESNS.v02r00.query.ResponseOptionType(REFELEMENTQUALIFIED=1,REFNAMESPACE="urn:oasis:names:tc:ebxml-regrep:xsd:query:3.0",XMLREF=1), AdhocQuery As EsquemasDatos.RESNS.v02r00.rim.AdhocQueryType(REFELEMENTQUALIFIED=1,REFNAMESPACE="urn:oasis:names:tc:ebxml-regrep:xsd:rim:3.0",XMLREF=1), federated As %Boolean(XMLPROJECTION="attribute"), federation As %xsd.anyURI(XMLPROJECTION="attribute"), ByRef startIndex As %Integer(XMLPROJECTION="attribute"), maxResults As %Integer(XMLPROJECTION="attribute"), Output ResponseSlotList As EsquemasDatos.RESNS.rim.SlotListType, Output RegistryErrorList As EsquemasDatos.RESNS.rs.RegistryErrorList(REFELEMENTQUALIFIED=1,REFNAMESPACE="urn:oasis:names:tc:ebxml-regrep:xsd:rs:3.0",XMLREF=1), Output status As EsquemasDatos.RESNS.rim.referenceURI(XMLPROJECTION="attribute"), Output requestId As %xsd.anyURI(XMLPROJECTION="attribute"), Output RegistryObjectList As EsquemasDatos.RESNS.rim.RegistryObjectListType(REFELEMENTQUALIFIED=1,REFNAMESPACE="urn:oasis:names:tc:ebxml-regrep:xsd:rim:3.0",XMLREF=1), Output totalResultCount As %Integer(XMLPROJECTION="attribute")) [ Final, ProcedureBlock = 1, SoapAction = "urn:ihe:iti:2007:CrossGatewayQuery", SoapBindingStyle = document, SoapBodyUse = literal, SoapMessageName = AdhocQueryResponse, SoapTypeNameSpace = "urn:oasis:names:tc:ebxml-regrep:xsd:query:3.0", WebMethod ]
{
 // 22 05 2023 Obtener cabecera SAML enviada por Ministerio RESNS
 #dim SAML AS %SAML.Assertion
 Set SAML = ..SecurityIn.FindElement("Assertion") 
 $$$LOGALERT("SAML.%ClassName(): "_SAML.%ClassName())
 
 Set IssuerNameID = SAML.Issuer.NameID
 $$$LOGALERT("IssuerNameID: "_IssuerNameID)
 
 set ..MTOMRequired = 0
 set pRequest = ##class(Mensajes.Request.RESNS.RespondingGatewayCrossGatewayQueryRequest).%New()
 // 22 05 2023 Asignar cabecera SAML enviada por Ministerio RESNS
 set pRequest.CabeceraSAML = SAML
 
 $$$LOGWARNING("pRequest.CabeceraSAML.%ClassName(): "_pRequest.CabeceraSAML.%ClassName())
 $$$LOGWARNING("pRequest.CabeceraSAML.Issuer.NameID: "_pRequest.CabeceraSAML.Issuer.NameID)
 
 [...]
 }
 }

 

Where the important line is:

Set SAML = ..SecurityIn.FindElement("Assertion")

 

With the LOGS we observe in the Service's log the following being printed:

Warning 2023-05-22 13:40:46.084 pRequest.CabeceraSAML.Issuer.NameID: urn:initgw:countryB  92705904
Warning 2023-05-22 13:40:46.084 pRequest.CabeceraSAML.%ClassName(): Assertion  92705903
Alert 2023-05-22 13:40:46.083 IssuerNameID: urn:initgw:countryB  92705902
Alert 2023-05-22 13:40:46.083 SAML.%ClassName(): Assertion  92705901

 

We have added a Property to the Request which is being dispatched from Service to Process, being the code as follows:

Class Mensajes.Request.RESNS.RespondingGatewayCrossGatewayQueryRequest Extends Ens.Request [ ProcedureBlock ]
{

Parameter RESPONSECLASSNAME = "Mensajes.Response.RESNS.RespondingGatewayCrossGatewayQueryResponse";
Property RequestSlotList As EsquemasDatos.RESNS.v02r00.rim.SlotListType;
[... other properties ...]

/// 22 05 2023 Añadimos propiedad para guardar la SAML que nos remite Sistema origen MINISTERIO
Property CabeceraSAML As %SAML.Assertion;
}

 

Where the relevant line is this one:

Property CabeceraSAML As %SAML.Assertion;

 

However we do have an interesting issue here, when we try to print "CabeceraSAML" into the Bussiness Process, it does not show.

To be precise, we have added a <code> block as follows:

Where we have written:

   Set IssuerNameID = request.CabeceraSAML.Issuer.NameID
 $$$LOGALERT("IssuerNameID: "_IssuerNameID)
 
 $$$LOGWARNING("request.CabeceraSAML.%ClassName(): "_request.CabeceraSAML.%ClassName())

 

So then we would expect to visualize in the LOGS the values which have been previously stored in the Service. However, the fact turns out to be that what we see in the Process LOG is:

  IssuerNameID:

Which means that somehow request.HeaderSAML.Issuer.NameID is empty.

Even more strange is that in step [3] we read in the error being prompted:

  ERROR <Ens>ErrException: <INVALID OREF>zS1+5^Procesos.RESNS.EnrutadorRESNS.Thread1.1 -- - registrado como '-' número - @' Do ##class(Ens.Util.Log).LogWarning($classname(),"S1","request.CabeceraSAML.%ClassName(): "_request.CabeceraSAML.%ClassName())'

 

···> 💻👩‍💻👨‍💻💡🔧📚🚀 We do not understand this behaviour.

 

 

 

 

 

 

What is the way you recommend from Intersystems to get a SAML Header in a SOAP Service and communicate it directly to the Target System?

 

Specifically, once we have developed to obtain a SAML header in the SOAP Service, what mechanism, way, alternative, option, development, do you recommend, to send a SAML Assertion through the Process and Operation and the WSCLIENT to the Target System?

What is the simplest, correct, stable and recommended way to send the SAML header to the Target System?

 

What is the most correct, stable simple and secure way to get a SAML header in the SOAP Service (which has already been developed) and send it raw, i.e. as is, to the Target System?

 

Again I insist; thank you very much for taking the time to read, think, reflect, answer and teach us the right way to solve this use case. Thanks because time is the most valuable resource.

Thanks for your replies.

Thanks.

Greetings.

Product version: IRIS 2020.1
$ZV: IRIS for UNIX (Red Hat Enterprise Linux for x86-64) 2020.1.1 (Build 408U) Sun Mar 21 2021 22:21:14 EDT
Discussion (1)1
Log in or sign up to continue

We would like to share our approach to try to solve this task:

First, we add the following lines inside the SOAP Service's method:

 #dim SAML AS %SAML.Assertion
 Set SAML = ..SecurityIn.FindElement("Assertion")  set writer=##class(%XML.Writer).%New()


 set status=writer.OutputToString()
 If $$$ISERR(status)  Do $system.OBJ.DisplayError(status)
 set status=writer.RootObject(SAML)
 If $$$ISERR(status) Do $system.OBJ.DisplayError(status)  set samlString = writer.GetXMLString()
 $$$LOGASSERT("samlString: "_samlString)  

set pRequest = ##class(Mensajes.Request.RESNS.RespondingGatewayCrossGatewayQueryRequest).%New()  set pRequest.CabeceraSAML = samlString
 

Being the current code as follows:

Class Servicios.RESNS.ConsultaRecetas Extends EnsLib.SOAP.Service [ ProcedureBlock ]
{

/// This is the namespace used by the Service
Parameter NAMESPACE = "urn:ihe:iti:xds-b:2007";
/// Use xsi:type attribute for literal types.
Parameter OUTPUTTYPEATTRIBUTE = 0;
/// Determines handling of Security header.
Parameter SECURITYIN = "IGNORE";
/// This is the name of the Service
Parameter SERVICENAME = "RespondingGateway_Service";
/// This is the SOAP version supported by the service.
Parameter SOAPVERSION = 1.2;
/// Namespaces of referenced classes will be used in the WSDL.
Parameter USECLASSNAMESPACES = 1;
Property sns As %String;
Parameter SETTINGS = "sns";
/// Default URL for invoking the WebService.
/// The URL may be absolute or relative to the WSDL request URL..
Method RespondingGatewayCrossGatewayQuery(RequestSlotList As EsquemasDatos.RESNS.v02r00.rim.SlotListType, id As %xsd.anyURI(XMLPROJECTION="attribute"), comment As %String(XMLPROJECTION="attribute"), ResponseOption As EsquemasDatos.RESNS.v02r00.query.ResponseOptionType(REFELEMENTQUALIFIED=1,REFNAMESPACE="urn:oasis:names:tc:ebxml-regrep:xsd:query:3.0",XMLREF=1), AdhocQuery As EsquemasDatos.RESNS.v02r00.rim.AdhocQueryType(REFELEMENTQUALIFIED=1,REFNAMESPACE="urn:oasis:names:tc:ebxml-regrep:xsd:rim:3.0",XMLREF=1), federated As %Boolean(XMLPROJECTION="attribute"), federation As %xsd.anyURI(XMLPROJECTION="attribute"), ByRef startIndex As %Integer(XMLPROJECTION="attribute"), maxResults As %Integer(XMLPROJECTION="attribute"), Output ResponseSlotList As EsquemasDatos.RESNS.rim.SlotListType, Output RegistryErrorList As EsquemasDatos.RESNS.rs.RegistryErrorList(REFELEMENTQUALIFIED=1,REFNAMESPACE="urn:oasis:names:tc:ebxml-regrep:xsd:rs:3.0",XMLREF=1), Output status As EsquemasDatos.RESNS.rim.referenceURI(XMLPROJECTION="attribute"), Output requestId As %xsd.anyURI(XMLPROJECTION="attribute"), Output RegistryObjectList As EsquemasDatos.RESNS.rim.RegistryObjectListType(REFELEMENTQUALIFIED=1,REFNAMESPACE="urn:oasis:names:tc:ebxml-regrep:xsd:rim:3.0",XMLREF=1), Output totalResultCount As %Integer(XMLPROJECTION="attribute")) [ Final, ProcedureBlock = 1, SoapAction = "urn:ihe:iti:2007:CrossGatewayQuery", SoapBindingStyle = document, SoapBodyUse = literal, SoapMessageName = AdhocQueryResponse, SoapTypeNameSpace = "urn:oasis:names:tc:ebxml-regrep:xsd:query:3.0", WebMethod ]
{
 #dim SAML AS %SAML.Assertion
 Set SAML = ..SecurityIn.FindElement("Assertion") 

 set writer=##class(%XML.Writer).%New()
 set status=writer.OutputToString()
 If $$$ISERR(status)

 Do $system.OBJ.DisplayError(status)
 set status=writer.RootObject(SAML)
 If $$$ISERR(status) Do $system.OBJ.DisplayError(status)

 set samlString = writer.GetXMLString()
 $$$LOGASSERT("samlString: "_samlString)

 set pRequest = ##class(Mensajes.Request.RESNS.RespondingGatewayCrossGatewayQueryRequest).%New()

 set pRequest.CabeceraSAML = samlString

 set ..MTOMRequired = 0
 set pRequest.RequestSlotList = RequestSlotList

[... other unrelated method code ...]

}
}


Second we have added the property in the Request:

Class Mensajes.Request.RESNS.RespondingGatewayCrossGatewayQueryRequest Extends Ens.Request [ ProcedureBlock ]
{

Parameter RESPONSECLASSNAME = "Mensajes.Response.RESNS.RespondingGatewayCrossGatewayQueryResponse";
[... other properties ...]

/// 22 05 2023 Añadimos propiedad para guardar la SAML que nos remite Sistema origen MINISTERIO
Property CabeceraSAML As %String(MAXLEN = "");
}

With the previous changes we get the SAML Assertion in the SOAP Service and then send it to the Process as a String.

Third, we need to convert the SAML String to Object in the Web Service Client to send it to nthe Target System as follows:

Include Ensemble

Class WSCLIENTE.RESNS.ConsultaRecetas Extends %SOAP.WebClient [ ProcedureBlock ]
{

/// This is the URL used to access the web service.
/// This is the namespace used by the Service
Parameter NAMESPACE = "urn:ihe:iti:xds-b:2007";
/// Use xsi:type attribute for literal types.
Parameter OUTPUTTYPEATTRIBUTE = 0;
/// Determines handling of Security header.
Parameter SECURITYIN = "ALLOW";
/// This is the name of the Service
Parameter SERVICENAME = "RespondingGateway_Service";
/// This is the SOAP version supported by the service.
Parameter SOAPVERSION = 1.2;
Method RespondingGatewayCrossGatewayQuery(RequestSlotList As EsquemasDatos.RESNS.rim.SlotListType, ResponseOption As EsquemasDatos.RESNS.query.ResponseOptionType, AdhocQuery As EsquemasDatos.RESNS.rim.AdhocQueryType(REFELEMENTQUALIFIED=1,REFNAMESPACE="urn:oasis:names:tc:ebxml-regrep:xsd:rim:3.0",XMLREF=1), federation As %xsd.anyURI(XMLPROJECTION="attribute"), ByRef startIndex As %Integer(XMLPROJECTION="attribute"), maxResults As %Integer(XMLPROJECTION="attribute"), id As %xsd.anyURI(XMLPROJECTION="attribute"), comment As %String(XMLPROJECTION="attribute"), federated As %Boolean(XMLPROJECTION="attribute"), Output ResponseSlotList As EsquemasDatos.RESNS.rim.SlotListType, Output RegistryErrorList As EsquemasDatos.RESNS.rs.RegistryErrorList(REFELEMENTQUALIFIED=1,REFNAMESPACE="urn:oasis:names:tc:ebxml-regrep:xsd:rs:3.0",XMLREF=1), Output RegistryObjectList As EsquemasDatos.RESNS.rim.RegistryObjectListType(REFELEMENTQUALIFIED=1,REFNAMESPACE="urn:oasis:names:tc:ebxml-regrep:xsd:rim:3.0",XMLREF=1), Output requestId As %xsd.anyURI(XMLPROJECTION="attribute"), Output totalResultCount As %Integer(XMLPROJECTION="attribute"), Output status As %xsd.anyURI(XMLPROJECTION="attribute"), CabeceraSAML As %String(MAXLEN="")) [ Final, ProcedureBlock = 1, SoapBindingStyle = document, SoapBodyUse = literal, SoapTypeNameSpace = "urn:oasis:names:tc:ebxml-regrep:xsd:query:3.0", WebMethod ]
{
 $$$LOGALERT("Dentro de WSCLIENTE.RESNS.ConsultaRecetas la CabeceraSAML: "_CabeceraSAML)
 do ##class(Ens.Util.XML.Reader).ObjectFromString(.objetoSAML,CabeceraSAML,"%SAML.Assertion",)
 $$$LOGALERT("Dentro de WSCLIENTE.RESNS.ConsultaRecetas el objetoSAML: "_objetoSAML)
 
 set x509alias = ##class(Util.TablasMaestras).getValorMaestra("PARAMETROS","aliasCertMSSSI")
 set password = ##class(Util.TablasMaestras).getValorMaestra("PARAMETROS","pwdCertMSSSI")
 Set credset = ##class(%SYS.X509Credentials).GetByAlias(x509alias,password)
 set ref=$$$KeyInfoX509Certificate
 set assertion=##class(%SAML.Assertion).CreateX509(credset,ref)
 ;do ..SecurityOut.AddElement(assertion)
 do ..SecurityOut.AddElement(objetoSAML) 

 Do (..WebMethod("RespondingGatewayCrossGatewayQuery")).Invoke($this,"urn:ihe:iti:2007:CrossGatewayQuery",.RequestSlotList,.ResponseOption,.AdhocQuery,.federation,.startIndex,.maxResults,.id,.comment,.federated,.ResponseSlotList,.RegistryErrorList,.RegistryObjectList,.requestId,.totalResultCount,.status)
}
}

Where the lines which convert the String back to a %SAML.Assertion object are:

 do ##class(Ens.Util.XML.Reader).ObjectFromString(.objetoSAML,CabeceraSAML,"%SAML.Assertion",)
 

And the ones which are supposed to send it as saml:Assertion inside SOAP:Header are:

 set x509alias = ##class(Util.TablasMaestras).getValorMaestra("PARAMETROS","aliasCertMSSSI")
 set password = ##class(Util.TablasMaestras).getValorMaestra("PARAMETROS","pwdCertMSSSI")
 Set credset = ##class(%SYS.X509Credentials).GetByAlias(x509alias,password)
 set ref=$$$KeyInfoX509Certificate
 set assertion=##class(%SAML.Assertion).CreateX509(credset,ref)
 ;do ..SecurityOut.AddElement(assertion)
 
 do ..SecurityOut.AddElement(objetoSAML)

However when we generate a Log SOAP, we do not observe the saml:Assertion being added .  In fact it only shows the following content inside SOAP Header:

Output from Web client with SOAP action = urn:ihe:iti:2007:CrossGatewayQuery
<?xml version="1.0" encoding="UTF-8" ?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV='http://www.w3.org/2003/05/soap-envelope' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xmlns:s='http://www.w3.org/2001/XMLSchema' xmlns:wsa='http://www.w3.org/2005/08/addressing'>
    <SOAP-ENV:Header>
        <wsa:Action>urn:ihe:iti:2007:CrossGatewayQuery</wsa:Action>
        <wsa:MessageID>urn:uuid:F5355A1C-F948-11ED-B9D9-005056AAA48E</wsa:MessageID>
        <wsa:ReplyTo>
            <wsa:Address>http://www.w3.org/2005/08/addressing/anonymous</wsa:Address>
        </wsa:ReplyTo>
        <wsa:To>...</wsa:To>
    </SOAP-ENV:Header>
    <SOAP-ENV:Body>
 ...
    </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

How would you recommend us to send the SAML as an assertion inside the SOAP Header to the target system from a SOAP Bussiness Operation?

Thans for answering this question

How would you recommend us to send the SAML as an assertion inside the SOAP Header to the target system from a SOAP Bussiness Operation?

Thanks for reading and replying.