Using Role-Based Security with Web Services Enhancements 2.0

 

Ingo Rammer
thinktecture

Updated January 2005

Applies to:
   Microsoft® .NET Framework
   Web Services Enhancements 2.0 SP1 for Microsoft® .NET
   WS-Policy specification

Summary: How to use Web Services Enhancements 2.0 SP1 for Microsoft .NET (WSE 2.0) to integrate X.509-based WS-Security authentication with role-based security features in the Microsoft .NET Framework. Highlights the use of WS-Policy in WSE 2.0 to greatly simplify tasks. (26 printed pages)

Download the associated sample code, wseRoleBasedSecurity_Samples.msi code.

Contents

Introduction
The Shortcut Route to Signed Messages
Using Declarative and Imperative Role-Based Security
Meet Identities and Principals
Let's Put it All Together
It's Just a Single Line of Code

Introduction

Microsoft .NET Framework and Microsoft ASP.NET support a number of security features for your code. So wouldn't it be great if you could just use a construct similar to HttpContext.Current.User.IsInRole() to guard access to your WSE-based Web service methods as well? In this article I will show you how to combine the ability in WSE 2.0 to sign and authenticate messages with the role-based permission system in the .NET Framework.

In conventional Web applications or Web services, you can simply rely on the means of authentication and encryption in IIS (SSL). You can, in this case, configure a directory in a way that requires the user to send logon credentials via the HTTP protocol by either using HTTP basic or Windows integrated security.

Using HTTP to authenticate your Web services requests might seem like a great idea in the beginning, but as soon as WS-Routing enters the game, the situation changes substantially: There is no direct HTTP connection between the sender and the ultimate recipient of the message anymore, but a potentially larger number different protocols which could be used along the routing path. This renders any means of transport-level security as a purely optional add-on that cannot guarantee the end-to-end integrity and security of your messages.

One means of providing these end-to-end services for Web services at large is to sign an outgoing message using an X.509 certificate according to the WS-Security specification.

The Shortcut Route to Signed Messages

You can obtain an X.509 certificate by using one of the well-known certificate authorities (CA) like Verisign, or simply by creating your own CA using Windows Certificate Services. After installing this service—which is an optional component of Windows 2000 Server or Windows Server 2003—you can point your browser to http://<servername>/certsrv to request the creation of a new certificate. After authorization of your request by a system administrator, you can add your newly created certificate to your private certificate store by using the same Web application at http://<servername>/certsrv. Please also make sure to add the certification authority's root certificate to your Web server's "Local Machine" store by manually selecting the store location (including "physical location") during the certificate import.

As soon as you created and obtained your X.509 certificate, you are ready to use it to sign your Web services requests. But wait! First, you have to enable your project to support Web Services Enhancements 2.0. After installing WSE 2.0 in .NET Developer mode, you can right-click on any project from within Visual Studio® .NET and choose WSE Settings 2.0 to open the dialog shown in Figure 1.

Figure 1. Enabling Web Services Enhancements 2.0

As soon as you select this check box, a number of different things happen. First, a reference to Microsoft.Web.Services.dll will automatically be added to your project. Second—and even more important—the behavior of the Add Web Reference ... and Update Web Reference commands will be changed in a way that allows you to later access the additional context properties. This for example makes it possible for you to alter the security tokens. This change is accomplished by creating a second proxy for each Web reference. The new generated proxy's name will have the added suffix "Wse" (instead of "MyService," the proxy would be called "MyServiceWse," for example) and the Framework will select a different base class for it: instead of inheriting from System.Web.Services.Protocols.SoapHttpClientProtocol as the non-WSE proxies do, the WSE proxies will extend Microsoft.Web.Services.WebServicesClientProtocol which contains a number of additional properties.

Using this new base class, you now have access to the WSE SoapContext and can add WS-Security tokens and signatures to your outgoing message. To further demonstrate the functionality of the role-based security extensions, I created the demo service shown here:

using System;
using System.Web.Services;
using System.Security.Permissions;

