I'm trying to make a call to a webservice and want to manually add the ws-security headers into the request because .net core 2.2 currently does not support ws-security.
I have created my custom security header class:
public class SoapSecurityHeader : MessageHeader
{
private readonly string _password, _username;
public SoapSecurityHeader(string id, string username, string password)
{
_password = password;
_username = username;
}
public override bool MustUnderstand => true;
public override string Name
{
get { return "Security"; }
}
public override string Namespace
{
get { return "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"; }
}
protected override void OnWriteStartHeader(XmlDictionaryWriter writer, MessageVersion messageVersion)
{
writer.WriteStartElement("wsse", Name, Namespace);
writer.WriteAttributeString("s", "mustUnderstand", "http://schemas.xmlsoap.org/soap/envelope/", "1");
writer.WriteXmlnsAttribute("wsse", Namespace);
}
protected override void OnWriteHeaderContents(XmlDictionaryWriter writer, MessageVersion messageVersion)
{
writer.WriteStartElement("wsse", "UsernameToken", Namespace);
writer.WriteAttributeString("wsu", "Id", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd", "UsernameToken-32");
// Username
writer.WriteStartElement("wsse", "Username", Namespace);
writer.WriteValue(_username);
writer.WriteEndElement();
// Password
writer.WriteStartElement("wsse", "Password", Namespace);
writer.WriteAttributeString("Type", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText");
writer.WriteValue(_password);
writer.WriteEndElement();
writer.WriteEndElement();
}
}
And this is my method calling the SOAP service:
public ActionResult<Ted_Result> Get(DateTime dateFrom, DateTime dateTo, int? pageFrom, int? pageTo)
{
BasicHttpBinding basicHttpBinding = new BasicHttpBinding(BasicHttpSecurityMode.Transport);
basicHttpBinding.Security.Transport.ClientCredentialType = HttpClientCredentialType.None;
EndpointAddress endpointAddress = new EndpointAddress(new Uri("https://localhost/SomeService.svc"));
ChannelFactory<IConnectPublicService> factory = new ChannelFactory<IConnectPublicService>(basicHttpBinding, endpointAddress);
GetContractNoticesResponseMessage result = null;
// Bypass SSL/TLS secure channel validation
#if DEBUG
factory.Credentials.ServiceCertificate.SslCertificateAuthentication = new X509ServiceCertificateAuthentication
{
CertificateValidationMode = X509CertificateValidationMode.None,
RevocationMode = X509RevocationMode.NoCheck
};
#endif
// Debugging inspector
factory.Endpoint.EndpointBehaviors.Add(new InspectorBehavior());
IConnectPublicService serviceProxy = factory.CreateChannel();
((ICommunicationObject)serviceProxy).Open();
var opContext = new OperationContext((IClientChannel)serviceProxy);
var soapSecurityHeader = new SoapSecurityHeader("UsernameToken-32", "sampleUsername", "samplePassword123");
// Adding the security header
opContext.OutgoingMessageHeaders.Add(soapSecurityHeader);
var prevOpContext = OperationContext.Current; // Optional if there's no way this might already be set
OperationContext.Current = opContext;
var info = new ExternalIntegrationRequestMessageInfo
{
UserCode = "1000249",
CompanyCode = "200000040"
};
var request = new GetContractNoticesRequestMessage
{
Info = info,
DateFrom = dateFrom,
DateTo = dateTo,
PageFrom = pageFrom,
PageTo = pageTo
};
result = serviceProxy.GetContractNoticesAsync(request).ConfigureAwait(false).GetAwaiter().GetResult();
return Ok(result);
}
If I put a breakpoint inside the inspector at BeforeSendRequest I can see that the security header is added to the request:
<wsse:Security s:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
<wsse:UsernameToken wsu:Id="UsernameToken-32" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
<wsse:Username>sampleUsername</wsse:Username>
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">samplePassword123</wsse:Password>
</wsse:UsernameToken>
</wsse:Security>
And putting a breakpoint inside the inspector at AfterReceiveReply, I get the CORRECT result, but I still get an exception. The result:
<...>
<s:Header>
<...>
<o:Security s:mustUnderstand="1" xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
<u:Timestamp u:Id="_0">
<u:Created>2019-01-11T19:42:53.606Z</u:Created>
<u:Expires>2019-01-11T19:47:53.606Z</u:Expires>
</u:Timestamp>
</o:Security>
</s:Header>
<s:Body>
<GetContractNoticesResponseMessage>
<ContractNotices>....</ContractNotices>
</GetContractNoticesResponseMessage>
</s:Body>
The exception:
An unhandled exception occurred while processing the request.
ProtocolException: The header 'Security' from the namespace 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd' was not understood by the recipient of this message, causing the message to not be processed. This error typically indicates that the sender of this message has enabled a communication protocol that the receiver cannot process. Please ensure that the configuration of the client's binding is consistent with the service's binding.
Why do I still get an exception after calling the webservice successfully?
For .net core 2.2 you need to pass Security header manually. You'll need to-do some workarounds - WCF isn't fully implemented yet in .Net Core (has been stated by project contributors). Assuming the requirements aren't too complex, you should be able to get something going without too much headache.
public class SecurityHeader : MessageHeader
{
public UsernameToken UsernameToken { get; set; }
public override string Name => "Security";
public override string Namespace => "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd";
public override bool MustUnderstand => true;
protected override void OnWriteHeaderContents(XmlDictionaryWriter writer, MessageVersion messageVersion)
{
XmlSerializer serializer = new XmlSerializer(typeof(UsernameToken));
serializer.Serialize(writer, this.UsernameToken);
}
}
[XmlRoot(Namespace = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd")]
public class UsernameToken
{
[XmlAttribute(Namespace = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd")]
public string Id { get; set; }
[XmlElement]
public string Username { get; set; }
}
Add below code in BeforeSendRequest method
public object BeforeSendRequest(ref Message request, IClientChannel channel)
{
var soapSecurityHeader = new SecurityHeader()
{
UsernameToken = new UsernameToken()
{
Username = "User Name"
}
};
request.Headers.Add(soapSecurityHeader);
}
I did some digging and in the AfterReceiveReply
you could do this:
public void AfterReceiveReply(ref Message reply, object correlationState)
{
var security = reply.Headers.Where(w => w.Namespace == "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd").First();
reply.Headers.UnderstoodHeaders.Add(security);
}
I suppose that in this step you could also check the value of the timestamp, if DateTime.UtcNow
is in range and act upon that...?
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With