Stefan Goßner

Senior Escalation Engineer for SharePoint (WSS, SPS, MOSS, SP2010) and MCMS

Blogs

ASP.NET 2.0 and MCMS - creating a custom membership provider for MCMS

  • Comments 7
  • Likes

ASP.NET 2.0 ships with many new composite controls to make developing a web site very easy. I have covered a couple of controls related to site navigation including development a custom SiteMapProvider in my previous article.

Today we will cover controls related to role membership. As MCMS has it's own role system which cannot be extended and it's own user management system which does not allow to add or remove users from roles using API the ASP.NET controls that can be used with MCMS directly are limited.

Controls that can be used on a MCMS site are

  • the Login control
  • the LoginStatus control
  • the LoginUser control

The LoginUser controls shows the currently logged in user on the web site. The LoginStatus controls shows a Login or Logout link - depending if a user has logged in or not. Finally the Login control provides the necessary dialog elements to let a user login to a site. The Login control requires a configured Membership Provider that does the work behind the scenes to verfiy the user credentials.

To do this the Membership Provider needs to implement a ValidateUser() method which then checks if the credentials entered are valid. If yes, then the Login control will generate the ASP.NET authentication token based on the given credentials and redirects back to the page that called the login page.

A simple implementation of the ValidateUser method for MCMS would look as follows:

   public override bool ValidateUser(string strNTorADUser, string strPassword)
   {
       bool ValidUser = false;
       CmsAuthenticationTicket Ticket;

       // if we get a Ticket from AuthenticateAsUser the credentials are valid
       Ticket = CmsFormsAuthentication.AuthenticateAsUser(strNTorADUser, strPassword);

       if (Ticket != null)
       {
           ValidUser = true;
       }

       return ValidUser;
   }

So far so good. This works good to check if the user is valid and the Login control would now generate the ASP.NET authentication cookie. But for MCMS it is also necessary to generate a MCMS authentication cookie using CmsFormsAuthentication.SetAuthCookie based on the Ticket we just got in the ValidateUser method.

Ok, no problem you might say now. Let's set the cookie right in ValidateUser method. We would be able to do this but then we would give up some of the flexibility of the Login control.

The login control has an optional check box to let the user decide whether to remember him the next time he visits the site or not. The 3rd parameter of the CmsFormsAuthentication.SetAuthCookie method would allow us to set this option also for the MCMS authentication cookie.

The problem here is that the information about whether the user checked this box is not passed to the MembershipProvider. So we would have to hard code in this method whether the information should be persisted or not. To not confuse the user we would also have to hide the Remember Me check box of the login control. Not very nice.

Thanks god the Login control provides an LoggedIn event to allow us to call the SetAuthCookie after the ValidateUser method indicated that the credentials are valid.

We could register an LoggedIn event handler for the control and then call the SetAuthCookie method. Unfortunatelly we do not have the Ticket here! It was generated in the Membership Provider. No problem you could say: lets generate a new one based on the values entered by the user - in a similar way as we did in the membership provider.

The event would then look like this:

   protected void Login1_LoggedIn(object sender, EventArgs e) 
   { 
       Login myLogin = sender as Login; 

       CmsAuthenticationTicket Ticket;

       // if we get a Ticket from AuthenticateAsUser the credentials are valid
       Ticket = CmsFormsAuthentication.AuthenticateAsUser(myLogin.UserName, myLogin.Password); 

        if (Ticket != null)
        { 
            CmsFormsAuthentication.SetAuthCookie(Ticket, false, myLogin.RememberMeSet);
        }
    }

The bad thing here is that we have to duplicate the code of the Membership Provider here in the LoggedIn event. For the given scenario where the user directly had to enter valid user NT/AD user accounts in the format "WinNT://domain/user" this would be ok as the effort is very small. But what if the customer has custom authentication schema where he uses LDAP, AD/AM or accounts stored in SQL server to let his users login? Then it would be ugly to duplicate the code as well in the membership provider and in his LoggedIn event - especially if he would like to switch the between different MCMS Membership providers by just configuring a different one in the web.config.

It would be better to just pass the generated Ticket from the Membership Provider down to the Login control.

Unfortunatelly the Membership Provider modell of ASP.NET 2.0 does not allow this either. It seems the concept of account mapping has not been considered when the Membership Provider Concept has been designed.

So we need to implement a custom way to pass the Ticket from the Membership Provider down to the Login control. The easiest way to achieve this is to use the HttpContext.Current.Items collection which serves as a container for any kind of objects. These objects can be accessed by any other object during the lifetime of the current HttpContext object.