[WebService(Namespace="http://schemas.ingorammer.com/wse/role-based-security-extensions/2003-08-10")]
public class DemoService: System.Web.Services.WebService
{
   [WebMethod]
   public string HelloWorld()
   {
      return "Hello World";
   }
}

I then added a Web reference to the project, specified its URL Behavior as "dynamic," and created the following app.config file to specify the server's location and the certificate that should be used by the client. If you follow along with this example on your machine, keep in mind that you have to specify the name of a certificate that is contained in your private certificate store!

<configuration>
   <appSettings>
      <add key="RoleBasedSecurityClient.demosvc.DemoService" value="https://server/WSEDemo/DemoService.asmx"/>
      <add key="CertificateName" value="user1@example.com"/>
   </appSettings>
</configuration>

Web Services Enhancements 2.0 can now be used to create a binary security token based on an X.509 certificate. This token will be attached to the outgoing message and will be used to cryptographically sign it. In the following client-side code, I added the functionality to access the user's private certificate store and look for the certificate specified in the configuration file shown above.

using System;
// ...
using Microsoft.Web.Services2;
using Microsoft.Web.Services2.Security;
using Microsoft.Web.Services2.Security.Tokens;
using Microsoft.Web.Services2.Security.X509;
// ...

public static void Main(string[] args)
{
   String sto = X509CertificateStore.MyStore;

   // Open the certificate store 
   X509CertificateStore store = X509CertificateStore.CurrentUserStore(sto);
   store.OpenRead();

   // Find the certificate you want to use
   String certname = System.Configuration.ConfigurationSettings.AppSettings["CertificateName"];
   X509CertificateCollection certcoll = store.FindCertificateBySubjectString(certname);

   if (certcoll.Count == 0)
   {
      Console.WriteLine("Certificate not found");
   }
   else
   {
      X509Certificate cert =  certcoll[0];

      DemoServiceWse svc = new DemoServiceWse();
      SoapContext ctx = svc.RequestSoapContext;

      // Use the certificate to sign the message
      SecurityToken tok = new X509SecurityToken(cert);
      ctx.Security.Tokens.Add(tok);
      ctx.Security.Elements.Add(new MessageSignature(tok));

      // Invoke the web service
      String res = svc.HelloWorld();
   }

   Console.WriteLine("Done");
   Console.ReadLine();
}

Et voilà! You have just reached the end of the shortcut route to signed messages. If you want to take the long road with beautiful sights along the path, please check out Matt Powell's article on WS-Security Authentication and Digital Signatures with Web Services Enhancements for more information.

Alternative: Client-Side Policy

Instead of using the manual token-creation and message-signing process as illustrated above, you can also simply use a client-side policy to attach an X.509 certificate to every outgoing request.

To do this, open the WSE 2.0 properties of your client project, and navigate to the Policy tab. Select Enable Policy and click Add to add a policy for the default endpoint. In the next dialog, choose Secure a client application, and then select Require signatures (i.e., clear the Require encryption option). Confirm the selection of X.509 Certificate as the client token type and choose a matching certificate by clicking Select Certificate....

After completing this wizard and closing the WSE 2.0 Properties dialog, you will see that a policy cache file has been added to your client-side project. This policy file configures the WSE 2.0 client framework so that it will automatically include the selected certificate to every outgoing message without you having to write a single line of code.

Using Declarative and Imperative Role-Based Security

The message is signed. Now what? You could on one hand simply access the security token and its certificate at your server side [WebMethod] and check if it matches the one required to invoke the service. Even though this might seem practical at the beginning, you have to take into account that your application will need to support a number of users, each of which will have their own certificate. If your Web service infrastructure is more complex than the simple sample shown above, you will quite likely also have different roles for your users. Each of these roles will have different permissions so that some methods may only be called by users belonging to certain roles.

When implementing classic HTTP-based Web applications, you could simply access the HttpContext to check membership with Windows user groups or Active Directory groups as shown here:

[WebMethod]
public void AuthorizeOrder(long orderID)
{
  if (! HttpContext.Current.User.IsInRole(@"DOMAIN\Accounting"))
     throw new Exception("Only members of 'Accounting' may call this method.");

  // ... update the database
}

