Manage Custom Security Credentials the Smart (Client) Way : Page 3

By default, you can only manage the security credentials of the SQL Server database that ships with ASP.NET 2.0 using a local instance of Visual Studio 2005. This article shows how to extend the management capabilities by wrapping the ASP.NET 2.0 providers with a Web service and using a Windows Forms application to manage the credentials store.

by Juval Lowy

Dec 6, 2005

Page 3 of 4

WEBINAR:

On-Demand

Full Text Search: The Key to Better Natural Language Queries for NoSQL in Node.js

The AspNetSqlProviderService Web Service
The AspNetSqlProviderService class shown in Listing 4 implements the five Web interfaces. Doing so is just like implementing any other interfaceyou derive and implement the methods either implicitly or explicitly (as in Listing 4). You implement most of the methods on the Web interface by simply delegating the implementation to the appropriate methods on the providers. Each time, before using either Roles or Membership, you must prime them by setting the application name to use. For example, to implement IRoleManager.CreateRole(), you would write:

Some of the methods require a little bit of work before or after calling the providers. For example, you can only retrieve the user password if password retrieval is enabled, and AspNetSqlProviderService asserts that.

However, a few methods have no direct support at all by the providers. There are two possible workarounds: the first is to try and use other methods on the providers to accomplish the desired operation. The second is to execute directly against the aspnetdb database. Both approaches have pros and cons. For example, consider implementing the IMembershipManager.DeleteAllUsers() method. You can call the DeleteUser() method of MembershipProvider on each user in the application, as done in Listing 4. First you would call the IMembershipManager.GetAllUsers() method to get all the users of the application. Note the implicit cast from this reference to IMembershipManager. That is how you use the explicit implementation of interface methods by the class that implements the interface. Next you can define an anonymous method that deletes the user, assign that anonymous method to an Action<string> delegate, and use the static method ForEach<T>() of the Array class to delete each user.

The advantage of this first approach is that any internal activities related to deleting a user (such as deleting all role membership as well) are still performed. The disadvantage is that you make many more calls to the database, in which case it is paramount to do that under a single transaction as well.

One of the nicer features of ASP.NET 2.0 is its support for Web service interfaces.

As just mentioned, the second approach is to program against the aspnetdb database directly. This is most useful when the providers do not provide any means at all for accomplishing the task. For example, the providers do not support deleting an application, let alone all applications. While you can write a stored procedure that does that, another goal of mine was to leave aspnetdb untouched. Instead, the implementation of IApplicationManager.DeleteApplication() and IApplicationManager.DeleteAllApplications() use raw SQL commands. I've encapsulated these commands in the AspNetDbTablesAdapter helper class (not shown). The advantage of directly accessing the database is that you execute just one command. The disadvantage is that if the database schema were to change, you would need to change your code. Given that operations such as deleting all users or an application are relatively uncommon, and that the number of administrators is often low, I think it is best for the AspNetSqlProviderService to use the ASP.NET 2.0 providers as much as possible.

Configuring the Service
The settings in the Web.Config file used by the AspNetSqlProviderService Web service affects all applications it manages. In particular, settings such as password policies are common to all applications. The service uses the default provider (SQL Server), so there is no need to specify a provider or even a connection string, if the default connection string (maintained in machine.config) is adequate. If you require a different connection string, you need to include a connectionStrings tag (see Listing 5). In addition, in order to use the Roles class, you must enable role-based security by including this directive.

<roleManager enabled="true" />

Securing the Service
While the applications whose credentials the AspNetSqlProviderService Web service manages can be Internet or intranet based, the service itself is designed to be accessed by an administrator over the local intranet. You should both authenticate and authorize calls to the service. In addition, you should provide for payload privacy by encrypting the communication. This is required because the service deals with sensitive information such as user names and passwords. The easiest way of ensuring privacy is to use HTTPS. AspNetSqlProviderService verifies in its constructor via the static VerifySecureConnection() helper method that a secure connection was used. VerifySecureConnection() uses the IsSecureConnection property of the current request. To support development or other kinds of non-production deployment of the service, the VerifySecureConnection() method is decorated with the Conditional attribute. Only if the compilation symbol HTTPS is defined will the method have any affect. With regards to authenticating the user of the service, since the Web service is a local Intranet service, there is nothing wrong with using Windows authentication. I chose to use Integrated Windows authenticationthis will save the user from explicitly logging in. Another advantage of integrated authentication is that it hashes the credentials sent in a proprietary way.

Use a Windows Forms application to consume that Web service while providing a rich user interface and a comprehensive credentials management experience.

To configure Integrated Windows authentication, go to the AspNetSqlProviderService Web service properties under IIS, select the Directory Security tab, and click the Edit button. Uncheck the Anonymous access checkbox and make sure that the Integrated Windows authentication checkbox is checked. The AspNetSqlProviderService class is configured to demand authentication (see Listing 4)it uses the PrincipalPermission attribute, with the Authenticated property set to true.

[PrincipalPermission(SecurityAction.Demand,...,
Authenticated=true)]

Once the caller is authenticated by IIS, the service still runs by default under the configured identity in IIS. I wanted the service to run under the identity of the caller. For that to work, the Web.Config file (see Listing 5) contains an identity tag with the impersonate attribute set to true.

<identity impersonate="true"/>

Next, you need to use the SQL Server administration tools to allow the caller of the Web service to read and write from the aspnetdb database.

The non-trivial part of securing this Web service is authorization. I wanted to verify that only members of the Windows Administrators group can access the service. To that end, the PrincipalPermission attribute on the AspNetSqlProviderService class demands that only members of the Administrators role be allowed use of the service.

You can replace "Administrators" with any other groups the actual user of the service should be a member of.

The PrincipalPermission attribute uses the security principal attached to the thread to verify that the caller is indeed a member of the specified role. When relying on NT groups (such as Administrators) this mandates that you use an instance of WindowsPrincipal.

public class WindowsPrincipal : IPrincipal
{
public WindowsPrincipal(
WindowsIdentity ntIdentity);
public virtual bool IsInRole(string role);
//Rest of the members
}

The problem is that the AspNetSqlProviderService Web.Config file must enable role-based security in order to use the Roles class.

<roleManager enabled="true" />

This in turn causes ASP.NET 2.0 to attach a different principal to the HttpContext and the thread, the RolePrincipal class.

public sealed class RolePrincipal : IPrincipal
{...}

Trying to use RolePrincipal and demanding membership in the NT Administrators role will fail because it will access aspnetdb to look it up rather than Windows groups. To compensate for that, you have to manually swap the principals and attach an instance of WindowsPrincipal to the thread on every request. The easiest way of accomplishing that is to add a Global.asax file to the Web service project designating the Global class in the Global.cs file as the code behind class.

If the caller was authenticated, you need to instantiate a new WindowsPrincipal object and attach it to the current thread. The WindowsPrincipal constructor requires a WindowsIdentity instance. Fortunately, because the service is using Windows Integrated authentication, after successful authentication, the identity associated with the current HTTP context is already of the type WindowsIdentity, so you can just grab that instance.