Monday 7 June 2010

N2CMS Meet VWT2OC: Day 2 – N2CMS Configuration

Yesterday went well and I’ve now got the basic N2CMS install working and the Forum Addon has been installed, so my objectives for today are:

  • Day 2 – N2CMS Configuration
    • Integrate the Membership Provider from n2cms/forum/legacy vwt2oc
    • Construct basic site structure (homepage/articles/forum/gallery/events calendar/etc)
    • Create VWT2OC basic theme

Integrating the Membership Provider

One of the first tasks I need to undertake is integrating the underlying security model.  Fortunately, n2cms implements the provider model (as does the forum add-on and my legacy VWT2OC site).  However, the implementations behind them are very different and each application has various customizations that need to be incorporated.  My aim for my integrated Membership Provider is:

  1. Provide all required features for n2cms and forum add-on
  2. Ensure that all of the user credentials from the existing site can work with the new one – I don’t want all the members to have to reset their passwords!

The major difference between the provided N2.Security.ContentMembershipProvider and the VWt2OC legacy implementation is that the n2 version stores passwords in clear text where VWT2OC uses hashes.  As a has is a one-way operation I can’t retrieve the existing passwords (only reset them), so I need to override several methods to implement hashing.

   1: using System;
   2: using System.Web.Security;
   3:  
   4: namespace VWT2OC.Website.Support.Security
   5: {
   6:     /// <summary>
   7:     /// Overrides the N2 MembershipProvider to support hashed passwords
   8:     /// </summary>
   9:     public class CustomMembershipProvider : N2.Security.ContentMembershipProvider 
  10:     {
  11:  
  12:         /// <summary>
  13:         /// Hashes the password.
  14:         /// </summary>
  15:         /// <param name="password">The password.</param>
  16:         /// <returns></returns>
  17:         internal string HashPassword(string password)
  18:         {
  19:             return HashPassword(password, Utility.GetSaltFromAssembly());
  20:         }
  21:         /// <summary>
  22:         /// Hashes the password.
  23:         /// </summary>
  24:         /// <param name="password">The password.</param>
  25:         /// <param name="salt">The salt.</param>
  26:         /// <returns></returns>
  27:         internal string HashPassword(string password, byte[] salt)
  28:         {
  29:             string sResult = password;
  30:             switch (PasswordFormat)
  31:             {
  32:                 case MembershipPasswordFormat.Hashed:
  33:                     sResult = Security.SimpleHash.ComputeHash(password, Security.HashType.SHA256, salt); //custom hashing wrapper
  34:                     break;
  35:                 case MembershipPasswordFormat.Encrypted:
  36:                     throw new NotImplementedException("Encrypted Passwords Not Implemented");
  37:             }
  38:             return sResult;
  39:         }
  40:  
  41:         /// <summary>
  42:         /// Gets the password format.
  43:         /// </summary>
  44:         /// <value>The password format.</value>
  45:         public override MembershipPasswordFormat PasswordFormat
  46:         {
  47:             get
  48:             {
  49:                 return MembershipPasswordFormat.Hashed;
  50:             }
  51:         }
  52:  
  53:         /// <summary>
  54:         /// Changes the password.
  55:         /// </summary>
  56:         /// <param name="username">The username.</param>
  57:         /// <param name="oldPassword">The old password.</param>
  58:         /// <param name="newPassword">The new password.</param>
  59:         /// <returns></returns>
  60:         public override bool ChangePassword(string username, string oldPassword, string newPassword)
  61:         {
  62:             N2.Security.Items.User u = Bridge.GetUser(username);
  63:             if (u == null || u.Password != HashPassword(oldPassword))
  64:                 return false;
  65:             u.Password = newPassword;
  66:             Bridge.Save(u);
  67:             return true;
  68:         }
  69:  
  70:         /// <summary>
  71:         /// Changes the password question and answer.
  72:         /// </summary>
  73:         /// <param name="username">The username.</param>
  74:         /// <param name="password">The password.</param>
  75:         /// <param name="newPasswordQuestion">The new password question.</param>
  76:         /// <param name="newPasswordAnswer">The new password answer.</param>
  77:         /// <returns></returns>
  78:         public override bool ChangePasswordQuestionAndAnswer(string username, string password, string newPasswordQuestion, string newPasswordAnswer)
  79:         {
  80:             N2.Security.Items.User u = Bridge.GetUser(username);
  81:             if (u == null || u.Password != HashPassword(password))
  82:                 return false;
  83:             u.PasswordQuestion = newPasswordQuestion;
  84:             u.PasswordAnswer = newPasswordAnswer;
  85:             Bridge.Save(u);
  86:             return true;
  87:         }
  88:  
  89:         /// <summary>
  90:         /// Validates the user.
  91:         /// </summary>
  92:         /// <param name="username">The username.</param>
  93:         /// <param name="password">The password.</param>
  94:         /// <returns></returns>
  95:         public override bool ValidateUser(string username, string password)
  96:         {
  97:             N2.Security.Items.User u = Bridge.GetUser(username);
  98:             if (u != null && u.Password == HashPassword(password))
  99:                 return true;
 100:             return false;
 101:         }
 102:  
 103:         /// <summary>
 104:         /// Resets the password.
 105:         /// </summary>
 106:         /// <param name="username">The username.</param>
 107:         /// <param name="answer">The answer.</param>
 108:         /// <returns></returns>
 109:         public override string ResetPassword(string username, string answer)
 110:         {
 111:             N2.Security.Items.User u = Bridge.GetUser(username);
 112:             if (u != null)
 113:             {
 114:                 string newPassword = System.IO.Path.GetRandomFileName();
 115:                 if (newPassword.Length > 7) newPassword = newPassword.Substring(0, 7);
 116:                 u.IsLockedOut = false;
 117:                 u.Password = HashPassword(newPassword);
 118:                 Bridge.Save(u);
 119:                 return newPassword;
 120:             }
 121:             return null;
 122:         }
 123:  
 124:         /// <summary>
 125:         /// Gets the password.
 126:         /// </summary>
 127:         /// <param name="username">The username.</param>
 128:         /// <param name="answer">The answer.</param>
 129:         /// <returns></returns>
 130:         public override string GetPassword(string username, string answer)
 131:         {
 132:             throw new NotImplementedException();
 133:         }
 134:  
 135:         /// <summary>
 136:         /// Creates the user.
 137:         /// </summary>
 138:         /// <param name="username">The username.</param>
 139:         /// <param name="password">The password.</param>
 140:         /// <param name="email">The email.</param>
 141:         /// <param name="passwordQuestion">The password question.</param>
 142:         /// <param name="passwordAnswer">The password answer.</param>
 143:         /// <param name="isApproved">if set to <c>true</c> [is approved].</param>
 144:         /// <param name="providerUserKey">The provider user key.</param>
 145:         /// <param name="status">The status.</param>
 146:         /// <returns></returns>
 147:         public override MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status)
 148:         {
 149:             N2.Security.Items.User u = Bridge.GetUser(username);
 150:             if (u != null)
 151:             {
 152:                 status = MembershipCreateStatus.DuplicateUserName;
 153:                 return null;
 154:             }
 155:             if (string.IsNullOrEmpty(username))
 156:             {
 157:                 status = MembershipCreateStatus.InvalidUserName;
 158:                 return null;
 159:             }
 160:             if (string.IsNullOrEmpty(password))
 161:             {
 162:                 status = MembershipCreateStatus.InvalidPassword;
 163:                 return null;
 164:             }
 165:             status = MembershipCreateStatus.Success;
 166:  
 167:             u = Bridge.CreateUser(username, HashPassword(password), email, passwordQuestion, passwordAnswer, isApproved, providerUserKey);
 168:  
 169:             MembershipUser m = u.GetMembershipUser(base.Name);
 170:             return m;
 171:         }
 172:      
 173:     }
 174: }