This syntax also allows you to check for finer grained permission sets that, for example, depend on the value of some parameters. In the following example, users in the role "HR" might call the method, but only members of the role "PointyHairedBoss" might set a salary higher than a certain amount:

public void SetMonthlySalary(long employeeID, double salary)
{
  if (! HttpContext.Current.User.IsInRole(@"DOMAIN\HR"))
     throw new Exception("Only members of 'HR' may call this method.");

  if (salary > 2000)
  {
    if (! HttpContext.Current.User.IsInRole(@"DOMAIN\PointyHairedBoss")) 
      throw new Exception("Only the pointy haired boss might set salaries larger than 2K");
  }

  // ... update the database

}

The code shown above relies on a standard ASP.NET feature—albeit one that has just a single but somewhat serious drawback in our scenario: It actually doesn't solve our requirement of using an end-to-end authentication scheme because it depends on HTTP authentication. A second downside is that the user's role membership is determined by looking at his Windows or Active Directory group membership. This would lead to a huge number of Windows groups whenever your application needs fine-grained permission levels—something which won't make your system administrators too happy.

A Custom Security Token Manager to the Rescue

Fortunately, WSE 2.0 provides necessary extensibility hooks that can be used for your own implementation of role-based security. It's easier than it might sound: you just have to provide the runtime with the necessary information to determine the role-membership of your users. After all, how should the Framework otherwise know that a message signed by "user1@example.com" has been sent from someone belonging to the PointyHairedBoss role?

To enable role-based security in this environment, you write a custom security token manager by deriving from Microsoft.Web.Services.Security.Tokens.X509SecurityTokenManager. The new token manager, which I'll present later, will read a configuration file like the one shown below to determine the mapping of certificates to roles:

<?xml version="1.0" encoding="utf-8" ?> 
<CertificateMapping>
  <Certificates>
    <!-- user1@example.com -->
    <CertificateMap Hash="f5 06 ba 1d 76 3b 59 1f ac 0c 3d ff e8 52 a3 41 44 b5 ed b1">
      <Roles>
        <Role>PointyHairedBoss</Role>
        <Role>Accounting</Role>
        <Role>HR</Role>
      </Roles> 
    </CertificateMap>
    <!-- user2@example.com -->
    <CertificateMap Hash="d7 fd 06 0d 43 7f 8f bb df a2 ee 9a 55 e4 c4 49 93 65 99 e4">
      <Roles>
        <Role>Accounting</Role>
        <Role>HR</Role>
      </Roles> 
    </CertificateMap>
  </Certificates>
</CertificateMapping>

In this configuration file, a <CertificateMap> entry has to exist for every certificate to which you want to assign roles. A certificate is identified by its fingerprint hash code that can be acquired, for example, by using the Windows Certificate Services management tool available at your certification authority (CA) by clicking Start>Administrative Tools>Certification Authority. When running this tool and selecting a certificate which you have issued before, you will see the window shown in Figure 2 that allows you to copy the hash code to the clipboard.

You can also access the same information for all certificates in the current user's certificate store by opening Internet Explorer and clicking Tools>Internet Options>Content>Certificates.

Figure 2. Certificate details

You can then use this hash code to create a new <CertificateMap> entry in the configuration file to specify the roles associated with the given certificate. This file will later be read by the XmlSerializer and will be deserialized to objects based on the following two classes:

public class CertificateMapping
{
   [XmlArrayItem(typeof(CertificateMap))]
   public ArrayList Certificates = new ArrayList();

   public CertificateMap this[String hash]
   {
      get
      {
         foreach (CertificateMap cert in Certificates)
         {
           if  (cert.CertificateHash.Replace(" ","").ToUpper() == hash.ToUpper()) 
             return cert;         
         }
         return null;
      }
   }
}

public class CertificateMap
{
   [XmlAttribute("Hash")]
   public String CertificateHash;

   [XmlArrayItem("Role", typeof(String))]
   public ArrayList Roles = new ArrayList();
}

Introducing the X509SecurityTokenManager

