Authentication using HTTP Authorization Header in WCF SOAP API

13,615

Solution 1

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.

Solution 2

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.

Share:
13,615
cariseldon
Author by

cariseldon

Sr. Software Developer @ Nudge.ai Mostly work with C#, Java, JavaScript, Bash

Updated on June 05, 2022

Comments

  • cariseldon
    cariseldon about 2 years

    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.

  • cariseldon
    cariseldon about 11 years
    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.
  • Jeff
    Jeff over 10 years
    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?
  • dthorpe
    dthorpe over 10 years
    @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.
  • Jeff
    Jeff over 10 years
    What about in your if (!authorized) block, where does the baseUri object come from?
  • dthorpe
    dthorpe over 10 years
    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.
  • Jeff
    Jeff over 10 years
    In the CredentialsHelper.AddCredentialsHeader method, what is the value of the callerDetails argument when you create the new MessageHeader<string>?
  • evgenyl
    evgenyl over 10 years
    Sorry, my fault. I edited the answer - I mean "cred" and not callerDetails