Blog

Home > Blog

Securing Web Services with Sitecore Authentication Providers in 5 easy steps

[Credits to http://keithelder.net/blog/]

During a complicated project involving several 3rd party system systems I discovered that I had a need to expose some Sitecore functionality to external applications.  The obvious model to go with was web services, since I could readily make use of the Sitecore context system to access and alter the environment.

In the many articles that I read on the subject none seemed to address the matter of security.  Not so much securing the web service from access by the outside world, but providing a properly authenticated Sitecore user for the Context object, rather than simply elevating privileges through the Sitecore.SecurityModel.SecurityDisabler class.

In may particular case I had a Windows application that would call into the Sitecore web service (a simple asmx file within the website) using the .Net 3.5 service reference model.  This required a number of fiddly configuration steps and I thought I would share these with the community.

This is my first Sitecore article so please bear with me if I have made some beginner errors or assumptions – feedback via comments is always welcome!

[
Edit
I was recently asked why anyone might bother securing their web services.  Good question.  The answer really depends on what your web service does.  For many people, web services will be used to perform complicated actions that will be referred to by several pages on a site.  They need to be public facing as they are often called by JavaScript directly from a client, so IPSec is no use, and they will often perform privileged functions such as creating a new user.

Let’s examine a plausible scenario.  You have a page that allows users to sign up to your site.  Posting the form calls a web service by AJAX to do the work of creating the user in Sitecore.  If I look at your AJAX code I will be able to see the URL of the web service.  From there I can query the web service’s WSDL file and discover all the methods you provide.  Some of them will do interesting things.  If I’m of malintent I could create 100,000,000,000 users in your system and attempt a denial-of-service.  Or much worse depending on what other methods your web service exposes.

You simply can’t rely on people not finding your file.  They will.  Google will.  A colleague once described it as “security through obscurity” and it’s a fool’s defence.

Incidentally you can prevent your web service from displaying the auto-generated WSDL (as you only need that for consumer web services once your site is live.  See here for more information on protecting your WSDL.
]

Step 1: Configure HTTPS

Sitecore authentication is performed with usernames and passwords being transmitted in the clear, a side-effect of Forms-based authentication.  It is therefore imperative that you configure https to ensure the credentials cannot be trapped in transit.

There are a million articles on configuring this for a production environment, but for a dev environment I’ll save you the trouble and describe them here.

  1. Create a self-signed certificate in IIS7 for the host-header that you are developing against
    • In IIS Manager navigate to the server root node
    • Select the Server Certificates feature
    • Click the action to Create Self-Signed Certificate
    • Name the certificate with the host header you are using, in my case that was localhost
  2. Enable the HTTPS binding on your IIS site
    • Navigate to the Site node for your site, in my case it was Default Web Site
    • Click the action to edit Bindings…
    • Add… a binding for HTTPS on port 443 and set the SSL certificate to the one you created before

You should now be able to access Sitecore using https, though you will likely get certificate errors from your browser because the certificate isn’t signed by a trusted authority.  Don’t worry about that for now, in production you will use a properly signed certificate, won’t you!

Step 2: Configure your Service Reference

I’m going to assume you already have a Visual Studio project to house the code that will call your web service (the caller).  I’m also going to assume you’ve already created your web service (the callee).  Mine is called HelloWorld().

  1. In your caller project, right-click the References node and select to Add Service Reference…
  2. Enter the url to your service in the Address box and hit Go
  3. From the tree on the left of the dialog you should now see your service and a SOAP interface that you can import – select it and give it a namespace and click OK

When you’ve done that you should see an app.config file appear in your project, or if you had one it now has some new content.  If you are in a web project you will likely have guessed to look in the web.config instead.  There is a <system.serviceModel> section in there.  This specifies binding and endpoint information for your auto-generated service classes.  There are a few things you will need to change – the important bits are below:

<bindings>
  <basicHttpBinding>
   <binding…>
     <security mode=”Transport”>
        <transport clientCredentialType=”None” proxyCredentialType=”None” realm=”" />
        <message clientCredentialType=”UserName” algorithmSuite=”Default” />
     </security>
   </binding>
  </basicHttpBinding>
</bindings>
<client>
  <endpoint address=”https://…” … />
</client>

Step 3: Creating the Web Service extensions

Ideally the integration of the authentication doesn’t interfere with the business logic of our methods.  We can use a series of attributes and supporting service extensions to help here.

SitecoreAuthenticationHeader – a custom header that will transport our credentials

using System.Web.Services.Protocols;
using SC = Sitecore;

public class SitecoreAuthenticationHeader : SoapHeader
{
        public string Domain;
        public string Username;
        public string Password;

        internal SitecoreAuthenticationHeader() { }
        public SitecoreAuthenticationHeader( string domain, string username, string password )
        {
            Domain = domain;
            Username = username;
            Password = password;
        }

        public static bool Validate( SitecoreAuthenticationHeader soapHeader )
        {
            try
            {
                return SC.Security.Authentication.AuthenticationManager.Login(
                        soapHeader.Domain + “\\” + soapHeader.Username,
                        soapHeader.Password,
                        false
                );
            }
            catch (Exception)
            {
                return false;
            }
        }
}

SitecoreAuthentication – a SoapExtension class that will detect the header and pass it on for validation

Note that this raises an exception when validation fails since there is no elegant way to return values with a declarative syntax.

using System.Web.Services.Protocols;
using System.Security.Authentication;

public class SitecoreAuthentication : SoapExtension
{
    public override object GetInitializer( Type serviceType ) { return null; }
    public override object GetInitializer( LogicalMethodInfo methodInfo, SoapExtensionAttribute attribute ) { return null; }
    public override void Initialize( object initializer ) { }

    public override void ProcessMessage( SoapMessage message )
    {
        if ( message.Stage == SoapMessageStage.AfterDeserialize )
        {
            bool validated = false;
            foreach ( SoapHeader hdr in message.Headers )
            {
                if ( hdr is SitecoreAuthenticationHeader )
                {
                    try
                    {
                        if ( !( validated = SitecoreAuthenticationHeader.Validate( (SitecoreAuthenticationHeader)hdr ) ) )
                            throw new Exception();
                    }
                    catch { throw new AuthenticationException( “SOAP authentication failed. Invalid credentials specified.” ); }
                    break;
                }
            }

            if ( !validated ) throw new AuthenticationException( “SOAP authentication failed. Credentials not specified.” );
        }
    }
}

SitecoreAuthenticationAttribute – a SoapExtensionAttribute that tells the infrastructure to include the SitecoreAuthentication extension into the processing stack

using System.Web.Services.Protocols;

public class SitecoreAuthenticationAttribute : SoapExtensionAttribute
{
    private int _priority;

    public override int Priority
    {
        get { return _priority; }
        set { _priority = value; }
    }
    public override Type ExtensionType
    {
        get { return typeof( SitecoreAuthentication); }
    }
}

Step 4: Integrating Sitecore Authentication Declaratively

We need to add a field to the WebService class.  This will receive the header but importantly it will tell the framework that there is a header of a particular type that it needs to watch out for, which triggers the rest of the wiring.

using System.ServiceModel;

public class WebServiceAPI : System.Web.Services.WebService
{
    public SitecoreAuthenticationHeader AuthenticationHeader;

    [WebMethod]
    [SoapHeader("AuthenticationHeader"), SitecoreAuthentication]
    public string HelloWorld()
    {
        try
        {
            if ( !Sitecore.Context.User.IsAuthenticated )
                return “Goodbye cruel world!”;
            else
                return “Hello world”;
        } catch ( FaultException fe )
        {
            return fe.Message;
        }
    }
}

If you were paying attention you would also have noticed the two additional attributes on the WebMethod itself.

The SoapHeaderAttribute connects up the field and the header from the soap message, while the SitecoreAuthenticationAttribute provides the connection to the SitecoreAuthentication SoapExtension that will process the header and ultimately perform the validation.

The FaultException is raised by the framework and contains the message from our AuthenticationException as a subset of some other messages.

Step 5: Calling the web service

Here is some sample code for actually making a call to the HelloWorld web service method.  Note that I have already configured all the other stuff as described above.

public class TestClass
{
  private void _testAuthentication()
  {
    //disable certificate checking on the certificate for now
    System.Net.ServicePointManager.ServerCertificateValidationCallback = ( s, cert, chain, pError ) => true;

    //create an instance of the service class
    SitecoreService.WebServiceAPISoapClient c = new SitecoreService.WebServiceAPISoapClient();

    //specify Sitecore credentials for executing the method
    SitecoreService.SitecoreAuthenticationHeader hdr = new SitecoreService.SitecoreAuthenticationHeader()
                                                                                      { Domain = “sitecore”,
                                                                                        Username = “admin”,
                                                                                        Password = “b” };

    //execute the method passing in the credentials
    string ret = c.HelloWorld( hdr );
  }
}

Of note is the first line, that overrides the systems native server certificate validation with a lambda expression that simply always evaluates to true.  This gets us around the fact our certificate is self-signed, but obviously don’t put that into production code (I use an app setting to disable it usually).

Also of note here, the code generator has added the header as the first parameter to my method – it will do that.  It ensures you don’t forget to supply the credentials.  You can re-use the header object you’ve created by storing it somewhere convenient.  I’d also be inclined not to hard-wire the credentials into the code, but that is up to your particular scenario.

1, 2, 3, 4, 5…Done

Once you’ve got all that you will notice that you have a properly validated Sitecore.Context.User and you can use code, or add more extensions, to decide whether the user that is logged in has sufficient rights to perform the action.


No Comments

Leave a Reply

Level 4, 263 Kent Street, Sydney, Australia  +61 2 9321 1555  info@5limes.com.au
Copyright ©2009 5 Limes Pty Ltd. ABN 87 119 340 680  All Rights Reserved