Thursday, April 10, 2014

Changing MachineKey on IIS while using SQLMembership provider

There is business need to change MachineKey after servers was already rolled out and SQLMembership provider created accounts on backend encrypted with MachineKey.
Below code and instructions how to properly do that.
Prerequisites:
1. Your password setting in SQLMembership provider is configured as "encrypted"
That's really the only requirement for the rest of the things to work.

Process is as follows

1. Create membership provider with setting which allows password decryption (enablePasswordRetrieval as "true") and put passwordFormat setting as "clear".
2. Go through the list of users with this membership provider and find all users which are locked and with passwords which can not be decrypted (happens if passwords were encrypted with wrong MachineKey etc). If locked user is encountered then user is unlocked and his password is reset. If any passwords found which are not decryptable then their passwords are reset to preset password. For the rest of the users add their passwords to in-memory Dictionary<> instance which will provide temporary storage of passwords during decryption process.
3. Run SQL statement on database which marks all passwords for all users as [PasswordFormat] =0 which tells provider that this user stores it's password in clear text format. (It's possible to have mix of users in database with both clear and "encrypted" format.
4. Run code against all users and reset their passwords to their original passwords.

By the end of step 4 you would have all passwords in clear text in database.

5. Change "MachineKey" value in web.config file for this application to value you'd like to use
6. Enumerate all users and get their passwords and store them in memory
7. Run SQL statement to mark their [PasswordFormat]=2 which means "encrypted"
8. Change password to the one stored in memory which will encrypt it on the fly to encrypted format in database using MachineKey
9. Change MachineKey in production database


Entire application can stay online (no downtime is required) during operation. The only part which will be offline is user creation and password changes. You need to make sure pages responsible for user creation and password changes are disabled during operations.




Code consists of single page and web.config


Web.config


<?xml version="1.0" encoding="UTF-8"?>
<configuration>
 <appSettings>
  <add key="aspnet:UseLegacyEncryption" value="true" />
  <add key="aspnet:UseLegacyMachineKeyEncryption" value="true" />
 </appSettings>
 <connectionStrings>
     <add connectionString="Data Source=LOCALHOST;Initial Catalog=MachineKeyProject;user id=user;password=user" name="MyConnString" />

 </connectionStrings>
 <system.web>
  <compilation debug="true" targetFramework="4.0" />
  <membership>
   <providers>
    <add name="ClearProvider" type="System.Web.Security.SqlMembershipProvider" connectionStringName="MyConnString" applicationName="MyApp" minRequiredPasswordLength="8" minRequiredNonalphanumericCharacters="0" enablePasswordReset="true" requiresQuestionAndAnswer="false" requiresUniqueEmail="true" maxInvalidPasswordAttempts="6" passwordAttemptWindow="10" passwordFormat="Clear" enablePasswordRetrieval="true" />
    <add name="EncryptedProvider" connectionStringName="MyConnString" type="System.Web.Security.SqlMembershipProvider" enablePasswordRetrieval="true" requiresQuestionAndAnswer="false" applicationName="MyApp" passwordFormat="Encrypted" minRequiredNonalphanumericCharacters="0" />
   </providers>
  </membership>
    
    <machineKey decryption="AES" decryptionKey="CF06DD7B2D8BBF2EB331FBE895C34BED2D6EC3CEB2C640F2" validation="SHA1" validationKey="69B68BE942659D80DE8CB41218A50B40FD63DA7E52E641DF8EA4FB6C7A19B44348BC8900488BD2E81EE64D78BC7D4A6C3C33231B4672ADA433262CE14F05CEC0" />

  <profile>
   <providers>
    <clear />
    <add name="AspNetSqlProfileProvider" type="System.Web.Profile.SqlProfileProvider" connectionStringName="ApplicationServices" applicationName="/" />
   </providers>
  </profile>
  <roleManager enabled="false">
   <providers>
    <clear />
    <add name="AspNetSqlRoleProvider" type="System.Web.Security.SqlRoleProvider" connectionStringName="ApplicationServices" applicationName="/" />
    <add name="AspNetWindowsTokenRoleProvider" type="System.Web.Security.WindowsTokenRoleProvider" applicationName="/" />
   </providers>
  </roleManager>
  <pages controlRenderingCompatibilityVersion="3.5" clientIDMode="AutoID" /></system.web>
