Extending DNN by forking an intrinsic DNN provider
Dot Net Nuke (DNN) as a CMS is developed for expansibility and the core framework is designed to make this achievable. This post does discuss extensibility in detail. It is hoped that it will be found beneficial for analysts or technology advisors whom are considering DNN for porting their businesses on DNN for ease of administration.
It is more or less a practice while developing on top of DNN, not to alter the source code often regarded as following,
“DNN’s core will remain un-altered during the course of development for this project”
Then how do you exactly achieve extensibility if you don’t really change the core? DNN heavily utilize the concept of providers from data access to navigation. As a developer or an architect, if you are restricted by an intrinsic feature of DNN, what you can do is to develop on top of existing framework by forking an existing provider.
By forking we mean that you take the source code of that particular provider (since that is redistributable as being open source), duplicate that yet following the standard inheritance chain in set of classes and create a new provider project. Then you simply plug the new provider entry in the web.config file.
Here we will consider a simple case of forking DNN’s sitemap provider. We only want to filter the Urls listed in sitemap of which parent Tab (DNN’s pages are termed tabs) are of type Url. This is pretty simple and will explain the concept of forking.
At the time of this writing the latest version of DNN’s (5.6.2 beta) sitemap provider can be found at,
http://dotnetnuke.codeplex.com/SourceControl/changeset/view/58582#608945
Duplicate this class in your own provider project. To explain that DNN’s default language (VB) doesn’t limit the extensibility during forking process, I have done so in a C# project,
namespace DNN.Providers.SampleSitemapProvider
{
public class TestSitemapProvider : DotNetNuke.Services.Sitemap.SitemapProvider
{
…
The only change we will have to bring in the code of this file to filter the Urls as per our need will be in method,
private SitemapUrl GetPageUrl(TabInfo objTab, string language)
{
…
Just before the line that says,
pageUrl.Priority = GetPriority(objTab);
place a method call,
//Filter Url
if (!CheckParentTabType(objTab))
{
return null;
}
And introduce a private method in the very file,
private bool CheckParentTabType(TabInfo objTab)
{
try
{
if (objTab != null)
{
if (objTab.ParentId != -1)
{
TabController tabController = new TabController();
TabInfo parentTabInfo = tabController.GetTab(objTab.ParentId);
if (parentTabInfo != null)
{
if (parentTabInfo.TabType == TabType.Url)
{
return true;
}
}
}
}
catch (Exception ex)
{
DotNetNuke.Services.Exceptions.Exceptions.LogException(ex);
}
return false;
}
That is all. Verify the build output path points to bin folder and bring following change in provider section for sitemap for this new provider to trigger when sitemap.aspx page is requested,
<sitemap defaultProvider="coreSitemapProvider">
<providers>
<clear />
<add name="coreSitemapProvider" type="DotNetNuke.SitemapProviders.CoreSitemapProvider, DotNetNuke.SitemapProviders.CoreSitemapProvider" providerPath="~\Providers\MembershipProviders\Sitemap\CoreSitemapProvider\" />
</providers>
</sitemap>
So we will only have to add a new node in this section as following,
<add name="sampleSitemapProvider" type="DNN.Providers.SampleSitemapProvider.TestSitemapProvider, TKCarsites.Providers.SampleSitemapProvider" providerPath="~\Providers\MembershipProviders\Sitemap\CoreSitemapProvider\" />
And flip the default sitemap entry from
<sitemap defaultProvider="coreSitemapProvider">
to
<sitemap defaultProvider="SampleSitemapProvider">
For our sitemap to take effect. Off course for a production deployment this makes more sense to be performed using manifest file config section. The complete project is attached for quick review.
Cannot unbox 'currentUser.ProviderUserKey' as a 'System.Guid' System.Guid
Today I was testing MySql membership provider using MySql data connector 6.2.4. While the setup for running form based authentication on top of MySql membership was fairly straight forward, something that surprised me for a bit was to get an unexpected exception when I tried to read the ProviderUserKey,
MembershipUser currentUser = Membership.GetUser();
Guid currentUserId = (Guid)currentUser.ProviderUserKey;
at second line I got the exception, "Cannot unbox 'currentUser.ProviderUserKey' as a 'System.Guid' System.Guid". Why would that be I thought when ProviderUserKey is object type?
Looking closely I identified that MySQLMembershipProvider offers Integer type ProviderUserKey as shown below,
Why is that? Perhaps becuase MySql doesn't offer an MS SQL server's UniqueIdentifer equivelent data type intrinsically out of box and unqie identification is achieved by function UUID() as given here,http://dev.mysql.com/doc/refman/5.1/en/miscellaneous-functions.html#function_...
Decrypt aspnetMembership Provider based DNN Passwords
The brief steps are as following,
1- Create a new website (I am using C# here) in Visual Studio2- Inside the web.config file, add the machineKey tag and provide “validationKey” and “decryptionKey” as found from web.config file of your running DNN site using which passwords are stored. (The keys provided here will work with the password as given in step 5)<machineKey validationKey="5D47DA8BBE8C9D02378BC3360FD6724A43C69016" decryptionKey="F5292CB499D6A71955A7B389BFBF3712D0A48D1971DEE889"
decryption="3DES" validation="SHA1" >3- Create a new class and name it, “RecoverPassword.cs”4- Copy paste the following code inside the above created class. This way we inherit our class from aspnet membership provider pattern class and thus will use its decryption procedure to decrypt the passwords. If you want to do that manually, do remember that Ctrl+K+M is a nice shortcut to implement base class methods which saves quite an effort from your end.using System;using System.Configuration.Provider;
using System.Text;
using System.Web.Security;public class RecoverPassword : MembershipProvider { //Create a static instance of this class as a singelton private static readonly RecoverPassword _instance = new RecoverPassword(); public override MembershipPasswordFormat PasswordFormat {
get {
return MembershipPasswordFormat.Encrypted;
}
} public static string RecoverEncryptedString(string target)
{
try
{
// Decode the password in Base64
byte[] data = Convert.FromBase64String(target);
//Get advantage of the DecryptPassword method
byte[] decryptedPassword = _instance.DecryptPassword(data);
string encodedPassword = Encoding.Unicode.GetString(decryptedPassword);
// Remove the salt value prepended to the value
// Salt value doesn''t do anything more than being appended to thte password string, just strip it out
return encodedPassword.Substring(8);
}
catch (ProviderException ex)
{
throw ex;
}
} public override string ApplicationName
{
get
{
throw new NotImplementedException();
}
set
{
throw new NotImplementedException();
}
} public override bool ChangePassword(string username, string oldPassword, string newPassword) {
throw new NotImplementedException();
} public override bool ChangePasswordQuestionAndAnswer(string username, string password, string newPasswordQuestion, string newPasswordAnswer)
{
throw new NotImplementedException();
} public override MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status)
{
throw new NotImplementedException();
} public override bool DeleteUser(string username, bool deleteAllRelatedData)
{
throw new NotImplementedException();
} public override bool EnablePasswordReset
{
get
{
throw new NotImplementedException();
}
} public override bool EnablePasswordRetrieval
{
get
{
throw new NotImplementedException();
}
} public override MembershipUserCollection FindUsersByEmail(string emailToMatch, int pageIndex, int pageSize, out int totalRecords)
{
throw new NotImplementedException();
}
public override MembershipUserCollection FindUsersByName(string usernameToMatch, int pageIndex, int pageSize, out int totalRecords)
{
throw new NotImplementedException();
} public override MembershipUserCollection GetAllUsers(int pageIndex, int pageSize, out int totalRecords)
{
throw new NotImplementedException();
} public override int GetNumberOfUsersOnline()
{
throw new NotImplementedException();
} public override string GetPassword(string username, string answer)
{
throw new NotImplementedException();
} public override MembershipUser GetUser(string username, bool userIsOnline)
{
throw new NotImplementedException();
} public override MembershipUser GetUser(object providerUserKey, bool userIsOnline)
{
throw new NotImplementedException();
} public override string GetUserNameByEmail(string email)
{
throw new NotImplementedException();
} public override int MaxInvalidPasswordAttempts
{
get
{
throw new NotImplementedException();
}
} public override int MinRequiredNonAlphanumericCharacters
{
get
{
throw new NotImplementedException();
}
} public override int MinRequiredPasswordLength
{
get
{
throw new NotImplementedException();
}
} public override int PasswordAttemptWindow
{
get
{
throw new NotImplementedException();
}
} public override string PasswordStrengthRegularExpression
{
get
{
throw new NotImplementedException();
}
} public override bool RequiresQuestionAndAnswer
{
get
{
throw new NotImplementedException();
}
} public override bool RequiresUniqueEmail
{
get
{
throw new NotImplementedException();
}
} public override string ResetPassword(string username, string answer)
{
throw new NotImplementedException();
} public override bool UnlockUser(string userName)
{
throw new NotImplementedException();
} public override void UpdateUser(MembershipUser user)
{
throw new NotImplementedException();
} public override bool ValidateUser(string username, string password)
{
throw new NotImplementedException();
}
}
5- Your default.ascx.cs file should look like this, (Optionally change the password in Page_Load to the encrypted password you want to decrypt).
using System;public partial class _Default : System.Web.UI.Page {
{
//This password can be obtained from the DNN''s table aspnet_Membership column "Password"
//Or you can query ther datbase row and call decryption method for each user
string password = "vhicPWw3Eo/+z+mrKM5ZQCIcURj1O5Cq9Epw942lfpmsDPagupzLGw=="; //Call our inherited class to get Decrypted Password
string recoveredPassword = RecoverPassword.RecoverEncryptedString(password); //Write down the decrypted password
Response.Write(recoveredPassword);
}
}
6- Right click > View in Browser and the password is decrypted on a fly. If you surf on this topic you will find that DNN uses password salt to encrypt values. What actually goes inside the encryption is not important. What is important to understand is that, how password salt is used in the encryption process. If you would know that, you will easily be able to decrypt the password.
What happens is as following, we get the encrypted password. Normalize it to base 64 byte stream. We then convert this encoded byte stream into decrypted byte stream (using base class decryptPassowrd method). Then we convert this decrypted byte stream into Unicode string and strip out the first 8 characters which makes up the portion of password salt. The remainder is our decrypted password.