Using the ASP.NET Providers to Secure WCF Services

| | Comments (5) | TrackBacks (0)

Windows Communication Foundation (WCF) services can be secured using the ASP.NET providers, an identity management system included with the second version of the .NET framework.  It supplies a way to storing user- and group-related information in SQL Server or Active Directory (AD).  As its name implies, its original use was with ASP.NET Web applications; however, the third version of the .NET platform has made it security capabilities available to WCF services as well. 

Just as with Web application, the ASP.NET providers allow WCF services to be locked down to specific users and/or groups of user (i.e., roles).   The toolkit allows operations (methods) of a service to be secured by applying attributes to them that indicate which users and/or groups are allowed to invoke them. 

The use of the ASP.NET providers furnishes WCF services with a number of attractive alternatives to the built-in security system of the OS.  For instance, it allows any user to login and use the service and not just those with an local account or one in the AD domain in which the service is hosted.  It also provides many other attractive benefits which will be pointed out in the following discussion.

The ASP.NET providers abstract the backend storage in which users, groups, passwords, and other such information is stored.  A provider in the context of this toolkit refers to a component with specialized capabilities for storing user data in one particular database.  It includes pre-built providers for SQL Server and AD.  Which one the application uses can be changed via configuration.  The use of the former will be detailed here. 

To begin using the ASP.NET SQL providers, the database in which they store their information must be created.  The initialization is handled by the tool aspnet_regsql.exe, which is installed with version 2 of the .NET framework.  This wizard can be run by invoking the following from the Visual Studio Command Prompt:

    C:\ >"%FrameworkDir%\%FrameworkVersion%\aspnet_regsql.exe" 

See http://msdn2.microsoft.com/en-us/library/x28wfk74.aspx for more information about the switches and options that this tool supports.  After completing the wizard, a new database called aspnetdb will be created on the specified SQL Server machine (which will be localhost unless aspnet_regsql is given another host name on the command line when started).  It will contain all the tables, stored procedures, and other infrastructure that the SQL providers need.  A nice schema diagram of this database can be found at http://msdn2.microsoft.com/en-us/library/aa478948.asp2prvdr0102l(en-us,msdn.10).gif.

After the underlying system is initialized, how does one populate it and begin managing its entities?  The methods to do so include the following: 

  1. The stored procedures created by aspnet_regsql.exe can be called directly;
  2. Juval Löwy has provided a feature-full utility he calls the Credentials Manger that is available with the source code of his book Programming WCF Services;
  3. a Web-based interface can be opened from any Web application project in Visual Studio that allows for the management of users and groups; and
  4. the SqlMembershipProvider and SqlRoleProvider classes expose APIs for creating, updating, viewing, and deleting entities within the aspnetdb database.

In my experience, options one, two, and three were not straightforward (option four wasn’t explored).  Specifically, after an hour of failed attempts, option one was abandoned without any success.  While Löwy’s explanation of the Credentials Manager in his book was impressive, configuring it was not possible in short order, so its use was also abandoned.  In the end, a new Web project was created in Visual Studio, and the Web Site Administration Tool was opened by selecting ASP.NET Configuration from the Website menu.  Once the Web-based configuration UI was opened, new roles and user were created and users were assigned to different roles.  After which, the Web application was discarded.  See http://msdn2.microsoft.com/en-us/library/yy40ytx0.aspx for more information about the Web Site Administration Tool. 

Using the ASP.NET providers requires special modification to be made to the code and configuration of the client and service.  Specifically, using this toolkit requires the following alterations:

  1. Operations must be secured by applying the PrincipalPermission attribute.  This attribute is also used when securing a method using integrated security provided by the OS; however, when using the ASP.NET providers, the domain name to which users/groups belong should not be provided.
  2. The message passed between the client and service must be encrypted using a public/private key that must be configured at both ends.  When using native Windows security, the messages between the client and service are secured as part of the communication protocol (NTLM and/or Kerberos).  This is not the case when using the ASP.NET providers because messages may originate from a non-Windows client making this unfeasible.

The first modification is applied to the class that implements the service contract.  For example, say that a service contract called ICalculator exposes an Add operation.  To prevent anyone but mangers from calling this method, the service class should be coded thus: 

    public class CalculatorService : ICalculator
    {
        [PrincipalPermission(SecurityAction.Demand, Role = "Managers")]
       
public double Add(double x, double y)
       
{
            return x + y;
        }
    }

The addition of the PrincipalPermission attribute to the Add operation (in bold) is the only coding change necessary in the service, in order to restrict all calls of it to managers. Two important things to note about this addition: 

  1. If NT groups are being used, the domain name or local machine name should be passed to the Role value of the PrincipalPermission attribute:

    [PrincipalPermission(..., Role = @"ServerMachine1\Managers")]

  2. The group names are hard-coded.  This is a real problem and must be avoided somehow in production environments.  I haven’t found a way to factor out this information into a configuration file, but one must be or else this system is unusable for many applications.