Pass the Salt!

Of course, being too clever for my own good, I didn’t use an unsalted hash…I generated the salt from the strong name key of the current assembly.  Which was fine for my green field application where all assemblies were strong names…not so good now where the chain of dependencies aren’t strongly named.  If you’re interested the algorithm is pretty simple:

   1: public static byte[] GetSaltFromAssembly()
   2: {
   3:     System.Reflection.Assembly ass = System.Reflection.Assembly.GetExecutingAssembly();
   4:     byte[] fullKey = ass.GetName().GetPublicKey();
   5:     byte[] salt = new byte[8];
   6:     for (int i = 0; i < salt.Length; i++)
   7:     {
   8:         salt[i] = fullKey[i];
   9:     }
  10:     return salt;
  11: }

To resolve, this I will need to obtain the salt and replicate it in the new assembly.  This time around I’m going to use a simpler mechanism and store the salt as a Base64 encoded string using the default Settings Class for the project.

Hooking It Up

Finally, I just need to wire up the Provider in the web.config:

   1: <membership defaultProvider="CustomMembershipProvider">
   2:     <providers>
   3:         <clear />
   4:         <add name="CustomMembershipProvider" type="VWT2OC.Website.Support.Security.CustomMembershipProvider, VWT2OC.Website.Support" />
   5:     </providers>
   6: </membership>
Integrating into the Forum

