How to add ws-security header in .net core?
Asked Answered
O

2

7

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?

Overcash answered 14/1, 2019 at 12:37 Comment(4)
if you have access to server, I would recommend looking at the logs. If you don't I would recommend that you create a full .NET client that works. and then, using breakpoints compare what's the difference between what you are trying .NET core and what is in the clientMock
Did you ever find a solution? I'm having the same issue and I could remove the headers of the reply message in AfterReceiveReply but that sounds like a bad plan :-)Mar
@Mar No solution found yet. I think the workaround is modifying the security headers of the reply without compromising security, removing the headers is not a good idea.Overcash
Did anyone found solution for this? :) Having the same issue.Pinero
F
6

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);
}
Fawnfawna answered 24/6, 2020 at 13:19 Comment(1)
this worked well. just needs the password added to the model and worked perfectly for my purposes.Dodecasyllable
M
3

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...?

Mar answered 28/3, 2019 at 15:2 Comment(2)
It seems like you really have to modify the security headers from the reply. Like someone did here: github.com/dotnet/wcf/issues/8#issuecomment-474783080 So I am assuming that your proposed code is the way to go.Overcash
Just by looking at the method name AfterReceiveReply I would expect to be way to late to set the security header. This must be done before the request is sent, not after.Twinscrew

© 2022 - 2024 — McMap. All rights reserved.