Once the code of the service has been updated, it must also be configured. 

The configurations that are needed are the enabling of the role manger and membership provider, the definition of the aspnetdb connection string (unless SQL Express is being used), the configuration of the binding, and the service behavior.  The role manger and membership provider are enabled by adding the following to the system.web section of the service’s config file (web.config if it is being hosted in IIS and app.config if it is being self-hosted):

    <roleManager enabled="true" defaultProvider="Foo">
        <providers>
            <add name="Foo"
                 type="System.Web.Security.SqlRoleProvider"
                 connectionStringName="qqq"
                 applicationName="/" />
        </providers>
    </roleManager>
    <membership defaultProvider="Bar" userIsOnlineTimeWindow="15">
        <providers>
            <clear/>
            <add name="Bar"
                 type="System.Web.Security.SqlMembershipProvider"
                 connectionStringName="qqq"
                 applicationName="/"
                 enablePasswordRetrieval="false"
                 enablePasswordReset="false"
                 requiresQuestionAndAnswer="false"
                 requiresUniqueEmail="true"
                 passwordFormat="Hashed" />
        </providers>
    </membership>

The addition of all this code was unfortunate in my view considering that the default configuration in machine.config is almost identical.  Despite different attempts, however no way could be found to avoid it.  After enabling these two components, the connection string had to be defined. 

Because the experiments were performed on the standard edition of SQL Server and not the express version, the default connection string defined in machine.config for the aspnetdb database was insufficient and a new one had to be defined.  This was done by adding the following to the connectionStrings element of the service’s configuration file:

    <add name="qqq"
        connectionString="Data Source=localhost;Integrated
            Security=SSPI;Initial Catalog=aspnetdb;"/>
 

The next configuration was to set the client credential type of the message security to username.  This was done by adding a binding element and assigning its name to endpoint of the service as follows:

    <endpoint address=""
        binding="wsHttpBinding"
        bindingConfiguration="Foobar"
        contract="Microsoft.ServiceModel.Samples.ICalculator" />
    <wsHttpBinding>
        <binding name="Foobar">
            <security mode="Message">
                <message clientCredentialType="UserName" />
            </security>
        </binding>
    </wsHttpBinding>

The last configuration that the service required was the definition of a custom behavior that set the username and password authentication mode and the service certificate information as seen in the following listing: 

    <behavior name="CalculatorServiceBehavior">    
       
<serviceCredentials>
            <userNameAuthentication
                userNamePasswordValidationMode="MembershipProvider" />
            <serviceCertificate storeLocation="LocalMachine"
                storeName="My"
                x509FindType="FindBySubjectName"
                findValue="localhost" />
        </serviceCredentials>          
       
<serviceAuthorization
            principalPermissionMode="UseAspNetRoles" />
     </behavior>

Once the service configuration was complete, the last thing that was needed before it could begin receiving requests from clients was the creation of the certificate.  This was done using a script included with a Microsoft WCF sample (on which the code of the experiment was derived).  It boiled down to these two commands: 

    makecert.exe -sr LocalMachine -ss MY -a sha1 -n CN=localhost –sky exchange –pe
    certmgr.exe -add -r LocalMachine -s My -c -n localhost –r CurrentUser -s TrustedPeople

Configuring and coding the client was much simpler than the service.  The configurations needed in the client were setting the credential type to username (as in the service) and setting the certificate validation mode to PeerTrust.  The configuration file used by the client in the experiments can be found in my stash.  Before calling the Add method of the service, the client had to provide its credentials.  To pass these to the server, the ClientCredentials property of the auto-generated proxy was used as follows: 

    // Create a proxy object using the auto-generated
    // CalculatorClient class.
    CalculatorClient client = new CalculatorClient();
    client.ClientCredentials.UserName.Password = "user4_user4";
    client.ClientCredentials.UserName.UserName = "user4";
    // Call the Add service operation.
    double result = client.Add(100.0, 15.99);

When the given username wasn’t in the managers role, the invocation of Add resulted in a MessageSecurityException.  When the user was, the operation succeeded. 

The ASP.NET providers offer an important alternative to NT and AD groups.  Their use is effective and poses few obstacles.  As the experiments were performed the most time consuming hang ups encountered were the configuration of the membership and role providers (due to a futile attempt to use the settings defined in machine.config), the need for a certificate to secure client/service communications, and the creation of new users and groups.  After overcoming these hurtles, the system was easy to use and work with.  Unsolved problems that were encountered include a simpler user/group management system and a way to avoid hard coding user and group information in method attributes.  If these complications can be solved, the system offers developers a fantastic way to secure their WCF services.

The complete source code used in this experiment can be found at http://travisspencer.com/stash/dotnet_providers/.