2015-01-26

Creating a .Net WebApi service that uses SAML2 authentication

In the last post, I demonstrated how to consume a cross-domain WebApi service that uses SAML2 for authentication and authorization. This is useful when you want to use a shared service layer that provides data for multiple websites. However, some code work is needed to enable this on the server side as well.

In this example we'll create a new .Net WebApi layer in Visual Studio 2013. Everything in this post can be transferred to an MVC app just as easily if needed, however Ajax calls to MVC apps are most likely on the same domain so will not need this extra work. The only difference lies in whether you register CorsConfiguration in the WebApiConfig.cs or RouteConfig.cs. That said, let's get started.

First, create a new ASP.Net Web Application. We'll call the new project POC_SAML_WebApi. When you create the project, you'll be prompted for the type of web application you wish to create. Choose WebApi:

1. Choose WebApi; 2. Change Authentication; 3. Choose No Authentication

Next, you'll also need to make sure Visual Studio won't add all kinds of unnecessary authentication code and configuration to your project, as we'll add all the necessary configuration and code ourselves. Click the Change Authentication button and choose No Authentication in the pop-up. Click OK in both screens and your project will be created.

Out of the box, .Net WebApi does not support SAML2. To facilitate this, add the Thinktecture.IdentityModel library through the NuGet Package Manager.



Next, add the following code to your WebApiConfig.cs, or merge it with the existing file. Be sure to include the relevant namespaces that Visual Studio suggests:

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        CorsConfiguration corsConfig = new CorsConfiguration();
        corsConfig.AllowAll().AllowAll();
        var corsHandler = new CorsMessageHandler(corsConfig, config);
        config.MessageHandlers.Add(corsHandler);
        
        // default API route
        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );
    }
}

This registers the Thinktecture CORS module for handling incoming WebApi calls. The corsConfig.AllowAll().AllowAll() call may need to be adjusted to your personal needs, but for this demonstration we'll use this. If you want more information about the different configuration options, have a look at this post by Brock Allen. If you want to do SAML authentication for an MVC app, simply adjust above code for your RouteConfig.cs.

The last bit of code needed for SAML2 support is in the Global.asax file, which makes sure unauthorized Ajax requests aren't promptly redirected to ADFS but simply answered with a 401 error, enabling the error handling demonstrated in the last post:

protected void Application_BeginRequest(object s, EventArgs ea)
{
    FederatedAuthentication.WSFederationAuthenticationModule.AuthorizationFailed += (sender, e) =>
    {
        if (new HttpRequestWrapper(System.Web.HttpContext.Current.Request).IsAjaxRequest())
        {
            e.RedirectToIdentityProvider = false;
        }
    };
}
Of course, you should make sure your JavaScript calling this WebApi actually adds the X-Requested-With header for this to work.

Last but certainly not least are changes required in the Web.config file. These settings tell your application where your authentication server resides and how your application should communicate with it.


  <configSections>
    <section name="system.identityModel" type="System.IdentityModel.Configuration.SystemIdentityModelSection, System.IdentityModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089" />
    <section name="system.identityModel.services" type="System.IdentityModel.Services.Configuration.SystemIdentityModelServicesSection, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089" />
  </configSections>
 
  <appSettings>
    ...
    <add key="ida:AdfsMetadataEndpoint" value="[AD FS Federation Metadata endpoint]" />
    <add key="ida:Audience" value="[WebApi address]" />
  </appSettings>
 
  <system.webServer>   
    <modules>
      <add name="SessionAuthenticationModule" type="System.IdentityModel.Services.SessionAuthenticationModule, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" preCondition="managedHandler" />
      <add name="WSFederationAuthenticationModule" type="System.IdentityModel.Services.WSFederationAuthenticationModule, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" preCondition="managedHandler" />
    </modules>
  </system.webServer>
 
  <system.identityModel>
    <identityConfiguration>
      <audienceUris>
        <add value="[WebApi address]" />
      </audienceUris>
      <issuerNameRegistry>
        <trustedIssuers>
          <add name="[AD FS certificate name]" 
               thumbprint="[AD FS certificate thumbprint]" />
        </trustedIssuers>
      </issuerNameRegistry>
    </identityConfiguration>
  </system.identityModel>
  <system.identityModel.services>
    <federationConfiguration>
      <wsFederation passiveRedirectEnabled="true" 
                    issuer="[AD FS SAML2 endpoint address]" 
                    realm="[WebApi realm]" requireHttps="true" />
      <cookieHandler requireSsl="true" />
    </federationConfiguration>
  </system.identityModel.services>

In these configuration settings are a couple of things you need to adjust yourself (just find/replace the following strings):
  • [AD FS Federation Metadata endpoint]: This is the endpoint address for ADFS ending in "/FederationMetadata/2007-06/FederationMetadata.xml", for instance "https://adfs.mycompany.com/FederationMetadata/2007-06/FederationMetadata.xml";
  • [WebApi address]: The root address where your new WebApi will be running, for instance "https://poc-saml-webapi.mycompany.com:443";
  • [AD FS certificate name]: The name of the certificate you configured in ADFS for token signing;
  • [AD FS certificate thumbprint]: The thumbprint of the certificate mentioned above;
  • [AD FS SAML2 endpoint address]: The endpoint address for ADFS ending in "/adfs/ls"
  • [WebApi realm]: The realm you'll configure in ADFS by which your WebApi server can be identified, for instance "urn:services.mycompany.com:webapi". You can pick any valid URI for this setting but it's common practice to use a urn: name here.