The token manager itself has to inherit from Microsoft.Web.Services.Security.Tokens.X509SecurityTokenManager. After registering this class with the Framework, each X509 binary security token which is contained in a request message will pass through this new class' AuthenticateToken() method before reaching your [WebMethod]. This method therefore gives you the possibility to create a matching IPrincipal object for each token, which finally allows you to hook back into the .NET security model.

To implement this security token manager, you first have to add references to the necessary namespaces:

using System;
using System.Collections;
using Microsoft.Web.Services2.Security;
using Microsoft.Web.Services2;
using Microsoft.Web.Services2.Security.Tokens;
using System.Security.Principal;
using System.Threading;
using System.Xml.Serialization;
using System.IO;
using System.Web;
using System.Security.Permissions; 

You can then override the AuthenticateToken() method to access the incoming security token and attach a matching IPrincipal to it.

namespace RoleBasedSecurityExtension
{
  [SecurityPermission(SecurityAction.Demand, 
     Flags=SecurityPermissionFlag.UnmanagedCode)]
   public class X509RoleBasedSecurityTokenManager: X509SecurityTokenManager
   {
    protected override void AuthenticateToken(X509SecurityToken token)
    {
      base.AuthenticateToken(token);
      token.Principal = new CertificatePrincipal(token.Certificate); 
    }
   }
} 

Meet Identities and Principals

The .NET Framework role-based security features are built on Principal and Identity objects that are assigned to a request context. A principal object contains the roles to which the current user belongs, whereas the identity object stores information about the user itself and how the user has been authenticated.

These two interfaces can be found in System.Security.Principal and look like this:

public interface IPrincipal 
{
    IIdentity Identity { get; }
    bool IsInRole(string role);
}

public interface IIdentity 
{
    string AuthenticationType { get; }
    bool IsAuthenticated { get; }
    string Name { get; }
}

To complete the role-based security extensions, I implemented a custom principal (CertificatePrincipal) and a custom identity (CertificateIdentity) which give access to role membership of the current user and to the certificate used for authentication.

The class' public constructor is invoked from the security token manager whenever an incoming message reaches your server, and takes an X509Certificate object as a parameter. It will either load the role mapping from the XML file if it has changed, or use the cached version that has been stored in a static variable. The filter then looks up the given certificate in the information loaded from the XML file. It checks if role membership information has been recorded for the given certificate. If this is not the case, an exception is thrown—or a corresponding identity-object is created.

To implement this principal, you first have to include the necessary namespaces:

using System;
using System.Xml.Serialization;
using System.IO;
using System.Security.Principal;
using Microsoft.Web.Services2.Security.X509;

The public constructor reads the XML file if necessary and checks if the sender's certificate has been configured in certmap.config. It will create a new CertificateIdentity object in this case and throw an exception otherwise.

  public class CertificatePrincipal: IPrincipal
  {
    private static CertificateMapping _map;
    private static DateTime _certmapDateTime = DateTime.MinValue;
      
    private CertificateMap _certmap;
    private CertificateIdentity _ident;

    public CertificatePrincipal(X509Certificate cert)
    {
      String file = System.Web.HttpContext.Current.Server.MapPath("certmap.config");

      // compare the file's date with the cached version
      FileInfo f = new FileInfo(file);
      DateTime fileDate = f.LastWriteTime;

      // reload if necessary
      if (fileDate > _certmapDateTime)
      {
        XmlSerializer ser = new XmlSerializer(typeof (CertificateMapping));

        using (FileStream fs = new FileStream(file,FileMode.Open,FileAccess.Read))
        {
          _map = (CertificateMapping) ser.Deserialize(fs);
        }

        _certmapDateTime = fileDate;
      }

      _certmap = _map[cert.GetCertHashString()];
      if (_certmap == null)
      {
        throw new ApplicationException("The certificate " + 
          cert.GetCertHashString() + " has not been configured.");
      }
      _ident = new CertificateIdentity(cert);
    }

The rest of the principal implementation provides a means to access the role membership configured in the XML file and a property to retrieve the Identity object:

  public bool IsInRole(string role)
  {
    return _certmap.Roles.Contains(role);
  }

  public System.Security.Principal.IIdentity Identity
  {
    get
    {
      return _ident;
    }
  }
}

