How to add WS-Security to .NET Core SOAP Headers without WCF

by Landon | Jul 06, 2018 | Leave a comment

If you’ve ever enjoyed the wonderful experience that is consuming a web service that’s at least 10 years old, odds are you’ve dealt with WS-Security headers.

If you’ve had to deal with that while developing in .NET Core, you probably have spent a great deal of time banging your head against the wall.

I had this issue recently. I was developing a front-end RESTful web service which exposed one or two select internal web service operations to the wider public. One of these operations resided on a web service that was developed several years ago; it required WS-Security headers in order to authenticate.

Lucky for me, support for WS-Security in the WCF libraries for .NET Core is non-existent. This lack of support has been a known issue since 2016, but no one appears to have been able to find the time to fix this glaring hole.

Well, it may have been fixed at some point, but that work (if it was done) was lost.

So, what do you do if native support for WS-Security headers in your soap payloads isn’t supported by the framework you’re using for your development? Write your own custom headers that serialize the XML into the SOAP Payload.

You’re trying to get this in your serialized SOAP payloads

<wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" soapenv:mustUnderstand="1">
   <wsse:UsernameToken xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" wsu:Id="UsernameToken-12039161">
      <wsse:Username></wsse:Username>
      <wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText"></wsse:Password>
    </wsse:UsernameToken>   
</wsse:Security>

You have to model out the UsernameToken object into it’s own class first. Annotate the class with the necessary XmlSerializer annotations so that when it gets serialized out, it works as it needs to.

    [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; }
        [XmlElement]
        public Password Password { get; set; }
    }

Then use that model in a class that inherits from the System.ServiceModel.Channels.MessageHeader class. Override theĀ OnWriteHeaderContents method and manually serialize the XML.

    public class Security : MessageHeader
    {
        public UsernameToken UsernameToken { get; set; }

        public override string Name => this.GetType().Name;

        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);
        }
    }

And you’ll get a SOAP Header that does what it is designed to do … authenticate you against a crazy-old web service with an authentication scheme that isn’t supported by dotnet core… or the dotnet foundation … or anything.