Security

Unify Windows Forms and ASP.NET Providers for Credentials Management

Juval Lowy

This article is based on prerelease versions of the .NET Framework 2.0 and Visual Studio 2005. All information contained herein is subject to change.

This article discusses:

  • .NET user credentials management
  • The ASP.NET 2.0 security providers model
  • Architecting custom security for Windows Forms
  • Code access security considerations for client applications
This article uses the following technologies:
Windows Forms, ASP.NET, Web services, C#

Code download available at:Security.exe(220 KB)

Contents

ASP.NET Security Infrastructure
Solution Architecture
Implementing IPrincipal
The LoginControl Class
Authenticating the User
The AspNetLoginControl
Implementing IUserManager
AspNetLoginControl and Code Access Security
The UserManager Web Service
The WSLoginControl
WSLoginControl and Code Access Security
The Sample Application
The LoginDialog Class
Conclusion

Windows® Forms intranet applications often resort to storing user credentials in a database even when deployed in a homogenous Windows environment. This stems from the limitations of using Windows accounts. For ASP.NET applications, the Microsoft® .NET Framework 2.0 provides out-of-the-box custom credentials management. You can easily authenticate users and authorize them, without ever having to use Windows accounts. This saves developers valuable time and effort, not to mention providing a high-quality, secure solution.

This article presents a set of interacting helper classes that enable a Windows Forms application to use the ASP.NET credentials management infrastructure, with the same ease as if it were an ASP.NET application. Doing so provides the productivity benefits of ASP.NET as well as a unified credentials store, regardless of the application user interface that is being employed. I'll also present some .NET programming techniques and various other design and programming best practices.

ASP.NET Security Infrastructure

Before showing you how to take advantage of the ASP.NET user credentials management, you need to learn a bit about it. Out of the box, ASP.NET applications can store their custom user credentials in either SQL Server™ or SQL Server Express. That said, the credentials management architecture is that of a provider model, and you can easily add other storage options, such as a Microsoft Access database. ASP.NET developers can configure their application directly from within Visual Studio® 2005. When selecting ASP.NET Configuration from the Web site menu, Visual Studio 2005 browses to the ASP.NET administration pages and allows you to configure various security parameters, as shown in Figure 1. Here you have access to the site management provider where you can select which store to use, such as a SQL Server or SQL Server Express database. The administration pages also allow you to create new users and delete existing ones, create new roles and delete existing ones, and allocate users to roles. You should note that the same database tables are used to store the user information from multiple ASP.NET applications. As a result, each user or role record is also associated with a particular application name.

Figure 1 ASP.NET Application Security Configuration

Figure 1** ASP.NET Application Security Configuration **

To use the SQL Server provider, run the setup file aspnet_regsql.exe found under \WINDOWS\Microsoft.NET\Framework\Version\. The setup program will create a new database called aspnetdb, which contains the tables and stored procedures required to manage the credentials.

At run time, ASP.NET will authenticate the callers using the credentials in the database. The easiest way to allow a user to provide the credentials is to drop a Login control on the Web Form. The Login control collects the user name and password from the user and authenticates using a class called MembershipProvider, defined as the following:

public abstract class MembershipProvider : ProviderBase { public abstract string ApplicationName{get;set;} public abstract bool ValidateUser(string name, string password); ... }

The goal of MembershipProvider is to encapsulate the actual provider used and the details of the actual data access, as well as to enable changing the membership provider without affecting the application itself. Depending on the configured security provider, the Login control will use a concrete data access class such as SqlMembershipProvider, which derives from the MembershipProvider class, when using SQL Server:

public class SqlMembershipProvider : MembershipProvider {...}

However, the Login control interacts only with the MembershipProvider base class. The Login control obtains the required membership provider by accessing the Provider static property of the Membership class, defined as:

public sealed class Membership { public static string ApplicationName{get;set;} public static MembershipProvider Provider{get;} public static bool ValidateUser(string userName, string password); ... }

The Membership class offers many members, which support every aspect of user management. Membership.Provider returns an instance of the configured provider based on settings in the Web application configuration file.