The CertificateIdentity class contains a boilerplate implementation of an IIdentity with an additional property to access the X509Certificate object that has been used to sign the message.

public class CertificateIdentity: IIdentity 
{

  private X509Certificate _x509cert;

  internal CertificateIdentity(X509Certificate cert)
  {
    _x509cert = cert;
  }

  public bool IsAuthenticated
  {
    get
    {
      return true;
    }
  }

  public string Name
  {
    get
    {
      return _x509cert.GetName();
    }
  }

  public string AuthenticationType
  {
    get
    {
      return "X.509";
    }
  }

  public X509Certificate Certificate
  {
    get 
    {
      return _x509cert;
    }
  }
}

Let's Put it All Together

The last step in completing the desired functionality is to register the newly created token manager with the server application. After installing Web Services Enhancements 2.0, you can do this in two different ways. You could either directly edit the web.config file, or you could use the WSE 2.0 settings tool that I introduced in Figure 1.

To use the WSE 2.0 settings tool in Visual Studio .NET, right-click a Web Service Project and choose WSE Settings 2.0. You will be presented with the dialog shown in Figure 3. Make sure that both check boxes are selected.

Figure 3. Enabling WSE and the extended processing pipeline for Web services projects

You can then switch to the Security tab to specify the name of your token manager as shown in Figure 4.

Figure 4. Add a new token manager and verify the certificate settings.

To add a new token manager, click Add... and fill in the data as shown in Figure 5. Please note that the Type entry has to be specified in the format

<namespace>.<classname>, <assemblyname>

that leads to the example's full type name of RoleBasedSecurityExtension.X509RoleBasedSecurityTokenManager, RoleBasedSecurityExtension. The ValueType has to be set according to the WS-Security specification. The correct value in our case is http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3

Figure 5. Adding a new token manager

The final step is to configure a policy file on the Policy tab as shown in Figure 6. We'll look at the contents of this policy file shortly.

Figure 6. Setting the policy file name

The combination of the security token manager, the policy file, the custom principal, and the custom identity object allows you to use role-based security in a purely descriptive way by employing WS-Policy.

To enable automatic checking of incoming messages and their X.509 certificate, click Add in the dialog above to create a policy for the default endpoint. In the following dialog, you have to choose to Secure a service application. Click Next, make sure that Require Signatures is selected for the request messages. (Depending on your requirements you can clear the Require Encryption option for the response message.)

In the following screen, confirm X.509 Certificate as the client token type, leave the Trusted Certificates list in the following screen empty, and confirm and close the WSE Security Setting Tool.

It's Just a Single Line of Code

At the beginning of this article, I showed you the two different ways of using role-based security in classic ASP.NET applications: one was based on explicit code to check for roles, and the other was based on the use of declarative security with .NET attributes to specify a method's security needs.

In WSE 2.0-enabled Web service applications you have two similar possibilities. The major difference is that we won't use .NET attributes to designate method's security needs, but will instead rely on the WS-Policy features. The reason for making this choice is that policy offers one compelling advantage: it's an XML file which you can ship to the developer who is implementing a client so that he can determine upfront which requirements need to be fulfilled for each Web service.

But let's start with code based role membership checks. Unfortunately, the easy way of just using a construct similar to SomeContext.Current.User.IsInRole() is not possible anymore, because there might be more than one security token in an incoming SOAP message. What I've chosen to do instead is to provide two helper methods whereas the first one checks the current request's security context and looks for an X509SecurityToken that has been used to sign the complete body of the incoming message. The second helper is a small wrapper to provide a programming interface as easy as using SomeContext.Current.User.IsInRole().

