2

We currently have a WCF SOAP API that allows the consumer to authenticate using a username and password (internally uses a UserNamePasswordValidator) For reference the username and password is passed in the SOAP Body as follows:

<o:Security xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" mustUnderstand="1">
<Timestamp Id="_0">
    <Created>
        2013-04-05T16:35:07.341Z</Created>
        <Expires>2013-04-05T16:40:07.341Z</Expires>
    </Timestamp>
    <o:UsernameToken Id="uuid-ac5ffd20-8137-4524-8ea9-3f4f55c0274c-12">
        <o:Username>someusername</o:Username>
        <o:Password o:Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">somepassword
    </o:Password>
</o:UsernameToken>
</o:Security>

We we like to additionally support a consumer to specify credentials in the HTTP Authorization header, as either Basic auth, or an OAuth Bearer token

We already have several ways of actually doing the authentication for non-SOAP APIs, but I am not familiar with how to tell WCF to use any class I might create for this. How can I accomplish this? The only other question I have seen that attempts to answer this is here, but the accepted answer uses SOAP headers, not HTTP headers, and the asker essentially gave up.

Obviously any solution needs to be backwards compatible - we need to continue to support consumers specifying credentials in the SOAP Security Header.

Community
  • 1
  • 1
csauve
  • 5,904
  • 9
  • 40
  • 50
  • Did you try this? - http://cisforcoder.wordpress.com/2010/12/01/how-to-implement-basic-http-authentication-in-wcf-on-windows-phone-7 – Vedran Apr 11 '13 at 07:43
  • Have you had a look at the Service authorization manager? http://msdn.microsoft.com/en-us/library/ms731774(v=vs.110).aspx – Phil Carson Apr 11 '13 at 11:16
  • @Vedran that is how to add headers to the client, I need to parse them on the server. – csauve Apr 11 '13 at 14:01
  • @PhilCarson I have tried a few things with it, but I am really stuck with that if I try to include the UserNamePasswordValidator, if there is no SOAP Security header the request fails - If using OAuth I cannot get it to use just the authorization manager. – csauve Apr 11 '13 at 15:14
  • Also I am now getting a MustUnderstandSoapException‎ when I parse the Security header manually (which I'd rather not do in the first place) – csauve Apr 11 '13 at 20:45

3 Answers3

3

You should look into implementing a ServiceAuthorizationManager for your WCF service to handle the HTTP Authorization header authorization.

Create a class that inherits from System.ServiceModel.ServiceAuthorizationManager, and override one or more of the CheckAccess functions to examine the incoming web request and decide whether to allow it in or reject it. Rough sketch:

public class MyServiceAuthorizationManager: System.ServiceModel.ServiceAuthorizationManager
    {
        public override bool CheckAccess(OperationContext operationContext, ref Message message)
        {
            var reqProp = message.Properties[HttpRequestMessageProperty.Name] as HttpRequestMessageProperty;
            var authHeader = reqProp.Headers[HttpRequestHeader.Authorization];

            var authorized = // decide if this message is authorized...

            if (!authorized)
            {
                var webContext = new WebOperationContext(operationContext);
                webContext.OutgoingResponse.StatusCode = HttpStatusCode.Unauthorized;
                webContext.OutgoingResponse.Headers.Add(HttpResponseHeader.WwwAuthenticate, String.Format("Bearer realm=\"{0}\"", baseUri.AbsoluteUri));
            }

            return authorized;
        }
}

Wire this into your WCF service where you create the service host:

    restAPIServiceHost = new DataServiceHost(typeof(API.RestAPIService), restUris);

    var saz = restAPIServiceHost.Description.Behaviors.Find<ServiceAuthorizationBehavior>();
    if (saz == null)
    {
        saz = new ServiceAuthorizationBehavior();
        restAPIServiceHost.Description.Behaviors.Add(saz);
    }

    saz.ServiceAuthorizationManager = new MyServiceAuthorizationManager();

    restAPIServiceHost.Open();

This will inject an authorization check into every method exposed by the WCF service, without requiring any changes to the service methods themselves.

Your MyServiceAuthorizationManager implementation can also be installed into your WCF service using web.config magic, but I find direct code easier to understand and debug.

Note that it will be difficult to have multiple authorization check systems in force on the same service without them stomping on each other or leaving a gap in your security coverage. If you have a UserNamePasswordValidator in force to handle the SOAP user credentials case, it will reject a message that contains only an HTTP Authorization header. Similarly, a ServiceAuthorizationManager that only checks for HTTP Authorization header will fail a web request containing SOAP user credentials. You will most likely need to figure out how to check for both kinds of auth credential representations in the same auth check. For example, you could add code to the CheckAccess function above to look for, extract, and test the SOAP user credentials if an HTTP Authorization header is not present in the message.

When you have to accept multiple auth representations you'll need to decide on precedence, too. If an HTTP Authorization header is present, I suspect it should take precedence over anything contained in the SOAP message. If the HTTP Authorization header is present but invalid, full stop - reject the request as unauthorized. It doesn't matter what's in the SOAP stuff - an invalid HTTP Authorization header is always bad news. If there is no HTTP Authorization header at all, then you can poke around to see if there is a SOAP security element from which you can get SOAP user credentials and test them for validity.

dthorpe
  • 35,318
  • 5
  • 75
  • 119
  • When I do that, and try to combine both into the same endpoint, I have to parse the security header myself and push a security header back into the response... basically I have to implement WS-Security myself. – csauve Apr 14 '13 at 16:23
  • In the second line of your overridden `CheckAccess` method, you instantiate a new `AuthorizationHeader`. In what namespace is this class? I can't seem to find it. In your `if (!authorized)` block, where does the `baseUri` object come from? – Jeff Nov 05 '13 at 22:46
  • @Lumirris - woops. That was a helper class I use to parse the authorization header into its constituent parts. Not important to the discussion, so I've removed it from the code. – dthorpe Nov 06 '13 at 18:40
  • What about in your `if (!authorized)` block, where does the baseUri object come from? – Jeff Nov 06 '13 at 23:34
  • baseUri is the base Uri of the web service receiving the request. It's the Uri that was passed into the wcf service host constructor. – dthorpe Nov 08 '13 at 04:54
1

One of the ways you can go is using MessageInspectors.
Something like this:

First - create message inspector - to be responsible to add header with your credentials

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.ServiceModel.Dispatcher;
using System.ServiceModel.Channels;
using System.ServiceModel;
using System.Xml;

namespace your_namespace
{


    /// <summary>
    /// /************************************
    /// * 
    /// * Creating Message inspector for 
    /// * updating all outgoing messages with Caller identifier header
    /// * read http://msdn.microsoft.com/en-us/magazine/cc163302.aspx
    /// * for more details
    /// * 
    /// *********************/
    /// </summary>
    public class CredentialsMessageInspector : IDispatchMessageInspector,
        IClientMessageInspector
    {
        public object AfterReceiveRequest(ref Message request,
            IClientChannel channel,
            InstanceContext instanceContext)
        {
            return null;
        }

        public void BeforeSendReply(ref Message reply, object
            correlationState)
        {
#if DEBUG
            //// Leave empty 
            //MessageBuffer buffer = reply.CreateBufferedCopy(Int32.MaxValue);
            //Message message = buffer.CreateMessage();
            ////Assign a copy to the ref received
            //reply = buffer.CreateMessage();


            //StringWriter stringWriter = new StringWriter();
            //XmlTextWriter xmlTextWriter = new XmlTextWriter(stringWriter);
            //message.WriteMessage(xmlTextWriter);
            //xmlTextWriter.Flush();
            //xmlTextWriter.Close();

            //String messageContent = stringWriter.ToString();
#endif             
        }

        public void AfterReceiveReply(ref Message reply, object
            correlationState)
        {
#if DEBUG
            //// Leave empty 
            //MessageBuffer buffer = reply.CreateBufferedCopy(Int32.MaxValue);
            //Message message = buffer.CreateMessage();
            ////Assign a copy to the ref received
            //reply = buffer.CreateMessage();


            //StringWriter stringWriter = new StringWriter();
            //XmlTextWriter xmlTextWriter = new XmlTextWriter(stringWriter);
            //message.WriteMessage(xmlTextWriter);
            //xmlTextWriter.Flush();
            //xmlTextWriter.Close();

            //String messageContent = stringWriter.ToString();
#endif
        }

        public object BeforeSendRequest(ref Message request,
            IClientChannel channel)
        {
            request = CredentialsHelper.AddCredentialsHeader(ref request);
            return null;
        }

        #region IDispatchMessageInspector Members

        #endregion
    }
}

Second - add the code to add header

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.ServiceModel.Channels;
using System.ServiceModel;

namespace your_namespace
{

    public class CredentialsHelper
    {
       // siple string is for example - you can use your data structure here
        private static readonly string CredentialsHeaderName = "MyCredentials";
        private static readonly string CredentialsHeaderNamespace = "urn:Urn_probably_like_your_namespance";

        /// <summary>
        /// Update message with credentials
        /// </summary>
        public static Message AddCredentialsHeader(ref Message request)
        {

          string user = "John";
          string password = "Doe";

            string cred = string.Format("{0},{1}",   user, password);

            // Add header
            MessageHeader<string> header = new MessageHeader<string>(cred);
            MessageHeader untyped = header.GetUntypedHeader(CredentialsHeaderName, CredentialsHeaderNamespace);

            request = request.CreateBufferedCopy(int.MaxValue).CreateMessage();
            request.Headers.Add(untyped);

            return request;
        }

        /// <summary>
        /// Get details of current credentials from client-side added incoming headers
        /// 
        /// Return empty credentials when empty credentials specified 
        /// or when exception was occurred
        /// </summary>
        public static string GetCredentials()
        {
            string credentialDetails = string.Empty;
            try
            {
                credentialDetails = OperationContext.Current.IncomingMessageHeaders.
                    GetHeader<string>
                        (CredentialsHeaderName, CredentialsHeaderNamespace);
            }
            catch
            {
                    // TODO: ...
            }
            return credentialDetails;
        }

    }
}

Third - get your credentials on the server side

public void MyServerSideMethod()
{
   string credentials = CredentialsHelper.GetCredentials();
   . . . 
}

Hope this helps.

evgenyl
  • 7,837
  • 2
  • 27
  • 32
  • In the `CredentialsHelper.AddCredentialsHeader` method, what is the value of the `callerDetails` argument when you create the new `MessageHeader`? – Jeff Nov 13 '13 at 23:03
  • Sorry, my fault. I edited the answer - I mean "cred" and not callerDetails – evgenyl Nov 14 '13 at 17:27
0

For Basic authentication I got WCF working by setting the security mode to Transport.

For example in the web.config:

<system.serviceModel>
  <services>
    <service behaviorConfiguration="DefaultServiceBehavior" name="MyService">
      <endpoint address="basic" binding="basicHttpBinding" bindingConfiguration="BasicAuthenticationBinding" name="MyEndpoint" contract="MyContract" />
    </service>
  </services>
  <bindings>
    <basicHttpBinding>
      <binding name="BasicAuthenticationBinding">
        <security mode="Transport">
          <transport clientCredentialType="Basic" />
        </security>
      </binding>
    </basicHttpBinding>
   </bindings>
</system.serviceModel>
Benj
  • 405
  • 4
  • 12