I'll be focusing on two members of the Membership class. The first is the ApplicationName property, which is used to set and retrieve the application name. The second is the ValidateUser method, which authenticates the specified credentials against the store, returning true if they match and false otherwise. The static Membership.ValidateUser method is shorthand for retrieving the configured provider from Membership.Provider and using its instance ValidateUser method.

You can also apply role-based security to authorize operations or access to resources. The aspnetdb database stores the mapping of users to roles. Once the user is authenticated, ASP.NET will set the User property of the HTTP context and the page to a custom security principal object called RolePrincipal:

public sealed class RolePrincipal : IPrincipal {...}

RolePrincipal uses the abstract class RoleProvider:

public abstract class RoleProvider : ProviderBase { public abstract string ApplicationName{get;set;} public abstract bool IsUserInRole(string username, string roleName); public abstract string[] GetRolesForUser(string userName); ... }

The ApplicationName property of RoleProvider binds the role provider to the particular application. The IsUserInRole method verifies the user's role membership. The GetRolesForUser method returns all the roles that a specified user is a member of.

Like the membership providers, depending on the configured security provider, RolePrincipal uses a corresponding data access class such as SqlRoleProvider to authorize the caller:

public class SqlRoleProvider : RoleProvider {...}

You can obtain the required role provider by accessing the Provider static property of the Roles class, defined as:

public sealed class Roles { public static string ApplicationName{get;set;} public static string[] GetRolesForUser(string username); public static bool IsUserInRole(string username, string roleName); public static RoleProvider Provider{get;} ... }

Both Roles.GetRolesForUser and Roles.IsUserInRole are shorthand, using the Roles.Provider property internally. Roles.Provider returns an instance of the configured provider based on settings in the Web application configuration file.

Solution Architecture

While my primary goal was to take advantage of the ASP.NET 2.0 credentials management infrastructure, I also wanted to provide a general-purpose custom authentication and authorization infrastructure for Windows Forms. Such infrastructure should not necessarily be coupled to ASP.NET 2.0, and could easily use any custom credentials store, such as Access or a Lightweight Directory Access Protocol (LDAP) database. The first step was to decouple it from the actual credentials store by defining the IUserManager interface, as follows:

public interface IUserManager { bool Authenticate(string applicationName,string userName, string password); bool IsInRole(string applicationName, string userName, string role); string[] GetRoles(string applicationName, string userName); }

The Authenticate method is used to authenticate the specified user credentials against the credentials store. IsInRole authorizes the user when you're using role-based security. IUserManager also provides the GetRoles method which returns all the roles of which a specified user is a member. GetRoles is useful when caching role membership, which I'll discuss later.

Authenticate is used by an abstract Windows Forms custom control called LoginControl. LoginControl is used in a similar fashion to its ASP.NET cousin in that you add it (or rather, a subclass of it) to your Windows Forms application. LoginControl obtains an implementation of IUserManager and authenticates the supplied credentials using the Authenticate method. If the user specified valid credentials, LoginControl instantiates an implementation of IPrincipal called CustomPrincipal. LoginControl provides CustomPrincipal with the implementation of IUserManager, and attaches CustomPrincipal to the current thread. The CustomPrincipal class can use the IsInRole or GetRoles methods of IUserManager for authorizing the user. This architecture is shown in Figure 2. Both LoginControl and CustomPrincipal are defined in the WinFormsEx.dll class library assembly, available with the code download for this article.

Figure 2 LoginControl Architecture

Figure 2** LoginControl Architecture **

You should note that CustomPrincipal never authenticates the user since it implicitly trusts LoginControl to do so. This means that you should not allow CustomPrincipal to be attached to a thread without going through valid authentication. In order to enforce that, the CustomPrincipal class is an internal class, called only by LoginControl. While an instance of CustomPrincipal could still be created using reflection, that would require a high level of trust. Thus I recommend that you follow the principles of least privilege and not grant the assembly the ReflectionPermission that would allow this to happen.

Implementing IPrincipal

The sole purpose of CustomPrincipal is to replace the Windows security principal and service the PrincipalPermissionAttribute and PrincipalPermission classes. CustomPrincipal should only be installed after successful authentication. To further enforce and automate this design decision, CustomPrincipal doesn't have a public constructor, so its clients have no direct way to instantiate it. Instead, the clients use the Attach public static method. Attach first verifies that the identity provided is that of an authenticated user:

Debug.Assert(user.IsAuthenticated);

If the user is authenticated, Attach creates an object of type CustomPrincipal, providing it with the security identity and the implementation of IUserManager to use, as well as with the role caching policy, shown in Figure 3.

Figure 3 CustomPrincipal Class

internal class CustomPrincipal : IPrincipal { IIdentity m_User; IPrincipal m_OldPrincipal; IUserManager m_UserManager; string m_ApplicationName; string[] m_Roles; static bool m_ThreadPolicySet = false; CustomPrincipal(IIdentity user,string applicationName, IUserManager userManager,bool cacheRoles) { m_OldPrincipal = Thread.CurrentPrincipal; m_User = user; m_ApplicationName = applicationName; m_UserManager = userManager; if(cacheRoles) { m_Roles = m_UserManager.GetRoles(m_ApplicationName,m_User.Name); } //Make this object the principal for this thread Thread.CurrentPrincipal = this; } static public void Attach(IIdentity user,string applicationName, IUserManager userManager) { Attach(user,applicationName,userManager,false); } static public void Attach(IIdentity user,string applicationName, IUserManager userManager,bool cacheRoles) { Debug.Assert(user.IsAuthenticated); IPrincipal customPrincipal = new CustomPrincipal(user,applicationName, userManager,cacheRoles); //Make sure all future threads in this app domain use this principal //In addition, because default principal cannot be set twice: if(m_ThreadPolicySet == false) { AppDomain currentDomain = AppDomain.CurrentDomain; currentDomain.SetThreadPrincipal(customPrincipal); m_ThreadPolicySet = true; } } public void Detach() { Thread.CurrentPrincipal = m_OldPrincipal; } public IIdentity Identity { get { return m_User; } } public bool IsInRole(string role) { if(m_Roles != null) { return Array.Exists(m_Roles, delegate(string roleToMatch) { return roleToMatch == role; }); } else { return m_UserManager.IsInRole( m_ApplicationName, m_User.Name,role); } } }

The constructor of CustomPrincipal saves the identity provided, as well as a reference to the previous principal associated with that thread. Most importantly, the constructor replaces the default principal by setting the CurrentPrincipal property of the current thread to itself, as shown in the following line of code:

Thread.CurrentPrincipal = this;

To support log-out semantics, CustomPrincipal provides the Detach method for detaching itself from the thread and restoring the old principal saved during construction.

In addition, Attach sets the default thread principal object to CustomPrincipal, so that it will be attached to new threads automatically. However, since you can only set the thread principal object once per app domain, Attach verifies first that it was not done already (it is possible to attach and detach the principal), using the static flag m_ThreadPolicySet:

if(m_ThreadPolicySet == false) { m_ThreadPolicySet = true; currentDomain.SetThreadPrincipal(customPrincipal); }

While authentication is a one-off cost, authorization can be a frequent operation. Since verifying role membership may potentially be expensive (such as when querying a database or calling a Web service), CustomPrincipal can cache all the roles the user is a member of, by saving the roles in the m_Roles member array. The constructor of CustomPrincipal takes a Boolean parameter called cacheRoles. If cacheRoles is true, the constructor will initialize m_Roles by calling the GetRoles method of the provided user manager. While this will enable almost instant role membership verifications, unfortunately the downside is that when caching roles, CustomPrincipal will not detect changes to the roles repository, such as removing the user from a particular role. Use caching only in cases when the allocation of users to roles is a relatively infrequent event and when the performance and scalability goals absolutely mandate it. This is why the Attach version that does not take a cacheRoles parameter defaults to no caching.

Implementing IPrincipal is straightforward. In its implementation of the Identity property, CustomPrincipal returns the saved identity. To implement IsInRole, CustomPrincipal checks if there are any cached roles. If so, it searches the roles array using the static Exists method of the Array type. Exists takes a delegate of type Predicate, defined as the following:

public delegate bool Predicate<T>(T t);

Exists will evaluate the method targeted by the predicate for each item in the array and will return true if a match is found. IsInRole initializes the predicate with an anonymous method that compares the role specified in the call to IsInRole with the role passed in as a parameter. If there are no cached roles, IsInRole delegates the query to the provided implementation of IUserManager, returning its results.

The LoginControl Class

LoginControl provides two textboxes for capturing the user name and password. In addition, the control uses the ErrorProvider component. LoginControl will only authenticate if the user provides both user name and password, and the error provider will alert the user to missing input. Authentication takes place in the Click event-handling method for the Log In button. Figure 4 shows a partial listing of LoginControl (with some of the mundane code omitted in the interests of space).

Figure 4 Partial Listing of LoginControl

using LoginEventHandler = EventHandler<LoginEventArgs>; public class LoginEventArgs : EventArgs { public LoginEventArgs(bool authenticated); public bool Authenticated{get;internal set;} } [DefaultEvent("LoginEvent")] [ToolboxBitmap(typeof(LoginControl),"LoginControl.bmp")] public abstract partial class LoginControl : UserControl { string m_ApplicationName = String.Empty; bool m_CacheRoles = false; public event LoginEventHandler LoginEvent = delegate{}; [Category("Credentials")] public bool CacheRoles //Gets and sets m_CacheRoles {...} [Category("Credentials")] public string ApplicationName //Gets and sets m_ ApplicationName {...} string GetAppName() { if(ApplicationName != String.Empty) { return ApplicationName; } Assembly clientAssembly = Assembly.GetEntryAssembly(); AssemblyName assemblyName = clientAssembly.GetName(); return assemblyName.Name; } static public void Logout() { CustomPrincipal customPrincipal = Thread.CurrentPrincipal as CustomPrincipal; if(customPrincipal != null) { customPrincipal.Detach(); } } static public bool IsLoggedIn { get { return Thread.CurrentPrincipal is CustomPrincipal; } } protected virtual void OnLogin(object sender,EventArgs e) { string userName = m_UserNameBox.Text; string password = m_PasswordBox.Text; /* Validation of userName and password using the error provider */ string applicationName = GetAppName(); IUserManager userManager = GetUserManager(); bool authenticated; authenticated = userManager.Authenticate( applicationName,userName,password); if(authenticated) { IIdentity identity = new GenericIdentity(userName); CustomPrincipal.Attach( identity,applicationName,userManager,CacheRoles); } LoginEventArgs loginEventArgs = new LoginEventArgs(authenticated); LoginEvent(this,loginEventArgs); } protected abstract IUserManager GetUserManager(); }

When using LoginControl, you need to provide it with the credentials provider to use, the application name, and the role caching policy. Specifying the credential provider is done by subclasses of LoginControl. The subclasses need to override GetUserManager and return an implementation of IUserManager.

LoginControl provides the properties ApplicationName and CacheRoles. Because LoginControl derives from UserControl, it natively integrates with the Windows Forms Designer. These two properties are available for visual editing during the design time of a form or a window that uses LoginControl. To enrich the Designer support, the properties are decorated with the Category attribute, as shown here:

[Category("Credentials")]

When you select the Categories view of the control properties in the Designer, these properties will be grouped together under the Credentials category. Subclasses of LoginControl can add their own properties to this category, too. The CacheRoles property can be set to true or false, and LoginControl simply passes it as-is to CustomPrincipal.Attach. Unaltered, CacheRoles defaults to false.

The LoginControl cannot be used stand-alone, but it must be contained in another form or dialog. That container can, in turn, be used by different applications with different application names. You therefore have two options for supplying the application name. You can simply use the ApplicationName property to specify the application name. Or, if you do not know that name in advance (if you're developing a general-purpose container), LoginControl can retrieve the application name from the Windows Forms entry application assembly used to launch the control. During authentication, if the LoginControl has no value set in the ApplicationName property, LoginControl will use the friendly name of the entry assembly as the application name. This logic is encapsulated in the private helper method GetAppName.

Authenticating the User

The OnLogin method is called when the user clicks the Log In button. After validating the user name and password, OnLogin calls GetAppName to retrieve the application name. It then calls GetUserManager to obtain an implementation of IUserManager. Authentication itself is done simply by calling the Authenticate method of the user manager. If authentication is successful, OnLogin wraps a generic identity object around the user name and attaches the custom principal. Note that OnLogin never interacts with the custom principal directly. All OnLogin does is provide it with the IIdentity object, the application name, the caching policy, and the implementation of IUserManager to use.

The question now is what LoginControl should do after authentication. The control has no knowledge of the required behavior of its hosting container. If authentication fails, perhaps it should present a message box, or perhaps it should throw an exception. If authentication succeeds, maybe it should close the hosting dialog, or move to the next screen in a wizard, or something else. Since only the hosting application knows what to do after both successful and failed authentication, all LoginControl can do is inform it by firing an event. LoginControl declares the event LoginEvent of the type EventHandler<LoginEventArgs>. LoginEventArgs contains a Boolean property called Authenticated, indicating the outcome of the authentication. To avoid a multithreaded race condition where another thread removes all subscribers from the LoginEvent just before publishing, LoginControl initializes LoginEvent with an anonymous method.

LoginControl also provides two handy helpers: the static Boolean property IsLoggedIn, and the static method Logout. IsLoggedIn allows the caller to query if a user is logged in. LoginControl retrieves the current principal and checks if it is of the CustomPrincipal type. If the user is logged in, this of course will be the principal used. The Logout method allows the user to log out. Logout retrieves the current principal, and if it is of the CustomPrincipal type, detaches it from the current thread. As explained previously, calling Detach on CustomPrincipal will also restore the previous principal. Note that if multiple threads are involved, you will need to log out on each one of them.

The AspNetLoginControl

Let's take a look now at the definition of my AspNetLoginControl, as shown here:

public partial class AspNetLoginControl : LoginControl { protected override IUserManager GetUserManager() { return new AspNetUserManager(); } }

AspNetLoginControl derives from LoginControl, and in its overriding of GetUserManager it returns my AspNetUserManager implementation of IUserManager, which uses the ASP.NET providers directly, as shown in Figure 5.

Figure 5 AspNetLoginControl

Figure 5** AspNetLoginControl **

AspNetUserManager is capable of using any valid ASP.NET 2.0 provider, hence its name. To use AspNetLoginControl, you will need to add to the application's configuration file the same values you would have placed in a Web application configuration file, indicating which provider to choose, as well as any provider-specific values and settings.

For example, to use the SQL Server providers, you must add the settings that are shown in Figure 6 to the application configuration file. The connection string value shown in Figure 6 is used to connect to the aspnetdb database on the local machine after default installation. Note the use of the enabled attribute of the roleManager tag to enable authorization.

Figure 6 Settings for AspNetLoginControl

<?xml version="1.0"?> <configuration> <system.web> <membership defaultProvider="AspNetSqlProvider" /> <roleManager enabled="true" defaultProvider="AspNetSqlProvider" /> </system.web> <connectionStrings> <remove name="LocalSqlServer" /> <add name="LocalSqlServer" connectionString=" data source=(local);Integrated Security=SSPI; Initial Catalog=aspnetdb" /> </connectionStrings> </configuration>

By default, the configuration file generated by Visual Studio 2005 uses NTFS security to enforce access permission security to the application configuration file, so only application administrators can modify the provider's settings.

Implementing IUserManager

AspNetUserManager contains as member variables instances of the ASP.NET membership and role providers. All the functionality of IUserManager is accomplished by delegating to the ASP.NET providers, as shown in Figure 7.

Figure 7 AspNetUserManager Class

class AspNetUserManager : IUserManager { public bool Authenticate(string applicationName, string userName, string password) { Membership.ApplicationName = applicationName; return Membership.ValidateUser(userName,password); } public bool IsInRole(string applicationName, string userName, string role) { Roles.ApplicationName = applicationName; return Roles.IsUserInRole(userName,role); } public string[] GetRoles(string applicationName, string userName) { Roles.ApplicationName = applicationName; return Roles.GetRolesForUser(userName); } }

To implement Authenticate, AspNetUserManager calls the ValidateUser method of Membership. To implement IsInRole and GetRoles, AspNetUserManager calls the IsUserInRole and GetRolesForUser methods of Roles.

One thing that you should be aware of is that my code is not thread-safe in the face of another thread changing membership.ApplicationName. As a result, the code shown here is fine as long as multithreaded access is not a requirement.

AspNetLoginControl and Code Access Security

AspNetLoginControl works well, and you can certainly use it, but it does have one important shortcoming. The ASP.NET providers were designed to be used on the server, and they require a few nontrivial permissions, including unmanaged code access permisions, unrestricted SQL Server client access permissions, and minimal ASP.NET Hosting permissions. Typically, server-side applications run in an elevated trust environment, potentially even full trust, and will have no problem obtaining those permissions and executing properly.

The AspNetLoginControl, on the other hand, is going to be used by Windows Forms applications. It is quite likely that such applications will be executing in a partial trust environment, perhaps as ClickOnce applications. The client environment may very well not grant the applications using AspNetLoginControl the permission they require to operate.

Figure 8 lists the permissions currently required for AspNetLoginControl and lists who demands them. These permissions are the product of both the control itself and the components it uses: CustomPrincipal and AspNetUserManager.

Figure 8 Security Permissions Required by AspNetLoginControl

Permission Type Specific Permission Value Demanded By
Security Execution Any managed application in order to run
Security Unmanaged code access The ASP.NET providers for their internal implementation
ASP.NET hosting Minimal The ASP.NET providers such that any party that hosts them has permission to do so
SQL Server client Unrestricted The ASP.NET providers for accessing the database
User interface Safe subwindows LoginControl in order to display itself
Security Control principal CustomPrincipal to be able to set principal policy

You have a number of options in choosing how to deal with these permission demands. Although you can grant full trust to the WinFormsEx.dll assembly and all its clients, this is inadvisable because it opens the way for the assemblies to be lured into doing things they are not supposed to do.

You can grant most of these permissions using the .NET Configuration tool to both the WinFormsEx.dll assembly and to every client application that wants to use it. You can even list these permissions in the ClickOnce application manifest. To ease the task of granting the permissions, the source files accompanying this article contain the WinFormsEx.xml file. This is a custom permission set file that you can add to the .NET Configuration tool and grant the necessary permissions.

However, the best solution is to avoid the permissions demanded by the ASP.NET providers altogether, and to use a different implementation of IUserManager, one that does not demand server-side permissions in a client environment. I'll present this option next. Also note that the ASP.NET team is investigating shortening this list of required permissions prior to the final product release.

The UserManager Web Service

The solution to the partial-trust problem is to wrap the ASP.NET providers with a Web service. When using a Web service, none of the security permission demands made by the providers will ever make their way back to the client.

Using a Web service also has the advantage of better scalability, since only the Web service will be using the connection to the database, rather than each individual client application. Another benefit of a Web service is that it avoids some potential security issues with clients authenticating themselves against SQL Server and with secure connection string management on the client side.

There are, however, a few downsides to using a Web service. For example, you should secure the communication between the clients and the Web service, because the clients will be sending credentials over the wire. This can easily be done using HTTPS. You also have the problem of additional call latency, which should be resolved using role caching. Authenticating against the Web service itself may be an issue, but this is mitigated if you can sustain anonymous access to the service in your intranet environment. Finally, authorizing the Web service calls may present problems. The Web service allows callers to retrieve role information about a user. Role membership information may be sensitive information in its own right authenticate. This can be dealt with by adding role-based security to the Web service and authorizing the callers. Note, though, that authorization requires authentication.

Using the WebServiceBinding attribute, you can define IUserManager as a Web service interface and implement it, as shown in Figure 9. The UserManager class in Figure 9 uses both Membership and Roles to obtain the configured providers from the Web service configuration file. Note that each Web service method of UserManager also accepts the application name to use, so that a single Web service can support multiple Windows Forms applications.

Figure 9 Implementing IUserManager on a Web Service

[WebServiceBinding] public interface IUserManager { [WebMethod(Description="Authenticates the user.")] bool Authenticate(string applicationName, string userName, string password); [WebMethod(Description="Verifies user role's membership.")] bool IsInRole(string applicationName, string userName, string role); [WebMethod(Description="Returns all roles the user is a member of.")] string[] GetRoles(string applicationName, string userName); } [WebService(Namespace = "https://SecurityServices", Description = "Wraps with a web service the ASP.NET providers. " + "This web service should be accessed over https.")] class UserManager : IUserManager { public bool Authenticate(string applicationName, string userName, string password) { if(HttpContext.Current.Request.IsSecureConnection == false) { HttpContext.Current.Trace.Warn( "You should use HTTPS to avoid sending cleartext passwords."); } Membership.ApplicationName = applicationName; return Membership.ValidateUser(userName,password); } public bool IsInRole(string applicationName, string userName, string role) { Roles.ApplicationName = applicationName; return Roles.IsUserInRole(userName,role); } public string[] GetRoles(string applicationName, string userName) { Roles.ApplicationName = applicationName; return Roles.GetRolesForUser(userName); } }

The WSLoginControl

WinFormsEx.dll contains the definition of WSLoginControl, shown in the following code snippet:

public partial class WSLoginControl : LoginControl { protected override IUserManager GetUserManager() { return new UserManager(); } }

WSLoginControl derives from LoginControl, and in its overriding of GetUserManager, it returns UserManager, a client-side Web service proxy class used to invoke the UserManager Web service. WSLoginControl can use any Web service that manages user credentials as long as it supports the IUserManager interface, hence its name. To generate the proxy class, add a Web reference to the UserManager Web service. Then you should change the machine-generated UserManager Web service proxy class to derive from IUserManager. Since the proxy class is a partial class and is machine-generated, it's preferable to add that code in a separate file:

partial class UserManager : IUserManager {}

WinFormsEx.dll already contains the definition of the UserManager Web service proxy class. The machine-generated code in the proxy class looks up the Web service address from the application configuration file. Add to the application configuration file under the appSettings section a key called UserManager whose value is the Web service address:

<?xml version="1.0"?> <configuration> <appSettings> <add key="UserManager" value="https://localhost/SecurityServices/UserManager.asmx"/> </appSettings> </configuration>

Figure 10 WSLoginControl and Its Supporting Classes

Figure 10** WSLoginControl and Its Supporting Classes **

Figure 10 shows WSLoginControl, CustomPrincipal, and their interactions with the UserManager Web service.

WSLoginControl and Code Access Security

Using a Web service keeps the ASP.NET providers permission demands on the server. The trade-off is that you will need to grant the clients Web access permission to connect to the user management Web service. You will also need to grant the rest of the permissions required by LoginControl and CustomPrincipal. Figure 11 lists the reduced permissions required when using a credentials management Web service.

Figure 11 Security Permissions Required by WSLoginControl

Permission Type Specific Permission Value Demanded By
Security Execution Any managed application in order to run
Web access Connect to the user manager Web service The UserManager proxy class to be able to use the Web service
User interface Safe subwindows LoginControl in order to display itself
Security Control principal CustomPrincipal to be able to set principal policy

You can grant all the permissions from Figure 11 using the .NET Configuration tool to both the WinFormsEx.dll assembly and to every client application that wants to use it. You can also use Visual Studio 2005 to list these permissions in the ClickOnce application manifest. For example, in Visual Studio 2005, go to the Security tab in the project settings of a ClickOnce application that uses WSLoginControl. Check the "Enable ClickOnce Security Settings" checkbox and the "This is a partial trust application" checkbox. Under "Zone your application will be installed from," select (Custom). This will remove all permissions except execution permission. Next, select SecurityPermission, and under Settings, select Include. Click Properties to bring up the Permission Settings dialog. Select assembly execution and principal control permission, and click OK. To grant permission to call the Web service, include the WebPermission, and in its properties, specify the UserManager's URL and allow the application to connect to it.

In a similar manager, grant the user interface permission that's listed in Figure 8. When you publish your ClickOnce application, its application manifest will include these permissions. You can even use Visual Studio 2005 to debug the application under these partial trust settings.

The Sample Application

Figure 12 shows a sample Windows Forms multi-document interface (MDI) application that uses WSLoginControl. Under its Security tab, the application requests the permissions listed in Figure 11. The application requires users to log in and authenticate themselves before creating a new document window. When the user selects the File | New menu item, it is handled by OnFileNew method, which demands that the user be a member of the Manager role:

[PrincipalPermission(SecurityAction.Demand,Role = "Manager")] void OnFileNew(object sender,EventArgs args) {...}

Before using the application, you will need to create the users and roles in the database. You can and should use the support Visual Studio 2005 offers for managing the credentials stored in the aspnetdb database.

Figure 12 Sample Application

Figure 12** Sample Application **

Create a new ASP.NET Web site. Select ASP.NET Configuration from the Web site menu to bring up the Web Site Administration pages, shown in Figure 1. Select the Provider tab and then click the "Select a single provider for all site management data" link. Under the Provider list, click AspNetSqlProvider. Next, click the Security tab. Under Users, click Select authentication type, and on the next page click From the Internet. Go back to the Security tab.

Under Roles, select Enable Roles, followed by Create roles. Add a new role called Manager, and go back to the Security tab. Under Users, click Create user, and provide the user name and password as well as the other requested information. Make sure to check the Manager checkbox under Roles to make the new user a member of the Manager role. Click the Create User button and close Visual Studio 2005 (unless you would like to add more users and roles). Visual Studio 2005 uses a forward slash ("/") for the application name by default.

The LoginDialog Class

The sample application provides the Security menu item. When the user selects Security | Log In, the app brings up the LoginDialog dialog. The code for LoginDialog is shown in Figure 13.

Figure 13 LoginDialog Class Uses LoginControl

partial class LoginDialog : Form { LoginControl m_LogInControl; bool m_Authenticated; public LoginDialog() { Authenticated = false; InitializeComponent(); } public bool Authenticated { get { return m_Authenticated; } protected set { m_Authenticated = value; } } void OnLogin(object sender,LoginEventArgs args) { bool successful = args.Authenticated; if(successful == false) { MessageBox.Show( "Invalid user name or password. Please try again", "Log In",MessageBoxButtons.OK,MessageBoxIcon.Hand); } else { Authenticated = true; Close(); } } static public void Logout() { LoginControl.Logout(); } static public bool IsLoggedIn { get { return LoginControl.IsLoggedIn; } } }

LoginDialog contains the WSLoginControl. When you open LoginDialog in the Windows Forms Designer, you can set the WSLoginControl properties. LoginDialog sets the ApplicationName property to "/" and CacheRoles to False. The application configuration files set the UserManager Web service address to https://localhost/SecurityServices/UserManager.asmx.

LoginDialog subscribes to the LoginEvent event of LoginControl, providing the dialog's OnLogin as the event-handling method, as shown here:

m_LogInControl.LoginEvent += OnLogin;

In the OnLogin event-handling method, LoginDialog alerts the user with a message box if the login attempt failed. If the login was successful, LoginDialog sets a public property called Authenticated to true, and closes itself. Authenticated is used by the client of LoginDialog to find out the authentication outcome. Authenticated will be false if the user closed LoginDialog without logging in. Note that Authenticated uses public get and protected set accessors to allow clients to retrieve the value, but not set it. The Security menu of the sample app also contains a Log Out option. The implementation calls the Logout static method of LoginDialog, which delegates to LoginControl.Logout, thus detaching CustomPrincipal from the current thread.

I also wanted to constantly inform the user of the login status. The sample application uses a timer to update its status bar. To find out if the user is logged in, on every timer tick event the application checks the value of the LoginDialog.IsLoggedIn static property and updates the status bar accordingly. LoginDialog.IsLoggedIn simply delegates to LoginControl.IsLoggedIn.

Conclusion

Providing a credentials management solution requires a holistic approach, which takes into account the application deployment and code access security needs, as well as scalability, extensibility, design-time integration, and reuse contexts.

Whatever the reason your Windows Forms application cannot use Windows accounts, you now have access to the well-designed ASP.NET 2.0 security credentials store, providing the latest best practices for credentials management. The LoginControl provides you not only with the same productivity-oriented programming model as ASP.NET but also with a unified credentials store. Your middle tier can easily use different front ends with the same authentication and authorization infrastructure.

Juval Lowy is a software architect providing .NET architecture consultation and advanced training. He is also the Microsoft Regional Director for the Silicon Valley. This article contains excerpts from his upcoming book, Programming .NET Components 2nd Edition (O'Reilly, 2005). Contact Juval at www.idesign.net.