</configuration>



Default.aspx

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>
</head>
<body>
    <form id="form1" runat="server">
    <asp:Button ID="SwitchToClearTextButton" runat="server" onclick="ConvertToClear_Click" 
        Text="SwithToClearText" />
    </div>
    <asp:Label ID="Label1" runat="server" Text="Label"></asp:Label>
    <p>
        <asp:Button ID="Button1" runat="server" onclick="Encrypt_Click" 
            Text="EncryptWithNewMachineConfig" />
    </p>
    
    </form>
</body>
</html>



Default.aspx.cs

using System;
using System.Collections.Generic;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.Security;
using System.Linq;
using System.Data;
using System.Data.SqlClient;
using System.Configuration;

public partial class _Default : System.Web.UI.Page
{
    protected void ConvertToClear_Click(object sender, EventArgs e)
    {
        var clearProvider = Membership.Providers["ClearProvider"];
        var encryptedProvider = Membership.Providers["EncryptedProvider"];
        int doeees;
        foreach (MembershipUser user in clearProvider.GetAllUsers(0,int.MaxValue, out doeees))
        {
            if (user.IsLockedOut)
            {
                user.UnlockUser();
           }
        }
        List BadPasswords = new List() ;
        var passwords = new Dictionary();
        foreach (MembershipUser user in clearProvider.GetAllUsers(0, int.MaxValue, out doeees))
        {
            try
            {
                passwords.Add(user.UserName, user.GetPassword());
            }
            catch (Exception)
            {
                passwords.Add(user.UserName, "pAssword42");
                BadPasswords.Add (user.UserName);
            }
        }
        using (var conn = new SqlConnection(
                ConfigurationManager.ConnectionStrings["MyConnString"].ConnectionString))
        {
            conn.Open();
            using (var cmd = new SqlCommand(
                   "UPDATE [aspnet_Membership] SET [PasswordFormat]=0", conn))
                cmd.ExecuteNonQuery();
        }
        foreach (var entry in passwords)
        {
            var resetPassword = clearProvider.ResetPassword(entry.Key, null);
            clearProvider.ChangePassword(entry.Key, resetPassword, entry.Value);
        }
        Label1.Text = "Switch to Clear passwords completed";

    }
    protected void Encrypt_Click(object sender, EventArgs e)
    {
        var clearProvider = Membership.Providers["ClearProvider"];
        var encryptedProvider = Membership.Providers["EncryptedProvider"];
        int doeees;
        foreach (MembershipUser user in clearProvider.GetAllUsers(0, int.MaxValue, out doeees))
        {
            if (user.IsLockedOut) user.UnlockUser();
        }
     
        var passwords = new Dictionary();
        foreach (MembershipUser user in clearProvider.GetAllUsers(0, int.MaxValue, out doeees))
        {
                passwords.Add(user.UserName, user.GetPassword());
     
        }
        using (var conn = new SqlConnection(
                ConfigurationManager.ConnectionStrings["MyConnString"].ConnectionString))
        {
            conn.Open();
            using (var cmd = new SqlCommand(
                   "UPDATE [aspnet_Membership] SET [PasswordFormat]=2", conn))
                cmd.ExecuteNonQuery();
        }
        foreach (var entry in passwords)
        {
            var resetPassword = clearProvider.ResetPassword(entry.Key, null);
            encryptedProvider.ChangePassword(entry.Key, resetPassword, entry.Value);
        }
     
    }

}




Post a Comment