To fully integrate the forum into the site, a quick custom implementation of the AbstractN2ForumUser is in order:

   1: using System.Web;
   2: using N2;
   3: using N2.Security;
   4:  
   5: namespace VWT2OC.Website.Support.Security
   6: {
   7:     /// <summary>
   8:     /// Integrated the VWT2OC/N2CMS User into the Forum Addon
   9:     /// </summary>
  10:     public class ForumUser : N2.Templates.Forum.Services.AbstractN2ForumUser
  11:     {
  12:         ItemBridge bridge;
  13:  
  14:         /// <summary>
  15:         /// Gets the bridge.
  16:         /// </summary>
  17:         /// <value>The bridge.</value>
  18:         protected virtual ItemBridge Bridge
  19:         {
  20:             get { return bridge ?? (bridge = Context.Current.Resolve<ItemBridge>()); }
  21:         }
  22:  
  23:         /// <summary>
  24:         /// Initializes this instance.
  25:         /// </summary>
  26:         protected override void Initialize()
  27:         {
  28:             _isAuthenticated = false;
  29:             _userName = "";
  30:             HttpContext current = HttpContext.Current;
  31:             // Check wether the user is authenticated
  32:             if (current.User.Identity.IsAuthenticated)
  33:             {
  34:                 string username = current.User.Identity.Name;
  35:                 N2.Security.Items.User user = Bridge.GetUser(username);
  36:  
  37:                 // Get the data
  38:                 _userID = user.ID;
  39:                 _userName = username;
  40:                 _email = user.Email;
  41:                 
  42:                 _location = (current.Profile.Context[UserDetailKey.Location] as string) ?? string.Empty;
  43:                 _homePage = (current.Profile.Context[UserDetailKey.Website] as string) ?? string.Empty;
  44:                 _isAuthenticated = true;
  45:             }
  46:  
  47:         }
  48:     }
  49: }

This class will use the standard n2cms profile provider and User objects to fill in the blanks that the GenericForumUser provided with the Forum Addon is missing.

Construct Basic Site Structure

So, now comes the interesting bit – configuring the website structure for the first time and checking that the major features work.  But first things first, I need to switch from the default SQLite to SQLExpress as the Forum Addin will only work on MS SQL.

Fortunately, this is as simple as adding a new database to App_Data in visual studio and tweaking one of the provided connection strings (in web.config and again in yafnet.config) and firing up the website:

  1. When the website starts now, you’ll get a simple installation screen
    image
  2. Click to install a new database and log in using the default ‘admin/changeme’ credentials (unless you’ve changed them in the web.config file!)
  3. You’re now prompted to create the database tables by working through the installation step ‘tabs’
    image
  4. Test the connection on step 2
    image
  5. Click the ‘Create Tables’ button on step 3 to install the n2cms schema 
    image
  6. Select a theme…As the VWT2OC site is primarily yellow, I’ve gone with ‘Plain Orange Theme’ as it will be a simple modification
    image
  7. Restart the application on Step 5 and go to the management console (click on the managing link)
    image image
  8. Now to create an interim Administrator account, click on ‘Users’ in the top left and then ‘New’
    image
  9. That done I can now validate the modified Membership Provider by inspecting what’s been saved as the password for my new user.  This is in the n2Detail table (and it will be close to the bottom…)
    image
    You’ll have to trust me that that’s a hashed version of the password I entered!
  10. The final test is to log out and back in again!
  11. Now to remove the default administrator credentials from the web.config
       1: <authentication mode="Forms">
       2:     <forms loginUrl="n2/login.aspx" protection="All" timeout="30000" path="/">
       3:         <credentials passwordFormat="Clear">
       4:             <!-- WARNING: Change this default password. Please do it now. -->
       5: <!--
       6:             <user name="admin" password="changeme" />
       7: -->
       8:         </credentials>
       9:     </forms>
      10: </authentication>
  12. Now, to add a Forum underneath homepage so it appears in the top level navigation.  Right click on Home –> New and then select ‘Forum’.  Fill in the ‘Content’ and ‘Forum Settings’ as required
    image
    There are a couple of gotchas here:
    • Making sure the Custom Forum User is selected
    • The Forum Location is the location of the Addon files (should be left as default /Forum/YAF)
  13. When the forum is published we will be prompted to work through the installation wizard.
    1. If you hit a database connection problem, manually update the connection string in the yafnet.config file
    2. If you get ‘Cannot use full-text search in user instance’ error don’t worry…it’ll be fine apparently
  14. At the end of the wizard click ‘Finish’.  At this point, my installation erred as Session State had been disabled and the addon requires it. To enable it change the sessionState mode to InProc  in the web.config
       1: <!--<sessionState mode="Off" />-->
       2: <sessionState mode="InProc" />
  15. Now the forum should be installed
    image

‘Prettying’ It up

Now for a bit of light relief (ha ha) I’m going to spend some time merging the existing vwt2oc theme in with the plain orange n2cms theme.  This is pretty simple (on paper):

  • Copy the ‘Plain’ theme within App_Themes to VWT2OC
  • Tweak CSS
  • Switch the Site Theme to ‘VWT2OC’ by right clicking on ‘Home’ and editing the Layout
    image

It’s a shame my CSS skills are a bit lacking really….Oh well, it’ll be easy enough to change later.

image

No comments:

Post a Comment

Got something to say? Let it out then!
Comments are moderated, so it may take a while to for them to be displayed here!