So what we have to do is to add the generated Ticket to the HttpContext.Current.Items collection in the Membership Provider and then retrieve it again in the LoggedIn event of the Login control:

   public override bool ValidateUser(string strNTorADUser, string strPassword)
   {
       bool ValidUser = false;
       CmsAuthenticationTicket Ticket;

       // if we get a Ticket from AuthenticateAsUser the credentials are valid
       Ticket = CmsFormsAuthentication.AuthenticateAsUser(strNTorADUser, strPassword);

       if (Ticket != null)
       { 
           // pass the CmsAuthenticationTicket to the Login Control 
           HttpContext.Current.Items.Add("MCMSTicket", Ticket); 


           ValidUser = true;
       }

       return ValidUser;
   }

and here is the event of the Login Control:

    protected void Login1_LoggedIn(object sender, EventArgs e)
    {
        Login myLogin = sender as Login;

        if (HttpContext.Current.Items.Contains("MCMSTicket"))
        {
            CmsAuthenticationTicket Ticket = HttpContext.Current.Items["MCMSTicket"as CmsAuthenticationTicket;
            CmsFormsAuthentication.SetAuthCookie(Ticket, false, myLogin.RememberMeSet);
        }
    }

Much more elegant, isn't it?

This method will now work with any MCMS Membership Provider that passes the generated Ticket in the HttpContext's Items collection with using the MCMSTicket key. To simplify the generation of MCMS Membership Providers I have now implemented an abstract base class which implements all the mandatory methods of a Membership Provider and also the ValidateUser method that adds the Ticket to the HttpContext.Current.Items collection:

    public abstract class MCMSMembershipProviderBase : System.Web.Security.MembershipProvider
    {
        public override bool ValidateUser(string strNTorADUser, string strPassword)
        {
            bool ValidUser = false;
            CmsAuthenticationTicket Ticket;
            Ticket = CmsFormsAuthentication.AuthenticateAsUser(strNTorADUser, strPassword);

            if (Ticket != null)
            {
                // pass the CmsAuthenticationTicket to the Login Control
                HttpContext.Current.Items.Add("MCMSTicket", Ticket);
                ValidUser = true;
            }

            return ValidUser;
        }

        public override bool EnablePasswordReset
        {
            get { throw new Exception("The method or operation is not implemented."); }
        }

        public override bool EnablePasswordRetrieval
        {
            get { throw new Exception("The method or operation is not implemented."); }
        }

        public override bool RequiresQuestionAndAnswer
        {
            get { throw new Exception("The method or operation is not implemented."); }
        }

        public override string ApplicationName
        {
            get
            {
                throw new Exception("The method or operation is not implemented.");
            }
            set
            {
                throw new Exception("The method or operation is not implemented.");
            }
        }

        public override int MaxInvalidPasswordAttempts
        {
            get { throw new Exception("The method or operation is not implemented."); }
        }

        public override int PasswordAttemptWindow
        {
            get { throw new Exception("The method or operation is not implemented."); }
        }

        public override bool RequiresUniqueEmail
        {
            get { throw new Exception("The method or operation is not implemented."); }
        }

        public override MembershipPasswordFormat PasswordFormat
        {
            get { throw new Exception("The method or operation is not implemented."); }
        }

        public override int MinRequiredPasswordLength
        {
            get { throw new Exception("The method or operation is not implemented."); }
        }

        public override int MinRequiredNonAlphanumericCharacters
        {
            get { throw new Exception("The method or operation is not implemented."); }
        }

        public override string PasswordStrengthRegularExpression
        {
            get { throw new Exception("The method or operation is not implemented."); }
        }

        public override MembershipUser CreateUser(string username, string password, string email, 
                        string passwordQuestion, string passwordAnswer, bool isApproved, 
                        object providerUserKey, out MembershipCreateStatus status)
        {
            throw new Exception("The method or operation is not implemented.");
        }

        public override bool ChangePassword(string username, string oldPassword, string newPassword)
        {
            throw new Exception("The method or operation is not implemented.");
        }

        public override string GetPassword(string username, string answer)
        {
            throw new Exception("The method or operation is not implemented.");
        }

        public override string ResetPassword(string username, string answer)
        {
            throw new Exception("The method or operation is not implemented.");
        }

        public override void UpdateUser(MembershipUser user)
        {
            throw new Exception("The method or operation is not implemented.");
        }

        public override MembershipUserCollection GetAllUsers(int pageIndex, int pageSize, out int totalRecords)
        {
            throw new Exception("The method or operation is not implemented.");
        }

        public override bool ChangePasswordQuestionAndAnswer(string username, string password, 
                        string newPasswordQuestion, string newPasswordAnswer)
        {
            throw new Exception("The method or operation is not implemented.");
        }

        public override bool UnlockUser(string userName)
        {
            throw new Exception("The method or operation is not implemented.");
        }

        public override MembershipUser GetUser(object providerUserKey, bool userIsOnline)
        {
            throw new Exception("The method or operation is not implemented.");
        }

        public override MembershipUser GetUser(string username, bool userIsOnline)
        {
            throw new Exception("The method or operation is not implemented.");
        }

        public override string GetUserNameByEmail(string email)
        {
            throw new Exception("The method or operation is not implemented.");
        }

        public override bool DeleteUser(string username, bool deleteAllRelatedData)
        {
            throw new Exception("The method or operation is not implemented.");
        }

        public override int GetNumberOfUsersOnline()
        {
            throw new Exception("The method or operation is not implemented.");
        }

        public override MembershipUserCollection FindUsersByName(string usernameToMatch, int pageIndex, 
                        int pageSize, out int totalRecords)
        {
            throw new Exception("The method or operation is not implemented.");
        }

        public override MembershipUserCollection FindUsersByEmail(string emailToMatch, int pageIndex, 
                        int pageSize, out int totalRecords)
        {
            throw new Exception("The method or operation is not implemented.");
        }

    }

As you can see lots of methods just throw an exception when called as these are related to things like User generation and password reset - things that are not relevant to the Login control and which would only makes sense for a custom authentication schema that supports these things.

A simple Membership Provider that accepts input from the Login control in the format "domain\username" and a valid NT/AD Password for this account would now look like the following:

    public class SimpleMCMSMembershipProvider : MCMSMembershipProviderBase
    {
        public override bool ValidateUser(string strName, string strPassword)
        {
            // here add code if you need custom authentication. E.g. use strName and strPassword to lookup in a 
            // SQL table 
or in a LDAP directory if the authentication is correct. 
            // Then set strLogin and strPassword to a valid 
NT/AD user for authorization. 
            string strNTLogin = "WinNT://" + strName.Replace("\\""/");

            return base.ValidateUser(strNTLogin, strPassword);
        }
    }

A Membership Provider that to validate against users in a SQL database table would look like this:

    public class SQLMCMSMembershipProvider : MCMSMembershipProviderBase
    {
        public override bool ValidateUser(string strName, string strPassword)
        { 
            string strNTLogin = GetMappedNTUserNameFromSQLDB(strName, strPassword);
            string strNTPassword = GetMappedNTUserPasswordFromSQLDB(strName, strPassword);
            return base.ValidateUser(strNTLogin, strNTPassword);
        }
    }

(you would need to implement the two methods to retrieve Name and Password of the mapped NT user from the SQL database)

Feel free to implement a LDAP MCMS Membership Provider similar to the method described by Spencer!

A final step in the implementation is now the registration of the desired MCMS Membership Provider in the web.config. To do this add the following section to your web.config right below the authentication section:

        <membership defaultProvider="SimpleMCMSMembershipProvideruserIsOnlineTimeWindow="30">
            <providers> 
                 <add name="SimpleMCMSMembershipProvider
                          type="StefanG.MembershipProviders.SimpleMCMSMembershipProvider, MCMSMembershipProvider
                          applicationName="/" />
            </providers>
        </membership>

Now as the Login control works correct with MCMS we need to ensure that the Logout function of the LoginState control also works correct. In the standard implementation it will only destroy the ASP.NET authenticaton cookie and not the MCMS authentication cookie. To enable this we need to register on the LoggedOut event of the LoginStatus control and also logout from MCMS:

   protected void LoginStatus1_LoggedOut(object sender, EventArgs e) 
   { 
       CmsFormsAuthentication.SignOut(); 
   }

The source code for the membership provider framework can be found on GotDotNet.

Comments
  • Now after MCMS 2002 SP2 has been released I would like to point you again to an article series I have...

  • Too complex. Tutorial is not very clear. Presumes way to much advanced ASP.NET prior knowledge.

  • This is exactly what I was looking for and could possibly help solve some items were dealing with.

    Thanks for the tutorial and the explanation behind it.

  • As some of you already noticed: GotDotNet is now down and the code samples previously hosted there have

  • PingBack from http://www.keyongtech.com/362781-how-to-set-up-cmslogin

Your comment has been posted.   Close
Thank you, your comment requires moderation so it may take a while to appear.   Close
Leave a Comment
Raw Html Fix