Solution Architecture


While my primary goal in this appendix is to show you how to unify Windows Forms and ASP.NET 2.0 security, I also want to provide a general-purpose custom authentication and authorization infrastructure for Windows Forms. Such an infrastructure should not necessarily be coupled to ASP.NET 2.0 and should easily use any custom credentials store, such as an Access or LDAP database. The first step is to decouple the infrastructure from the actual credentials store by defining the IUserManager interface:

     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( ) is used to authorize the user when using role-based security. IUserManager also provides the Getroles( ) method, which returns all the roles a specified user is a member of. Getroles( ) is useful when caching role membership, discussed later.

Authenticate( ) is used by an abstract Windows Forms custom control called LoginControl. LoginControl is used similarly to its ASP.NET cousinyou add it (or rather, a subclass of it) to your Windows Forms application, and the LoginControl authenticates the caller. LoginControl obtains an implementation of IUserManager and authenticates using the Authenticate( ) method. If the user specified valid credentials, LoginControl creates 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 to authorize the user. This architecture is shown in Figure B-2.

Figure B-2. The LoginControl architecture


Both LoginControl and CustomPrincipal are defined in the WinFormsEx.dll class library assembly available with this book.

Note that CustomPrincipal never authenticates the userit 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. To enforce that, the CustomPrincipal class is an internal class, called only by LoginControl.

Implementing IPrincipal

The sole purpose of CustomPrincipal is to replace the Windows security principal and service the PrincipalPermissionAttribute and PrincipalPermission classes. CustomPrincipal should be installed only 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 (see Example B-1).

Example B-1. The 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);       AppDomain currentDomain = AppDomain.CurrentDomain;       currentDomain.SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal);       //Make sure all future threads in this app domain use this principal       //but because default principal cannot be set twice:       if(m_ThreadPolicySet == false)       {          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)       {          Predicate<string> exists = delegate(string roleToMatch)                                     {                                        return roleToMatch == role;                                     };          return Array.Exists(m_Roles,exists);       }       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:

     Thread.CurrentPrincipal = this;

To support logout 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 set the thread principal object only once for each app domain, Attach( ) first verifies that this has not already been done (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 be an expensive operation (e.g., by 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, there is a drawback: if you cache roles, CustomPrincipal will not detect changes to the roles repository, such as when a user is removed from a particular role. This is why the Attach( ) version that does not take a cacheRoles parameter defaults to no caching. You should use caching with caution, only when the allocation of users to roles is a relatively infrequent event and when the performance and scalability goals mandate it.

As I mentioned in Chapter 12, you should set the app domain principal policy for each app domain where role-based security is employed to PrincipalPolicy.WindowsPrincipal. To save the developer the need for doing so, Attach( ) sets the principal policy as well:

     AppDomain currentDomain = AppDomain.CurrentDomain;     currentDomain.SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal);

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:

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

Exists( )evaluates the method targeted by the predicate for each item in the array and returns 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.

The LoginControl Class

LoginControl provides two text boxes for capturing the username and password (see Figure B-3).

In addition, the control uses the ErrorProvider component to validate the user input. LoginControl will authenticate only if the user provides both a username and a password, and the error provider will alert the user of any missing input. Authentication takes place in the Click event-handling method for the Log In button. Example B-2 shows a partial listing of LoginControl, with some of the mundane code omitted in the interest of space.

Figure B-3. The LoginControl


Example B-2. Partial listing of LoginControl
 using LoginEventHandler = GenericEventHandler<LoginControl,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;    [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);       EventsHelper.Fire(LoginEvent,this,loginEventArgs);    }     protected abstract IUserManager GetUserManager(  ); }

Providing input to LoginControl

When using LoginControl, you need to provide it with the following information:

  • Which credentials provider to use

  • The application name

  • The role-caching policy

Subclasses of LoginControl are responsible for specifying the credentials provider. The subclasses need to override GetUserManager( ) and return an implementation of IUserManager.

For the second and third items, 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:

     [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.

LoginControl cannot be used on its ownit must be contained in another form or dialog, which can in turn be used by different applications with different application names. You have two options for supplying the application name: you can use the ApplicationName property to specify the name, or, if you do not know that name in advance (i.e., 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 no value is 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( ).

The CacheRoles property can be set to true or false; LoginControl simply passes it as-is to CustomPrincipal.Attach( ). Unaltered, CacheRoles defaults to false.

Authenticating the user

The OnLogin( ) method is called when the user clicks the Log In button. After validating the username 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 was successful, OnLogin( ) wraps a generic identity object around the username and attaches the custom principal. Note that OnLogin( ) never interacts directly with the custom principalall 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, perhaps it should close the hosting dialog, or move to the next screen in a wizard, or do something else. Since only the hosting application knows what to do after both successful and failed authentication, all LoginControl can do is inform it of whether the authentication succeeded by firing an event. To this end, LoginControl declares the delegate LoginEvent of the type GenericEventHandler<LoginControl,LoginEventArgs>. LoginEventArgs contains a Boolean property called Authenticated, which indicates the outcome of the authentication. To fire the login event to all interested subscribers, LoginControl uses defensive event publishing via EventsHelper.

LoginControl also provides two handy helpers. The static Boolean property IsLoggedIn allows the caller to query whether or not a user is logged in. LoginControl retrieves the current principal and checks if it is of the type CustomPrincipal. If the user is logged in, this of course will be the principal used. The static method Logout( ) allows the user to log out. Logout( ) retrieves the current principal and, if it is of the type CustomPrincipal, 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.



Programming. NET Components
Programming .NET Components, 2nd Edition
ISBN: 0596102070
EAN: 2147483647
Year: 2003
Pages: 145
Authors: Juval Lowy

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net