public X509SecurityToken GetBodySigningToken(Security sec) 
{ 
  X509SecurityToken token = null; 
  foreach (ISecurityElement securityElement in sec.Elements) 
  { 
    if (securityElement is Signature) 
    { 
      Signature sig = (Signature)securityElement; 
      if ((sig.SignatureOptions & SignatureOptions.IncludeSoapBody) != 0) 
      { 
        SecurityToken sigToken = sig.SecurityToken; 
        if (sigToken is X509SecurityToken) 
        {
          token = (X509SecurityToken)sigToken;
        }
      } 
    } 
  } 
  return token; 
} 

private bool CurrentCertificatePrincipalIsInRole(String role)
{    
  X509SecurityToken tok = GetBodySigningToken(RequestSoapContext.Current.Security);
  if (tok == null) return false;
  if (tok.Principal == null) return false;
  return tok.Principal.IsInRole(role);
}

You now can use these helpers in your [WebMethod] with just a single line of code to check the role membership of the sender's X.509 certificate:

[WebMethod]
public void SetMonthlySalary(long employeeID, double salary)
{
  if (! CurrentCertificatePrincipalIsInRole  ("HR"))
    throw new Exception("Only members of 'HR' may call this method.");

  if (salary > 2000)
  {
    if (! CurrentCertificatePrincipalIsInRole("PointyHairedBoss")) 
      throw new Exception("Only the pointy haired boss might set " + 
        "salaries larger than 2K");
  }

  // ... actual work removed ...

}

Checking Roles With Server-Side Policy

The final missing part is the use of a policy file to declaratively specify the security needs for your Web service. To add this "claim" to the policy file, you first have to open this file, which has been automatically added to your project in the previous step. This file is usually called policyCache.config.

In this file, look for a profile with the Id "Sign-X.509". Two levels undern

eath this <Policy> element, you will find a <wssp:SecurityToken> tag. You have to add a sibling node to <wssp:TokenType> that specifies the required claim. (A WS-Profile claim is essentially a "requirement" which a certain client-side token has to fulfill; in this case the membership in a certain role.)

The claim in our case is for <wse:Role> with a value of Accounting to ensure that only members of this group can invoke this service. The complete <SecurityToken> element therefore as to look like this:

<wssp:SecurityToken wse:IdentityToken="true">
  <wssp:TokenType>http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3</wssp:TokenType>
  <wssp:Claims>
    <wse:Role 
      value="Accounting" 
      xmlns:wse="https://schemas.microsoft.com/wse/2003/06/Policy" />
  </wssp:Claims>
</wssp:SecurityToken>

These settings will make the Framework check all incoming messages for the security requirements mentioned in the policy file. If for example the incoming message has not been signed, or if the certificate has not been assigned to the role Accounting, a SOAP fault will be returned to the client, telling it that the incoming message does not conform to the policy. Please note: if you receive an exception telling you that the certificate's trust chain could not be verified, then your custom certificate authority's root certificate has not been installed in the "Local Machine" store. To do so, please head to http://<your_certificate_server>/certsrv and import the root certificate by manually selecting the import location and the physical import location in the Import Certificate wizard.

Summary

In this article, you've seen how you can create and use a custom security token manager with the Web Services Enhancements 2.0 for Microsoft .NET to check for X.509 certificates, map them to roles and populate context information with custom principal and identity objects. You have also seen how easy it is to use WS-Policy from within Visual Studio .NET to add declarative checking of role membership to your applications. The advantage of this approach based on WS-Security when compared to classic HTTP based security is that it doesn't rely on transport-level integrity or security but instead works solely with the SOAP message. This provides you with end-to-end security capabilities over multiple hops and protocols.

About the author

Ingo Rammer is co-founder of thinktecture, a company providing in-depth technical consulting and training services for software architects and developers. Ingo is a world-renowned expert for design and development of distributed applications, and he provides architecture, design, and architecture review services for teams of all sizes.

Apart from his consulting services, he is a regular speaker at developer conferences around the world, and has authored two best-selling books, Advanced .NET Remoting and Advanced .NET Remoting in VB.NET (Apress). Ingo is the Microsoft Regional Director for Austria, and was recently awarded the Microsoft MVP status of Solution Architect. You can reach Ingo at http://www.thinktecture.com/staff/ingo.