With this, your service is ready to be published in IIS. Make sure to configure an HTTPS binding and use the address that you configured in [WebApi address] (or vice versa, of course). One last important thing you need to do, which is often overlooked, is to enable anonymous authentication in IIS. If you don't, the OPTIONS call that is part of the CORS pre-flight will not be accepted:

For your website running the WebApi service, select Authentication under the IIS header

Next, select Anonymous Authentication and click Enable

Stay tuned for a demonstration on how to configure the settings needed in ADFS itself to enable single sign-on for this WebApi service or another relying party.

2015-01-21

Calling a cross-domain web service using JavaScript and SAML2


When you have a client application and a webservice application but they both reside on different (sub-) domains, you'll want to link them using single sign-on, using for instance ADFS, or your user experience will suffer. Nowadays with OAuth2, this isn't too much of an issue. But when you're forced to work with SAML2 (for instance because your infrastructure is still at Windows Server 2008R2 and ADFS v2), things aren't that simple.

Since cookies may not be shared between domains as a security measure, one must find a way around this. Luckily, ADFS supports you in this. You just need to get JavaScript to do the same.

Starting with the actual call to a WebApi service. Assume the following JavaScript is gained from a secure part of a client application, residing on https://www.mycompany.com/:

var serviceUrl = "https://services.mycompany.com/api/Employee";

function makeCall() {
    return $.ajax({
        url: serviceUrl,
        type: 'GET',
        contenttype: 'application/json',
        dataType: 'json',
        xhrFields: { withCredentials: true },
        headers: {
            'X-Requested-With': 'XMLHttpRequest',
        },
        crossDomain: true
    });
};

Using these parameters, authentication when you're already logged in is automatically handled. And since the above snippet is retrieved from a secure site, that should be the case, right? Sadly, no. We're logged into the domain of the client application, but for this example we're calling a WebApi service on another sub-domain. Because of this, invoking makeCall() will result in a 401 (not authorized) error. So, the following line is added to the JavaScript code:

$.support.cors = true;

This tells JQuery we will be making cross-domain requests and to set up CORS pre-flight checks. Sadly, JQuery (or Angular, for that matter) doesn't handle the redirects that are inherent to ADFS authentication very well, nor does it handle the Set-Cookie headers that give you the FedAuth cookies you're after. The browser, however, is stellar at this. So we let the browser handle logging in, using an iframe:

var serviceUrl = "https://services.mycompany.com/api/Employee",
    loginUrl = "https://services.mycompany.com/api/Login",
    authenticating = false,
    retryCount = 5,
    iFrame;

function authenticate() {

    return $.Deferred(function (d) {
        // Potentially could make this into a little popup layer
        // that shows we are authenticating, 
        // and allows for re-authentication if needed

        if (!authenticating) {

            authenticating = true;

            iFrame = $("<iframe></iframe>");
            iFrame.hide();
            iFrame.appendTo("body");
            iFrame.load(function (data) {
                d.resolve();
            });
           
            iFrame.attr('src', loginUrl);

         } else {
            d.resolve();
        }
    });
};

function makeCall() {
    return $.ajax({
        url: serviceUrl,
        type: 'GET',
        contenttype: 'application/json',
        dataType: 'json',
        xhrFields: { withCredentials: true },
        headers: {
            'X-Requested-With': 'XMLHttpRequest',
        },
        crossDomain: true
    })
    .error(function (error) {

        // In IE10 we can get status = 0 in case of some errors, 
        // including 401 because of a bug...
        if ((error.status == 401 || error.status == 0)
            && retryCount > 0) {
            //Authenticating,

            retryCount--;

            return authenticate().then(function () {
                // Making the call again, just wait a bit 
                // for the login to complete. Better than to keep firing 
                // requests while the iframe is still busy

                setTimeout(function () { makeCall() }, 1000);
            });
        } else {
            return $.Deferred(function (d) {
                d.reject(error);
            });
        }
    })

    .success(function (data) {

        if (iFrame) {
            iFrame.remove();
            authenticating = false;
        }

        console.log(data);
    });
};

In this case, the service behind the LoginUrl simply returns an empty result, but does enforce authorization. This makes sure the authentication and authorization process is kicked off, but won't unnecessarily burden your server. If your ADFS server and webservice are configured correctly, the user won't be prompted for credentials again and will be logged in to the webservice automatically.

So blogging is a thing...

Over the years in my career as a software engineer, I've seen (and, even if I say so myself, made), some pretty cool bits of programming. Until now, I shared this knowledge with direct colleagues and they've taught me a lot in return. I decided to start this blog as a method of streamlining that process and tap into an even greater knowledge pool of like-minded people.

Recently, I've been diving into some pretty complicated technology and in the process of sharing my newly found knowledge, I noticed people were very interested, but quickly overwhelmed by what I had to tell them. Pointing them to my research notes only made them come back with even more questions. So, this blog will be about questions I get asked about security, AD FS, certificates, systems integration and of course practical applications and issues with technologies discussed, conveying what I've learned in (hopefully) easily digestible chunks. 

I don't claim to be all-knowing, but rather I hope to ignite a conversation between those that are interested in the technologies I will be blogging about and those that are already knowledgeable in them.

Lastly, this blog makes certain assumptions about prior knowledge. I use terms like cookies, web services, authentication and authorization, cross-domain security and the like without necessarily explaining them. I'll try to keep things as clear as can be, but this is definitely a blog aimed at software developers.

So here goes! Stay posted for the first entry about Cross-domain security, starting a bit backwards with a client JavaScript implementation. Following posts will be about other links in the chain, including creating a SAML-ready WebApi service, ADFS configuration, a custom attribute store and more.