<?xml version="1.0" encoding="utf-8"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/"><channel><language>en</language><title>Blog posts by Amit Mittal</title> <link>https://world.optimizely.com/blogs/amit-mittal/</link><description></description><ttl>60</ttl><generator>Optimizely World</generator><item> <title>Migrating Optimizely 11 to 12: SQL Membership &amp; Legacy Hashes (Part 2)</title>            <link>https://world.optimizely.com/blogs/amit-mittal/dates/2025/12/migrating-optimizely-11-to-12-sql-membership--legacy-hashes-part-2/</link>            <description>&lt;p&gt;In &lt;a href=&quot;/manage-your-blogs/PreviewBlog?blogPageId=341160&quot;&gt;&lt;strong&gt;[Part 1]&lt;/strong&gt;&lt;/a&gt;, we handled the migration of users who were already using ASP.NET Identity. Now, we tackle the more complex scenario: the &lt;code&gt;MembershipUsersUpgrade&lt;/code&gt; project.&lt;/p&gt;
&lt;p&gt;This project is still using the legacy &lt;strong&gt;SQL Server Membership Provider&lt;/strong&gt; (&lt;code&gt;aspnet_*&lt;/code&gt; tables). This adds two layers of complexity:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Data Migration:&lt;/strong&gt; We need to physically move user data from the old &lt;code&gt;aspnet_Users&lt;/code&gt; tables to the new &lt;code&gt;AspNetUsers&lt;/code&gt; tables.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Complex Hashing:&lt;/strong&gt; The SQL Membership provider used a specific (and somewhat quirky) implementation of &lt;code&gt;HMACSHA512&lt;/code&gt; (or SHA1/SHA256) that handles salts and keys differently than modern standards.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;The Strategy: The &quot;Legacy Container&quot; Column&lt;/h2&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;To keep the migration smooth, we won&#39;t try to convert the old passwords to the new format during the SQL migration (which is impossible without the user&#39;s plain-text password).&lt;/p&gt;
&lt;p&gt;Instead, we will create a temporary container column called &lt;code&gt;LegacyPasswordSalt&lt;/code&gt; in our new table. We will store the old Hash, the Salt, and the Format in this one column. When the user logs in, our code will parse this column, verify the old password, and then automatically upgrade them to the new format.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;Step 1: The SQL Migration Script&lt;/h2&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;I created an idempotent SQL script to handle this. It does three things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Adds the &lt;code&gt;LegacyPasswordSalt&lt;/code&gt; column to the new table.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Migrates Users, Roles, and mappings.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Concatenates the old password data into the new column using a pipe (&lt;code&gt;|&lt;/code&gt;) delimiter.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;/p&gt;
&lt;div class=&quot;code-block ng-tns-c3425601710-70 ng-animate-disabled ng-trigger ng-trigger-codeBlockRevealAnimation&quot;&gt;
&lt;div class=&quot;code-block-decoration header-formatted gds-title-s ng-tns-c3425601710-70 ng-star-inserted&quot;&gt;&lt;span class=&quot;ng-tns-c3425601710-70&quot;&gt;SQL&lt;/span&gt;
&lt;div class=&quot;buttons ng-tns-c3425601710-70 ng-star-inserted&quot;&gt;&lt;!----&gt;&lt;!----&gt;&lt;/div&gt;
&lt;!----&gt;&lt;/div&gt;
&lt;!----&gt;
&lt;div class=&quot;formatted-code-block-internal-container ng-tns-c3425601710-70&quot;&gt;
&lt;div class=&quot;animated-opacity ng-tns-c3425601710-70&quot;&gt;
&lt;pre class=&quot;ng-tns-c3425601710-70&quot;&gt;&lt;code class=&quot;code-container formatted ng-tns-c3425601710-70&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;/*
================================================================================
ASP.NET Membership to ASP.NET Core Identity Migration Script
================================================================================
*/&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;SET&lt;/span&gt; NOCOUNT &lt;span class=&quot;hljs-keyword&quot;&gt;ON&lt;/span&gt;;

&lt;span class=&quot;hljs-comment&quot;&gt;-- 1. Add LegacyPasswordSalt column to store old hash/salt data&lt;/span&gt;
IF &lt;span class=&quot;hljs-keyword&quot;&gt;NOT&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;EXISTS&lt;/span&gt; (&lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; sys.columns &lt;span class=&quot;hljs-keyword&quot;&gt;WHERE&lt;/span&gt; Name &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; N&lt;span class=&quot;hljs-string&quot;&gt;&#39;LegacyPasswordSalt&#39;&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;AND&lt;/span&gt; Object_ID &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; Object_ID(N&lt;span class=&quot;hljs-string&quot;&gt;&#39;dbo.AspNetUsers&#39;&lt;/span&gt;))
&lt;span class=&quot;hljs-keyword&quot;&gt;BEGIN&lt;/span&gt;
    PRINT &lt;span class=&quot;hljs-string&quot;&gt;&#39;Adding column [LegacyPasswordSalt] to [dbo].[AspNetUsers]...&#39;&lt;/span&gt;;
    &lt;span class=&quot;hljs-keyword&quot;&gt;ALTER&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;TABLE&lt;/span&gt; [dbo].[AspNetUsers] &lt;span class=&quot;hljs-keyword&quot;&gt;ADD&lt;/span&gt; [LegacyPasswordSalt] NVARCHAR(MAX) &lt;span class=&quot;hljs-keyword&quot;&gt;NULL&lt;/span&gt;;
&lt;span class=&quot;hljs-keyword&quot;&gt;END&lt;/span&gt;

GO

&lt;span class=&quot;hljs-keyword&quot;&gt;BEGIN&lt;/span&gt; TRANSACTION;
&lt;span class=&quot;hljs-keyword&quot;&gt;BEGIN&lt;/span&gt; TRY
    &lt;span class=&quot;hljs-comment&quot;&gt;-- 2. INSERT USERS&lt;/span&gt;
    PRINT &lt;span class=&quot;hljs-string&quot;&gt;&#39;Migrating users...&#39;&lt;/span&gt;;

    &lt;span class=&quot;hljs-keyword&quot;&gt;INSERT&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;INTO&lt;/span&gt; dbo.AspNetUsers (
        Id, UserName, NormalizedUserName, Email, NormalizedEmail, 
        EmailConfirmed, PasswordHash, SecurityStamp, ConcurrencyStamp, 
        PhoneNumber, PhoneNumberConfirmed, TwoFactorEnabled, 
        LockoutEnd, LockoutEnabled, AccessFailedCount, 
        LegacyPasswordSalt, &lt;span class=&quot;hljs-comment&quot;&gt;-- &amp;lt;--- The Key Column&lt;/span&gt;
        IsApproved, CreationDate, LastLoginDate, LastLockoutDate, IsLockedOut
    )
    &lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt;
        u.UserId,
        u.UserName,
        &lt;span class=&quot;hljs-built_in&quot;&gt;UPPER&lt;/span&gt;(u.UserName),
        m.Email,
        &lt;span class=&quot;hljs-built_in&quot;&gt;UPPER&lt;/span&gt;(m.Email),
        &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;, &lt;span class=&quot;hljs-comment&quot;&gt;-- EmailConfirmed&lt;/span&gt;
        &lt;span class=&quot;hljs-comment&quot;&gt;-- We store the formatted legacy string in PasswordHash strictly as a placeholder&lt;/span&gt;
        (m.Password &lt;span class=&quot;hljs-operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&#39;|&#39;&lt;/span&gt; &lt;span class=&quot;hljs-operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;CAST&lt;/span&gt;(m.PasswordFormat &lt;span class=&quot;hljs-keyword&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;hljs-type&quot;&gt;VARCHAR&lt;/span&gt;(&lt;span class=&quot;hljs-number&quot;&gt;10&lt;/span&gt;)) &lt;span class=&quot;hljs-operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&#39;|&#39;&lt;/span&gt; &lt;span class=&quot;hljs-operator&quot;&gt;+&lt;/span&gt; m.PasswordSalt), 
        NEWID(), NEWID(), &lt;span class=&quot;hljs-keyword&quot;&gt;NULL&lt;/span&gt;, &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;, &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;, 
        m.LastLockoutDate, &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;, &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;,
        &lt;span class=&quot;hljs-comment&quot;&gt;-- Actual Legacy Data storage: Hash|Format|Salt&lt;/span&gt;
        (m.Password &lt;span class=&quot;hljs-operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&#39;|&#39;&lt;/span&gt; &lt;span class=&quot;hljs-operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;CAST&lt;/span&gt;(m.PasswordFormat &lt;span class=&quot;hljs-keyword&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;hljs-type&quot;&gt;VARCHAR&lt;/span&gt;(&lt;span class=&quot;hljs-number&quot;&gt;10&lt;/span&gt;)) &lt;span class=&quot;hljs-operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&#39;|&#39;&lt;/span&gt; &lt;span class=&quot;hljs-operator&quot;&gt;+&lt;/span&gt; m.PasswordSalt), 
        &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;, m.CreateDate, m.LastLoginDate, m.LastLockoutDate, &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;
    &lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; dbo.aspnet_Users u
    &lt;span class=&quot;hljs-keyword&quot;&gt;LEFT&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;JOIN&lt;/span&gt; dbo.aspnet_Membership m &lt;span class=&quot;hljs-keyword&quot;&gt;ON&lt;/span&gt; u.UserId&lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; m.UserId
    &lt;span class=&quot;hljs-keyword&quot;&gt;WHERE&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;NOT&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;EXISTS&lt;/span&gt; (&lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; dbo.AspNetUsers anp &lt;span class=&quot;hljs-keyword&quot;&gt;WHERE&lt;/span&gt; anp.Id &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; u.UserId)
      &lt;span class=&quot;hljs-keyword&quot;&gt;AND&lt;/span&gt; m.Password &lt;span class=&quot;hljs-keyword&quot;&gt;IS&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;NOT&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;NULL&lt;/span&gt;;

    PRINT &lt;span class=&quot;hljs-string&quot;&gt;&#39;Users migrated successfully.&#39;&lt;/span&gt;;

    &lt;span class=&quot;hljs-comment&quot;&gt;-- 3. INSERT ROLES&lt;/span&gt;
    PRINT &lt;span class=&quot;hljs-string&quot;&gt;&#39;Migrating roles...&#39;&lt;/span&gt;;
    &lt;span class=&quot;hljs-keyword&quot;&gt;INSERT&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;INTO&lt;/span&gt; dbo.AspNetRoles (Id, Name, NormalizedName)
    &lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt; RoleId, RoleName, &lt;span class=&quot;hljs-built_in&quot;&gt;UPPER&lt;/span&gt;(RoleName)
    &lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; dbo.aspnet_Roles r
    &lt;span class=&quot;hljs-keyword&quot;&gt;WHERE&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;NOT&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;EXISTS&lt;/span&gt; (&lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; dbo.AspNetRoles anr &lt;span class=&quot;hljs-keyword&quot;&gt;WHERE&lt;/span&gt; anr.Id &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; r.RoleId);

    &lt;span class=&quot;hljs-comment&quot;&gt;-- 4. INSERT USER ROLES&lt;/span&gt;
    PRINT &lt;span class=&quot;hljs-string&quot;&gt;&#39;Migrating user-role relationships...&#39;&lt;/span&gt;;
    &lt;span class=&quot;hljs-keyword&quot;&gt;INSERT&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;INTO&lt;/span&gt; dbo.AspNetUserRoles (UserId, RoleId)
    &lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt; UserId, RoleId &lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; dbo.aspnet_UsersInRoles ur
    &lt;span class=&quot;hljs-keyword&quot;&gt;WHERE&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;NOT&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;EXISTS&lt;/span&gt; (&lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; dbo.AspNetUserRoles anur &lt;span class=&quot;hljs-keyword&quot;&gt;WHERE&lt;/span&gt; anur.UserId &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; ur.UserId &lt;span class=&quot;hljs-keyword&quot;&gt;AND&lt;/span&gt; anur.RoleId &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; ur.RoleId);

    &lt;span class=&quot;hljs-keyword&quot;&gt;COMMIT&lt;/span&gt; TRANSACTION;
    PRINT &lt;span class=&quot;hljs-string&quot;&gt;&#39;Migration script completed successfully.&#39;&lt;/span&gt;;
&lt;span class=&quot;hljs-keyword&quot;&gt;END&lt;/span&gt; TRY
&lt;span class=&quot;hljs-keyword&quot;&gt;BEGIN&lt;/span&gt; CATCH
    IF @&lt;span class=&quot;hljs-variable&quot;&gt;@TRANCOUNT&lt;/span&gt; &lt;span class=&quot;hljs-operator&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;ROLLBACK&lt;/span&gt; TRANSACTION;
    PRINT &lt;span class=&quot;hljs-string&quot;&gt;&#39;Error: &#39;&lt;/span&gt; &lt;span class=&quot;hljs-operator&quot;&gt;+&lt;/span&gt; ERROR_MESSAGE();
    THROW;
&lt;span class=&quot;hljs-keyword&quot;&gt;END&lt;/span&gt; CATCH
&lt;/code&gt;&lt;/pre&gt;
&lt;!----&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;Step 2: Extending the Identity User&lt;/h2&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;We need to tell ASP.NET Core Identity about our new column.&lt;/p&gt;
&lt;p&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;/p&gt;
&lt;div class=&quot;code-block ng-tns-c3425601710-71 ng-animate-disabled ng-trigger ng-trigger-codeBlockRevealAnimation&quot;&gt;
&lt;div class=&quot;code-block-decoration header-formatted gds-title-s ng-tns-c3425601710-71 ng-star-inserted&quot;&gt;&lt;span class=&quot;ng-tns-c3425601710-71&quot;&gt;C#&lt;/span&gt;
&lt;div class=&quot;buttons ng-tns-c3425601710-71 ng-star-inserted&quot;&gt;&lt;!----&gt;&lt;!----&gt;&lt;/div&gt;
&lt;!----&gt;&lt;/div&gt;
&lt;!----&gt;
&lt;div class=&quot;formatted-code-block-internal-container ng-tns-c3425601710-71&quot;&gt;
&lt;div class=&quot;animated-opacity ng-tns-c3425601710-71&quot;&gt;
&lt;pre class=&quot;ng-tns-c3425601710-71&quot;&gt;&lt;code class=&quot;code-container formatted ng-tns-c3425601710-71&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;using&lt;/span&gt; Microsoft.AspNetCore.Identity;
&lt;span class=&quot;hljs-keyword&quot;&gt;using&lt;/span&gt; System.ComponentModel.DataAnnotations.Schema;

&lt;span class=&quot;hljs-keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;hljs-title&quot;&gt;MyCustomUser&lt;/span&gt; : &lt;span class=&quot;hljs-title&quot;&gt;IdentityUser&lt;/span&gt; &lt;span class=&quot;hljs-comment&quot;&gt;// Or ApplicationUser depending on your setup&lt;/span&gt;
{
    [&lt;span class=&quot;hljs-meta&quot;&gt;Column(TypeName = &lt;span class=&quot;hljs-meta-string&quot;&gt;&quot;nvarchar(max)&quot;&lt;/span&gt;)&lt;/span&gt;]
    &lt;span class=&quot;hljs-keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;string&lt;/span&gt;? LegacyPasswordSalt { &lt;span class=&quot;hljs-keyword&quot;&gt;get&lt;/span&gt;; &lt;span class=&quot;hljs-keyword&quot;&gt;set&lt;/span&gt;; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;!----&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;Step 3: The Membership Password Hasher&lt;/h2&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;This is the most critical part. The old &lt;code&gt;SqlMembershipProvider&lt;/code&gt; used a specific logic for &lt;code&gt;HMACSHA512&lt;/code&gt;. It didn&#39;t just pass the salt to the constructor; it had a specific way of padding the key if the salt length differed from the key length.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Warning:&lt;/strong&gt; You must use &lt;code&gt;Encoding.Unicode&lt;/code&gt; (Little Endian UTF-16) for the password bytes. Modern systems use UTF-8, but legacy ASP.NET Membership used Unicode. If you change this, the hashes will not match.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;/p&gt;
&lt;div class=&quot;code-block ng-tns-c3425601710-72 ng-animate-disabled ng-trigger ng-trigger-codeBlockRevealAnimation&quot;&gt;
&lt;div class=&quot;code-block-decoration header-formatted gds-title-s ng-tns-c3425601710-72 ng-star-inserted&quot;&gt;&lt;span class=&quot;ng-tns-c3425601710-72&quot;&gt;C#&lt;/span&gt;
&lt;div class=&quot;buttons ng-tns-c3425601710-72 ng-star-inserted&quot;&gt;&lt;!----&gt;&lt;!----&gt;&lt;/div&gt;
&lt;!----&gt;&lt;/div&gt;
&lt;!----&gt;
&lt;div class=&quot;formatted-code-block-internal-container ng-tns-c3425601710-72&quot;&gt;
&lt;div class=&quot;animated-opacity ng-tns-c3425601710-72&quot;&gt;
&lt;pre class=&quot;ng-tns-c3425601710-72&quot;&gt;&lt;code class=&quot;code-container formatted ng-tns-c3425601710-72&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;using&lt;/span&gt; Microsoft.AspNetCore.Identity;
&lt;span class=&quot;hljs-keyword&quot;&gt;using&lt;/span&gt; System;
&lt;span class=&quot;hljs-keyword&quot;&gt;using&lt;/span&gt; System.Security.Cryptography;
&lt;span class=&quot;hljs-keyword&quot;&gt;using&lt;/span&gt; System.Text;

&lt;span class=&quot;hljs-keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;hljs-title&quot;&gt;FallbackPasswordHasher&lt;/span&gt; : &lt;span class=&quot;hljs-title&quot;&gt;PasswordHasher&lt;/span&gt;&amp;lt;&lt;span class=&quot;hljs-title&quot;&gt;MyCustomUser&lt;/span&gt;&amp;gt;
{
    &lt;span class=&quot;hljs-comment&quot;&gt;// The legacy field format: Hash|Format|Salt&lt;/span&gt;
    &lt;span class=&quot;hljs-keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;int&lt;/span&gt; LegacyHashIndex = &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;;
    &lt;span class=&quot;hljs-keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;int&lt;/span&gt; LegacyFormatIndex = &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;; 
    &lt;span class=&quot;hljs-keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;int&lt;/span&gt; LegacySaltIndex = &lt;span class=&quot;hljs-number&quot;&gt;2&lt;/span&gt;;
    &lt;span class=&quot;hljs-keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;int&lt;/span&gt; ExpectedLegacyPropertyCount = &lt;span class=&quot;hljs-number&quot;&gt;3&lt;/span&gt;;
    &lt;span class=&quot;hljs-keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;int&lt;/span&gt; HashedPasswordFormat = &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;; &lt;span class=&quot;hljs-comment&quot;&gt;// 1 = Hashed&lt;/span&gt;

    &lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;override&lt;/span&gt; PasswordVerificationResult &lt;span class=&quot;hljs-title&quot;&gt;VerifyHashedPassword&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;MyCustomUser user, &lt;span class=&quot;hljs-built_in&quot;&gt;string&lt;/span&gt; hashedPassword, &lt;span class=&quot;hljs-built_in&quot;&gt;string&lt;/span&gt; providedPassword&lt;/span&gt;)&lt;/span&gt;
    {
        &lt;span class=&quot;hljs-comment&quot;&gt;// 1. Modern Hash Check&lt;/span&gt;
        &lt;span class=&quot;hljs-comment&quot;&gt;// If LegacyPasswordSalt is empty, the user has already been migrated or is new.&lt;/span&gt;
        &lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt; (&lt;span class=&quot;hljs-built_in&quot;&gt;string&lt;/span&gt;.IsNullOrEmpty(user.LegacyPasswordSalt))
        {
            &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;base&lt;/span&gt;.VerifyHashedPassword(user, hashedPassword, providedPassword);
        }

        &lt;span class=&quot;hljs-comment&quot;&gt;// 2. Legacy Hash Fallback&lt;/span&gt;
        &lt;span class=&quot;hljs-built_in&quot;&gt;string&lt;/span&gt;[] passwordProperties = user.LegacyPasswordSalt.Split(&lt;span class=&quot;hljs-string&quot;&gt;&#39;|&#39;&lt;/span&gt;);
        &lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt; (passwordProperties.Length &amp;lt; ExpectedLegacyPropertyCount)
        {
            &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; PasswordVerificationResult.Failed;
        }

        &lt;span class=&quot;hljs-built_in&quot;&gt;string&lt;/span&gt; legacyHashBase64 = passwordProperties[LegacyHashIndex];
        &lt;span class=&quot;hljs-built_in&quot;&gt;string&lt;/span&gt; saltBase64 = passwordProperties[LegacySaltIndex];
            
        &lt;span class=&quot;hljs-comment&quot;&gt;// We assume format is &#39;1&#39; (Hashed).&lt;/span&gt;
        &lt;span class=&quot;hljs-comment&quot;&gt;// If you have users with format &#39;0&#39; (Clear), handling that here is a security risk.&lt;/span&gt;
        &lt;span class=&quot;hljs-built_in&quot;&gt;byte&lt;/span&gt;[] providedHashBytes = HashLegacyPassword(providedPassword, HashedPasswordFormat, saltBase64);
        
        &lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt; (providedHashBytes == &lt;span class=&quot;hljs-literal&quot;&gt;null&lt;/span&gt;) &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; PasswordVerificationResult.Failed;

        &lt;span class=&quot;hljs-built_in&quot;&gt;byte&lt;/span&gt;[] storedHashBytes;
        &lt;span class=&quot;hljs-keyword&quot;&gt;try&lt;/span&gt;
        {
            storedHashBytes = Convert.FromBase64String(legacyHashBase64);
        }
        catch (FormatException)
        {
            &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; PasswordVerificationResult.Failed;
        }

        &lt;span class=&quot;hljs-comment&quot;&gt;// 3. Secure Comparison&lt;/span&gt;
        &lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt; (CryptographicOperations.FixedTimeEquals(providedHashBytes, storedHashBytes))
        {
            &lt;span class=&quot;hljs-comment&quot;&gt;// Success! Return &#39;SuccessRehashNeeded&#39; to trigger an automatic database update.&lt;/span&gt;
            &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; PasswordVerificationResult.SuccessRehashNeeded;
        }

        &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; PasswordVerificationResult.Failed;
    }

    &lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;hljs-title&quot;&gt;HashPassword&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;MyCustomUser user, &lt;span class=&quot;hljs-built_in&quot;&gt;string&lt;/span&gt; password&lt;/span&gt;)&lt;/span&gt;
    {
        &lt;span class=&quot;hljs-comment&quot;&gt;// When creating a NEW hash (e.g. Change Password, or Re-hashing after login),&lt;/span&gt;
        &lt;span class=&quot;hljs-comment&quot;&gt;// we must clear the legacy field to ensure future logins use the modern standard.&lt;/span&gt;
        user.LegacyPasswordSalt = &lt;span class=&quot;hljs-literal&quot;&gt;null&lt;/span&gt;;
        &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;base&lt;/span&gt;.HashPassword(user, password);
    }

    &lt;span class=&quot;hljs-comment&quot;&gt;&lt;span class=&quot;hljs-doctag&quot;&gt;///&lt;/span&gt; &lt;span class=&quot;hljs-doctag&quot;&gt;&amp;lt;summary&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-comment&quot;&gt;&lt;span class=&quot;hljs-doctag&quot;&gt;///&lt;/span&gt; Replicates the specific HMACSHA512 logic used by the legacy SqlMembershipProvider.&lt;/span&gt;
    &lt;span class=&quot;hljs-comment&quot;&gt;&lt;span class=&quot;hljs-doctag&quot;&gt;///&lt;/span&gt; &lt;span class=&quot;hljs-doctag&quot;&gt;&amp;lt;/summary&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;byte&lt;/span&gt;[] &lt;span class=&quot;hljs-title&quot;&gt;HashLegacyPassword&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;span class=&quot;hljs-built_in&quot;&gt;string&lt;/span&gt; password, &lt;span class=&quot;hljs-built_in&quot;&gt;int&lt;/span&gt; passwordFormat, &lt;span class=&quot;hljs-built_in&quot;&gt;string&lt;/span&gt; salt&lt;/span&gt;)&lt;/span&gt;
    {
        &lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt; (passwordFormat != HashedPasswordFormat) &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;hljs-literal&quot;&gt;null&lt;/span&gt;;
        &lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt; (&lt;span class=&quot;hljs-built_in&quot;&gt;string&lt;/span&gt;.IsNullOrEmpty(password) || &lt;span class=&quot;hljs-built_in&quot;&gt;string&lt;/span&gt;.IsNullOrEmpty(salt)) &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;hljs-literal&quot;&gt;null&lt;/span&gt;;

        &lt;span class=&quot;hljs-built_in&quot;&gt;byte&lt;/span&gt;[] passwordBytes;
        &lt;span class=&quot;hljs-built_in&quot;&gt;byte&lt;/span&gt;[] saltBytes;
            
        &lt;span class=&quot;hljs-keyword&quot;&gt;try&lt;/span&gt;
        {
            &lt;span class=&quot;hljs-comment&quot;&gt;// IMPORTANT: Membership provider used Unicode (UTF-16), not UTF-8.&lt;/span&gt;
            passwordBytes = Encoding.Unicode.GetBytes(password);
            saltBytes = Convert.FromBase64String(salt);
        }
        catch
        {
            &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;hljs-literal&quot;&gt;null&lt;/span&gt;;
        }

        &lt;span class=&quot;hljs-keyword&quot;&gt;using&lt;/span&gt; (&lt;span class=&quot;hljs-keyword&quot;&gt;var&lt;/span&gt; hmac = (KeyedHashAlgorithm)HashAlgorithm.Create(&lt;span class=&quot;hljs-string&quot;&gt;&quot;HMACSHA512&quot;&lt;/span&gt;))
        {
            &lt;span class=&quot;hljs-comment&quot;&gt;// --- Replicated ASP.NET Membership Key Logic ---&lt;/span&gt;
            &lt;span class=&quot;hljs-comment&quot;&gt;// Do not refactor this block. It handles specific key padding used by the old provider.&lt;/span&gt;
            &lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt; (hmac.Key.Length == saltBytes.Length)
            {
                hmac.Key = saltBytes;
            }
            &lt;span class=&quot;hljs-keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt; (hmac.Key.Length &amp;lt; saltBytes.Length)
            {
                &lt;span class=&quot;hljs-keyword&quot;&gt;var&lt;/span&gt; key = &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;byte&lt;/span&gt;[hmac.Key.Length];
                Buffer.BlockCopy(saltBytes, &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;, key, &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;, key.Length);
                hmac.Key = key;
            }
            &lt;span class=&quot;hljs-keyword&quot;&gt;else&lt;/span&gt; 
            {
                &lt;span class=&quot;hljs-keyword&quot;&gt;var&lt;/span&gt; key = &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;byte&lt;/span&gt;[hmac.Key.Length];
                &lt;span class=&quot;hljs-keyword&quot;&gt;for&lt;/span&gt; (&lt;span class=&quot;hljs-keyword&quot;&gt;var&lt;/span&gt; i = &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;; i &amp;lt; key.Length;)
                {
                    &lt;span class=&quot;hljs-keyword&quot;&gt;var&lt;/span&gt; len = Math.Min(saltBytes.Length, key.Length - i);
                    Buffer.BlockCopy(saltBytes, &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;, key, i, len);
                    i += len;
                }
                hmac.Key = key;
            }
            &lt;span class=&quot;hljs-comment&quot;&gt;// -----------------------------------------------&lt;/span&gt;

            &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; hmac.ComputeHash(passwordBytes);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;!----&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Just like in &lt;a href=&quot;/manage-your-blogs/PreviewBlog?blogPageId=341160&quot;&gt;Part 1&lt;/a&gt;, don&#39;t forget to register this in your &lt;code&gt;Startup.cs&lt;/code&gt; before the Identity initialization:&lt;/p&gt;
&lt;p&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;/p&gt;
&lt;div class=&quot;code-block ng-tns-c3425601710-73 ng-animate-disabled ng-trigger ng-trigger-codeBlockRevealAnimation&quot;&gt;
&lt;div class=&quot;code-block-decoration header-formatted gds-title-s ng-tns-c3425601710-73 ng-star-inserted&quot;&gt;&lt;span class=&quot;ng-tns-c3425601710-73&quot;&gt;C#&lt;/span&gt;
&lt;div class=&quot;buttons ng-tns-c3425601710-73 ng-star-inserted&quot;&gt;&lt;!----&gt;&lt;!----&gt;&lt;/div&gt;
&lt;!----&gt;&lt;/div&gt;
&lt;!----&gt;
&lt;div class=&quot;formatted-code-block-internal-container ng-tns-c3425601710-73&quot;&gt;
&lt;div class=&quot;animated-opacity ng-tns-c3425601710-73&quot;&gt;
&lt;pre class=&quot;ng-tns-c3425601710-73&quot;&gt;&lt;code class=&quot;code-container formatted ng-tns-c3425601710-73&quot;&gt;services.AddScoped(&lt;span class=&quot;hljs-keyword&quot;&gt;typeof&lt;/span&gt;(IPasswordHasher&amp;lt;&amp;gt;), &lt;span class=&quot;hljs-keyword&quot;&gt;typeof&lt;/span&gt;(FallbackPasswordHasher&amp;lt;&amp;gt;));
services.AddCmsAspNetIdentity&amp;lt;MyCustomUser&amp;gt;();
&lt;/code&gt;&lt;/pre&gt;
&lt;!----&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;/p&gt;
&lt;p&gt;This solution provides a seamless experience. Users log in, the system detects the old &quot;pipe-delimited&quot; legacy data, verifies it using the old logic, and instantly upgrades them to the modern secure standard without them ever knowing.&lt;/p&gt;</description>            <guid>https://world.optimizely.com/blogs/amit-mittal/dates/2025/12/migrating-optimizely-11-to-12-sql-membership--legacy-hashes-part-2/</guid>            <pubDate>Mon, 01 Dec 2025 09:13:44 GMT</pubDate>           <category>Blog post</category></item><item> <title>Migrating Optimizely 11 to 12: Handling Legacy Password Hashes (Part 1)</title>            <link>https://world.optimizely.com/blogs/amit-mittal/dates/2025/12/migrating-optimizely-11-to-12-handling-legacy-password-hashes-part-1/</link>            <description>&lt;div class=&quot;js-quote-selection-container&quot;&gt;
&lt;div class=&quot;js-discussion js-socket-channel&quot;&gt;
&lt;div class=&quot;discussion-timeline-actions&quot;&gt;
&lt;div class=&quot;pl-0 pl-md-6 ml-md-3 ml-0 js-comment-container&quot;&gt;
&lt;div class=&quot;d-md-block d-none&quot;&gt;
&lt;p&gt;Recently, I was tasked with a complex migration: moving existing users from two Optimizely 11 projects to the new Optimizely 12 (ASP.NET Core). The core requirement for both projects was seamlessness&amp;mdash;existing users needed to be able to log in with their current passwords without being forced to reset them.&lt;/p&gt;
&lt;p&gt;This presented two distinct scenarios:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Project &lt;code&gt;IdentityUserUpgrade&lt;/code&gt;:&lt;/strong&gt; Already using ASP.NET Identity (but the older .NET Framework version).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Project &lt;code&gt;MembershipUsersUpgrade&lt;/code&gt;:&lt;/strong&gt; Still using the legacy SQL Server Membership Provider.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In this post, I will focus on &lt;strong&gt;Project &lt;code&gt;IdentityUserUpgrade&lt;/code&gt;&lt;/strong&gt;. Even though this project was already using Identity, moving from .NET Framework to .NET Core changes the underlying hashing algorithm. We need a way to bridge that gap.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;The Investigation: Analyzing the Web.Config&lt;/h2&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Since the &lt;code&gt;IdentityUserUpgrade&lt;/code&gt; project was already on Identity tables, I didn&#39;t need to migrate data between SQL tables. However, I needed to understand how the passwords were encrypted.&lt;/p&gt;
&lt;p&gt;I started by checking the legacy &lt;code&gt;web.config&lt;/code&gt; file, specifically looking for &lt;code&gt;passwordFormat&lt;/code&gt; and &lt;code&gt;hashAlgorithmType&lt;/code&gt;.&lt;/p&gt;
&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;
&lt;div class=&quot;code-block ng-tns-c3425601710-40 ng-animate-disabled ng-trigger ng-trigger-codeBlockRevealAnimation&quot;&gt;
&lt;div class=&quot;code-block-decoration header-formatted gds-title-s ng-tns-c3425601710-40 ng-star-inserted&quot;&gt;&lt;span class=&quot;ng-tns-c3425601710-40&quot;&gt;XML&lt;/span&gt;
&lt;div class=&quot;buttons ng-tns-c3425601710-40 ng-star-inserted&quot;&gt;&lt;!----&gt;&lt;!----&gt;&lt;/div&gt;
&lt;!----&gt;&lt;/div&gt;
&lt;!----&gt;
&lt;div class=&quot;formatted-code-block-internal-container ng-tns-c3425601710-40&quot;&gt;
&lt;div class=&quot;animated-opacity ng-tns-c3425601710-40&quot;&gt;
&lt;pre class=&quot;ng-tns-c3425601710-40&quot;&gt;&lt;code class=&quot;code-container formatted ng-tns-c3425601710-40&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;membership&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;hashAlgorithmType&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;SHA1&quot;&lt;/span&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;providers&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;add&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;name&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;DefaultConnection&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;...&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;passwordFormat&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;Hashed&quot;&lt;/span&gt; /&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;providers&lt;/span&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;membership&lt;/span&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;!----&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;
&lt;p&gt;The key takeaways here were:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;passwordFormat:&lt;/strong&gt; &lt;code&gt;Hashed&lt;/code&gt; (This is crucial for the solution below).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;hashAlgorithmType:&lt;/strong&gt; &lt;code&gt;SHA1&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; You may not explicitly see these values in your &lt;code&gt;web.config&lt;/code&gt; if the project was using the system defaults, but &lt;strong&gt;SHA1&lt;/strong&gt; was the standard for older Identity V2 implementations.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;The Solution: The Fallback Password Hasher&lt;/h2&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;ASP.NET Core Identity (V3) uses &lt;strong&gt;PBKDF2 with HMAC-SHA256&lt;/strong&gt; (or SHA512 in newer versions) and a distinct binary format. The old Identity (V2) used &lt;strong&gt;PBKDF2 with HMAC-SHA1&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;To solve this, we need a custom &lt;code&gt;IPasswordHasher&lt;/code&gt; that attempts to verify the password using the new format first. If that fails, it falls back to the legacy format. If the legacy format matches, we report success and instruct Identity to re-hash the password immediately.&lt;/p&gt;
&lt;p&gt;Here is the implementation:&lt;/p&gt;
&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;
&lt;div class=&quot;code-block ng-tns-c3425601710-41 ng-animate-disabled ng-trigger ng-trigger-codeBlockRevealAnimation&quot;&gt;&lt;!----&gt;
&lt;div class=&quot;formatted-code-block-internal-container ng-tns-c3425601710-41&quot;&gt;
&lt;div class=&quot;animated-opacity ng-tns-c3425601710-41&quot;&gt;
&lt;pre class=&quot;ng-tns-c3425601710-41&quot;&gt;&lt;code class=&quot;code-container formatted ng-tns-c3425601710-41&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;using&lt;/span&gt; Microsoft.AspNetCore.Identity;
&lt;span class=&quot;hljs-keyword&quot;&gt;using&lt;/span&gt; System;
&lt;span class=&quot;hljs-keyword&quot;&gt;using&lt;/span&gt; System.Security.Cryptography;

&lt;span class=&quot;hljs-comment&quot;&gt;&lt;span class=&quot;hljs-doctag&quot;&gt;///&lt;/span&gt; &lt;span class=&quot;hljs-doctag&quot;&gt;&amp;lt;summary&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;&lt;span class=&quot;hljs-doctag&quot;&gt;///&lt;/span&gt; A password hasher that supports legacy ASP.NET Identity V2 hashes (PBKDF2-SHA1)&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;&lt;span class=&quot;hljs-doctag&quot;&gt;///&lt;/span&gt; while ensuring all new passwords are created using the modern Identity V3 standard.&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;&lt;span class=&quot;hljs-doctag&quot;&gt;///&lt;/span&gt; &lt;span class=&quot;hljs-doctag&quot;&gt;&amp;lt;/summary&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;hljs-title&quot;&gt;FallbackPasswordHasher&lt;/span&gt;&amp;lt;&lt;span class=&quot;hljs-title&quot;&gt;TUser&lt;/span&gt;&amp;gt; : &lt;span class=&quot;hljs-title&quot;&gt;IPasswordHasher&lt;/span&gt;&amp;lt;&lt;span class=&quot;hljs-title&quot;&gt;TUser&lt;/span&gt;&amp;gt; &lt;span class=&quot;hljs-keyword&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;hljs-title&quot;&gt;TUser&lt;/span&gt; : &lt;span class=&quot;hljs-keyword&quot;&gt;class&lt;/span&gt;
{
    &lt;span class=&quot;hljs-comment&quot;&gt;// We use the default ASP.NET Core PasswordHasher for all new hashes and primary verification.&lt;/span&gt;
    &lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;readonly&lt;/span&gt; &lt;span class=&quot;hljs-title&quot;&gt;PasswordHasher&lt;/span&gt;&amp;lt;&lt;span class=&quot;hljs-title&quot;&gt;TUser&lt;/span&gt;&amp;gt; _newHasher&lt;/span&gt; = &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; PasswordHasher&amp;lt;TUser&amp;gt;();

    &lt;span class=&quot;hljs-comment&quot;&gt;&lt;span class=&quot;hljs-doctag&quot;&gt;///&lt;/span&gt; &lt;span class=&quot;hljs-doctag&quot;&gt;&amp;lt;summary&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-comment&quot;&gt;&lt;span class=&quot;hljs-doctag&quot;&gt;///&lt;/span&gt; Generates a hash for a password using the modern (V3) standard.&lt;/span&gt;
    &lt;span class=&quot;hljs-comment&quot;&gt;&lt;span class=&quot;hljs-doctag&quot;&gt;///&lt;/span&gt; &lt;span class=&quot;hljs-doctag&quot;&gt;&amp;lt;/summary&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;hljs-title&quot;&gt;HashPassword&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;TUser user, &lt;span class=&quot;hljs-built_in&quot;&gt;string&lt;/span&gt; password&lt;/span&gt;)&lt;/span&gt;
    {
        &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; _newHasher.HashPassword(user, password);
    }

    &lt;span class=&quot;hljs-comment&quot;&gt;&lt;span class=&quot;hljs-doctag&quot;&gt;///&lt;/span&gt; &lt;span class=&quot;hljs-doctag&quot;&gt;&amp;lt;summary&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-comment&quot;&gt;&lt;span class=&quot;hljs-doctag&quot;&gt;///&lt;/span&gt; Verifies a password against a stored hash.&lt;/span&gt;
    &lt;span class=&quot;hljs-comment&quot;&gt;&lt;span class=&quot;hljs-doctag&quot;&gt;///&lt;/span&gt; Tries the modern hasher first; if it fails, falls back to the legacy V2 verifier.&lt;/span&gt;
    &lt;span class=&quot;hljs-comment&quot;&gt;&lt;span class=&quot;hljs-doctag&quot;&gt;///&lt;/span&gt; &lt;span class=&quot;hljs-doctag&quot;&gt;&amp;lt;/summary&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;public&lt;/span&gt; PasswordVerificationResult &lt;span class=&quot;hljs-title&quot;&gt;VerifyHashedPassword&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;TUser user, &lt;span class=&quot;hljs-built_in&quot;&gt;string&lt;/span&gt; hashedPassword, &lt;span class=&quot;hljs-built_in&quot;&gt;string&lt;/span&gt; providedPassword&lt;/span&gt;)&lt;/span&gt;
    {
        &lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt; (&lt;span class=&quot;hljs-built_in&quot;&gt;string&lt;/span&gt;.IsNullOrEmpty(hashedPassword))
        {
            &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; PasswordVerificationResult.Failed;
        }

        &lt;span class=&quot;hljs-comment&quot;&gt;// 1. Attempt to verify using the current (modern) standard.&lt;/span&gt;
        &lt;span class=&quot;hljs-keyword&quot;&gt;var&lt;/span&gt; result = _newHasher.VerifyHashedPassword(user, hashedPassword, providedPassword);

        &lt;span class=&quot;hljs-comment&quot;&gt;// If the modern hasher recognizes it, return immediately.&lt;/span&gt;
        &lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt; (result == PasswordVerificationResult.Success || result == PasswordVerificationResult.SuccessRehashNeeded)
        {
            &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; result;
        }

        &lt;span class=&quot;hljs-comment&quot;&gt;// 2. If modern verification failed, check if this is a legacy ASP.NET Identity V2 hash.&lt;/span&gt;
        &lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt; (VerifyAspNetIdentityV2Hash(hashedPassword, providedPassword))
        {
            &lt;span class=&quot;hljs-comment&quot;&gt;// CRITICAL: Return &#39;SuccessRehashNeeded&#39;.&lt;/span&gt;
            &lt;span class=&quot;hljs-comment&quot;&gt;// This tells the Identity system: &quot;Login allowed, but re-hash this password &lt;/span&gt;
            &lt;span class=&quot;hljs-comment&quot;&gt;// with the new format and update the database immediately.&quot;&lt;/span&gt;
            &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; PasswordVerificationResult.SuccessRehashNeeded;
        }

        &lt;span class=&quot;hljs-comment&quot;&gt;// 3. Password matches neither format.&lt;/span&gt;
        &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; PasswordVerificationResult.Failed;
    }

    &lt;span class=&quot;hljs-comment&quot;&gt;&lt;span class=&quot;hljs-doctag&quot;&gt;///&lt;/span&gt; &lt;span class=&quot;hljs-doctag&quot;&gt;&amp;lt;summary&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-comment&quot;&gt;&lt;span class=&quot;hljs-doctag&quot;&gt;///&lt;/span&gt; Manually verifies a hash against the specific binary layout and parameters of ASP.NET Identity V2.&lt;/span&gt;
    &lt;span class=&quot;hljs-comment&quot;&gt;&lt;span class=&quot;hljs-doctag&quot;&gt;///&lt;/span&gt; Format: [1 byte: Version (0x00)] + [16 bytes: Salt] + [32 bytes: Subkey] = 49 bytes total.&lt;/span&gt;
    &lt;span class=&quot;hljs-comment&quot;&gt;&lt;span class=&quot;hljs-doctag&quot;&gt;///&lt;/span&gt; &lt;span class=&quot;hljs-doctag&quot;&gt;&amp;lt;/summary&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;bool&lt;/span&gt; &lt;span class=&quot;hljs-title&quot;&gt;VerifyAspNetIdentityV2Hash&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;span class=&quot;hljs-built_in&quot;&gt;string&lt;/span&gt; storedHash, &lt;span class=&quot;hljs-built_in&quot;&gt;string&lt;/span&gt; password&lt;/span&gt;)&lt;/span&gt;
    {
        &lt;span class=&quot;hljs-built_in&quot;&gt;byte&lt;/span&gt;[] decoded;
        &lt;span class=&quot;hljs-keyword&quot;&gt;try&lt;/span&gt;
        {
            decoded = Convert.FromBase64String(storedHash);
        }
        catch
        {
            &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;hljs-literal&quot;&gt;false&lt;/span&gt;; &lt;span class=&quot;hljs-comment&quot;&gt;// Not a valid Base64 string&lt;/span&gt;
        }

        &lt;span class=&quot;hljs-comment&quot;&gt;// Identity V2 hashes are exactly 49 bytes long.&lt;/span&gt;
        &lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt; (decoded.Length != &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt; + &lt;span class=&quot;hljs-number&quot;&gt;16&lt;/span&gt; + &lt;span class=&quot;hljs-number&quot;&gt;32&lt;/span&gt;)
        {
            &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;hljs-literal&quot;&gt;false&lt;/span&gt;;
        }

        &lt;span class=&quot;hljs-comment&quot;&gt;// The version byte for Identity V2 is always 0x00.&lt;/span&gt;
        &lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt; (decoded[&lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;] != &lt;span class=&quot;hljs-number&quot;&gt;0x00&lt;/span&gt;)
        {
            &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;hljs-literal&quot;&gt;false&lt;/span&gt;;
        }

        &lt;span class=&quot;hljs-comment&quot;&gt;// Extract the salt (next 16 bytes starting at index 1).&lt;/span&gt;
        &lt;span class=&quot;hljs-keyword&quot;&gt;var&lt;/span&gt; salt = &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;byte&lt;/span&gt;[&lt;span class=&quot;hljs-number&quot;&gt;16&lt;/span&gt;];
        Buffer.BlockCopy(decoded, &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;, salt, &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;, &lt;span class=&quot;hljs-number&quot;&gt;16&lt;/span&gt;);

        &lt;span class=&quot;hljs-comment&quot;&gt;// Extract the expected hash/subkey (next 32 bytes starting at index 17).&lt;/span&gt;
        &lt;span class=&quot;hljs-keyword&quot;&gt;var&lt;/span&gt; expectedSubkey = &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;byte&lt;/span&gt;[&lt;span class=&quot;hljs-number&quot;&gt;32&lt;/span&gt;];
        Buffer.BlockCopy(decoded, &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt; + &lt;span class=&quot;hljs-number&quot;&gt;16&lt;/span&gt;, expectedSubkey, &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;, &lt;span class=&quot;hljs-number&quot;&gt;32&lt;/span&gt;);

        &lt;span class=&quot;hljs-comment&quot;&gt;// Re-compute the hash using the provided password and extracted salt.&lt;/span&gt;
        &lt;span class=&quot;hljs-comment&quot;&gt;// Identity V2 explicitly used PBKDF2 with 1000 iterations and HMAC-SHA1.&lt;/span&gt;
        &lt;span class=&quot;hljs-comment&quot;&gt;// &lt;span class=&quot;hljs-doctag&quot;&gt;NOTE:&lt;/span&gt; If your legacy app used a different iteration count, update &#39;1000&#39; below.&lt;/span&gt;
        &lt;span class=&quot;hljs-keyword&quot;&gt;using&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;var&lt;/span&gt; deriveBytes = &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; Rfc2898DeriveBytes(password, salt, &lt;span class=&quot;hljs-number&quot;&gt;1000&lt;/span&gt;, HashAlgorithmName.SHA1);
        &lt;span class=&quot;hljs-keyword&quot;&gt;var&lt;/span&gt; actualSubkey = deriveBytes.GetBytes(&lt;span class=&quot;hljs-number&quot;&gt;32&lt;/span&gt;);

        &lt;span class=&quot;hljs-comment&quot;&gt;// Cryptographic comparison (constant time) to prevent timing attacks.&lt;/span&gt;
        &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; CryptographicOperations.FixedTimeEquals(actualSubkey, expectedSubkey);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;!----&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;A Note on Algorithms&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;The code above explicitly handles &lt;code&gt;HashAlgorithmName.SHA1&lt;/code&gt; because that was the default for my project. However, &lt;code&gt;Rfc2898DeriveBytes&lt;/code&gt; supports other algorithms if your legacy system used them (e.g., SHA256, SHA512, or MD5). You can adjust the &lt;code&gt;HashAlgorithmName&lt;/code&gt; parameter in the &lt;code&gt;VerifyAspNetIdentityV2Hash&lt;/code&gt; method to match your legacy configuration.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;Service Registration&lt;/h2&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Finally, to make this work, you must register your custom hasher in &lt;code&gt;Startup.cs&lt;/code&gt; (or &lt;code&gt;Program.cs&lt;/code&gt;). This must happen &lt;strong&gt;before&lt;/strong&gt; the call to &lt;code&gt;AddCmsAspNetIdentity&lt;/code&gt;.&lt;/p&gt;
&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;
&lt;div class=&quot;code-block ng-tns-c3425601710-42 ng-animate-disabled ng-trigger ng-trigger-codeBlockRevealAnimation&quot;&gt;
&lt;div class=&quot;code-block-decoration header-formatted gds-title-s ng-tns-c3425601710-42 ng-star-inserted&quot;&gt;&lt;span class=&quot;ng-tns-c3425601710-42&quot;&gt;C#&lt;/span&gt;
&lt;div class=&quot;buttons ng-tns-c3425601710-42 ng-star-inserted&quot;&gt;&lt;!----&gt;&lt;!----&gt;&lt;/div&gt;
&lt;!----&gt;&lt;/div&gt;
&lt;!----&gt;
&lt;div class=&quot;formatted-code-block-internal-container ng-tns-c3425601710-42&quot;&gt;
&lt;div class=&quot;animated-opacity ng-tns-c3425601710-42&quot;&gt;
&lt;pre class=&quot;ng-tns-c3425601710-42&quot;&gt;&lt;code class=&quot;code-container formatted ng-tns-c3425601710-42&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// Register the fallback hasher&lt;/span&gt;
services.AddScoped(&lt;span class=&quot;hljs-keyword&quot;&gt;typeof&lt;/span&gt;(IPasswordHasher&amp;lt;&amp;gt;), &lt;span class=&quot;hljs-keyword&quot;&gt;typeof&lt;/span&gt;(FallbackPasswordHasher&amp;lt;&amp;gt;));

&lt;span class=&quot;hljs-comment&quot;&gt;// Then add CMS Identity&lt;/span&gt;
services.AddCmsAspNetIdentity&amp;lt;ApplicationUser&amp;gt;();
&lt;/code&gt;&lt;/pre&gt;
&lt;!----&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;
&lt;p&gt;With this setup, old users can log in seamlessly. Upon their first login, the system will detect the old hash, verify it, and automatically upgrade the database entry to the new secure format.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Stay tuned for &lt;a href=&quot;/manage-your-blogs/PreviewBlog?blogPageId=341161&quot;&gt;Part 2&lt;/a&gt;, where I will tackle the more challenging &lt;strong&gt;MembershipUsersUpgrade&lt;/strong&gt; project involving SQL Membership providers!&lt;/em&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</description>            <guid>https://world.optimizely.com/blogs/amit-mittal/dates/2025/12/migrating-optimizely-11-to-12-handling-legacy-password-hashes-part-1/</guid>            <pubDate>Mon, 01 Dec 2025 09:04:18 GMT</pubDate>           <category>Blog post</category></item><item> <title>Troubleshooting Optimizely Shortcuts: Why PageShortcutLink Threw an Error and the Solution</title>            <link>https://world.optimizely.com/blogs/amit-mittal/dates/2025/7/troubleshooting-optimizely-shortcuts-why-pageshortcutlink-threw-an-error-and-the-solution/</link>            <description>&lt;p class=&quot;MsoNormal&quot;&gt;&lt;span style=&quot;mso-ansi-language: EN-US;&quot;&gt;As developers working with Optimizely, we often encounter unique challenges that push us to explore the platform&#39;s depths. Recently, I tackled a fascinating task involving content migration and external linking, which led me down a rabbit hole of property management and ultimately, a much smoother solution. I wanted to share my journey, especially for anyone else who might encounter similar hurdles.&lt;/span&gt;&lt;/p&gt;
&lt;div class=&quot;MsoNormal&quot; style=&quot;text-align: center;&quot; align=&quot;center&quot;&gt;&lt;hr style=&quot;color: #1b1c1d;&quot; /&gt;&lt;/div&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;strong&gt;&lt;span style=&quot;mso-ansi-language: EN-US;&quot;&gt;The Scenario: A Tale of Two Companies&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;span style=&quot;mso-ansi-language: EN-US;&quot;&gt;Our company recently sold a subsidiary, and as part of the transition, we needed to facilitate a smooth redirection from their old content on our Optimizely site to their new home on another Optimizely instance. The core task was to convert existing pages on our site into external shortcuts, pointing to their corresponding URLs on the new company&#39;s website.&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;span style=&quot;mso-ansi-language: EN-US;&quot;&gt;I was provided with an Excel file containing two columns: &lt;strong&gt;FirstUrl&lt;/strong&gt; (the existing URL on our site) and &lt;strong&gt;SecondUrl&lt;/strong&gt; (the target URL on the new company&#39;s site). My goal was to create a custom add-on that would automate this process, turning hundreds of internal pages into external links.&lt;/span&gt;&lt;/p&gt;
&lt;div class=&quot;MsoNormal&quot; style=&quot;text-align: center;&quot; align=&quot;center&quot;&gt;&lt;hr style=&quot;color: #1b1c1d;&quot; /&gt;&lt;/div&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;strong&gt;&lt;span style=&quot;mso-ansi-language: EN-US;&quot;&gt;The Initial Approach: Properties and Puzzlement&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;span style=&quot;mso-ansi-language: EN-US;&quot;&gt;My first instinct, and a common approach when manipulating page data in Optimizely, was to leverage the Property collection. I knew that Optimizely pages have properties like PageShortcutType and PageShortcutLink to manage shortcuts. So, I started with this:&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;code&gt;writablePage.Property[&quot;PageShortcutType&quot;].Value = PageShortcutType.External;&lt;/code&gt;&lt;br /&gt;&lt;code&gt;writablePage.Property[&quot;PageShortcutLink&quot;].Value = importResponse[index].SecondUrl;&lt;/code&gt;&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;span style=&quot;mso-ansi-language: EN-US;&quot;&gt;The first line, setting PageShortcutType to PageShortcutType.External, worked perfectly. This correctly flagged the page as an external shortcut. However, the second line, where I attempted to set the PageShortcutLink with the SecondUrl from my Excel file, threw a rather cryptic exception:&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;code&gt;&lt;span style=&quot;mso-ansi-language: EN-US;&quot;&gt;{&quot;ContentReference: Input string was not in a correct format.&quot;}&lt;/span&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;span style=&quot;mso-ansi-language: EN-US;&quot;&gt;This was puzzling. The SecondUrl was a valid, absolute URL string. I double-checked the format, ensured there were no leading/trailing spaces, and even tried URL encoding &amp;ndash; nothing seemed to work. A lengthy search on Google and various Optimizely forums yielded no clear solution to this specific error when assigning a direct URL string to PageShortcutLink via the Property collection.&lt;/span&gt;&lt;/p&gt;
&lt;div class=&quot;MsoNormal&quot; style=&quot;text-align: center;&quot; align=&quot;center&quot;&gt;&lt;hr style=&quot;color: #1b1c1d;&quot; /&gt;&lt;/div&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;strong&gt;&lt;span style=&quot;mso-ansi-language: EN-US;&quot;&gt;The Technical Deep Dive: Why the Error?&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;span style=&quot;mso-ansi-language: EN-US;&quot;&gt;This exception, &quot;ContentReference: Input string was not in a correct format,&quot; strongly suggests that Optimizely&#39;s internal mechanisms for PageShortcutLink (when accessed via Property[&quot;PageShortcutLink&quot;]) are expecting a ContentReference object, not a raw URL string, even if the PageShortcutType is set to External.&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;strong&gt;&lt;span style=&quot;mso-ansi-language: EN-US;&quot;&gt;Here&#39;s the technical breakdown:&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ul style=&quot;margin-top: 0in;&quot;&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l0 level1 lfo1; tab-stops: list .5in;&quot;&gt;&lt;strong&gt;&lt;span style=&quot;mso-ansi-language: EN-US;&quot;&gt;PageShortcutLink&#39;s Internal Expectation:&lt;/span&gt;&lt;/strong&gt;&lt;span style=&quot;mso-ansi-language: EN-US;&quot;&gt; While logically one might expect to store any external URL, it appears that when you interact with PageShortcutLink through the generic Property collection, Optimizely&#39;s underlying type conversion or validation expects a ContentReference. This is likely a design choice to maintain consistency within its content management system, potentially allowing for future enhancements or stricter validation.&lt;/span&gt;&lt;/li&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l0 level1 lfo1; tab-stops: list .5in;&quot;&gt;&lt;strong&gt;&lt;span style=&quot;mso-ansi-language: EN-US;&quot;&gt;ContentReference Nuance:&lt;/span&gt;&lt;/strong&gt;&lt;span style=&quot;mso-ansi-language: EN-US;&quot;&gt; A ContentReference is Optimizely&#39;s way of uniquely identifying a piece of content (a page, block, media file, etc.) within its system. It&#39;s not designed to hold arbitrary external URLs. The exception was a clear indicator that my string wasn&#39;t being parsed into a valid ContentReference.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;span style=&quot;mso-ansi-language: EN-US;&quot;&gt;This highlights a crucial aspect of Optimizely development: sometimes, the generic Property accessors might have underlying type expectations that aren&#39;t immediately obvious, especially for properties that can link to both internal content and external URLs.&lt;/span&gt;&lt;/p&gt;
&lt;div class=&quot;MsoNormal&quot; style=&quot;text-align: center;&quot; align=&quot;center&quot;&gt;&lt;hr style=&quot;color: #1b1c1d;&quot; /&gt;&lt;/div&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;strong&gt;&lt;span style=&quot;mso-ansi-language: EN-US;&quot;&gt;The &quot;Aha!&quot; Moment: Direct Properties to the Rescue&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;span style=&quot;mso-ansi-language: EN-US;&quot;&gt;Frustrated but undeterred, I started looking for alternative ways to achieve the same outcome. And then, it hit me. Optimizely&#39;s PageData object, which represents a page, has directly exposed properties for managing shortcuts!&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;code&gt;&lt;span style=&quot;mso-ansi-language: EN-US;&quot;&gt;writablePage.LinkType = PageShortcutType.External;&lt;br /&gt;writablePage.LinkURL = importResponse[index].SecondUrl;&lt;/span&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;span style=&quot;mso-ansi-language: EN-US;&quot;&gt;I swapped out my problematic Property assignments for these direct properties, and to my immense relief, it worked flawlessly! No exceptions, no wrestling with content references, just clean, straightforward assignment.&lt;/span&gt;&lt;/p&gt;
&lt;div class=&quot;MsoNormal&quot; style=&quot;text-align: center;&quot; align=&quot;center&quot;&gt;&lt;hr style=&quot;color: #1b1c1d;&quot; /&gt;&lt;/div&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;strong&gt;&lt;span style=&quot;mso-ansi-language: EN-US;&quot;&gt;The Final Solution: Robust and Reliable&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;span style=&quot;mso-ansi-language: EN-US;&quot;&gt;Here&#39;s the complete code snippet that successfully accomplished the task:&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;for (int index = 0; index &amp;lt; excelData.Count; index++)&lt;/code&gt;&lt;br /&gt;&lt;code&gt;{&lt;/code&gt;&lt;br /&gt;&lt;code&gt;&amp;nbsp; &amp;nbsp; try&lt;/code&gt;&lt;br /&gt;&lt;code&gt;&amp;nbsp; &amp;nbsp; {&lt;/code&gt;&lt;br /&gt;&lt;code&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; // Resolve the existing page using the FirstUrl&lt;/code&gt;&lt;br /&gt;&lt;code&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; var content = &amp;nbsp;_urlResolver.Route(new EPiServer.UrlBuilder(excelData[index].FirstUrl)) as PageData;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; // Ensure we found a page&lt;/code&gt;&lt;br /&gt;&lt;code&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if (content != null)&lt;/code&gt;&lt;br /&gt;&lt;code&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; {&lt;/code&gt;&lt;br /&gt;&lt;code&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; // Create a writable clone to modify the page&lt;/code&gt;&lt;br /&gt;&lt;code&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; PageData writablePage = content.CreateWritableClone();&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; // Set the LinkType to External&lt;/code&gt;&lt;br /&gt;&lt;code&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; writablePage.LinkType = PageShortcutType.External;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; // Assign the external URL directly&lt;/code&gt;&lt;br /&gt;&lt;code&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; writablePage.LinkURL = excelData[index].SecondUrl;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; // Save the modified page&lt;/code&gt;&lt;br /&gt;&lt;code&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; _contentRepository.Save(writablePage, SaveAction.Patch, AccessLevel.NoAccess);&lt;/code&gt;&lt;br /&gt;&lt;code&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }&lt;/code&gt;&lt;br /&gt;&lt;code&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; else&lt;/code&gt;&lt;br /&gt;&lt;code&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; {&lt;/code&gt;&lt;br /&gt;&lt;code&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; // Handle cases where the URL doesn&#39;t resolve to a page&lt;/code&gt;&lt;br /&gt;&lt;code&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; viewModel.ImportResponse[index].Errors.Add(new ValidationResult($&quot;Record for &#39;{excelData[index].FirstUrl}&#39; couldn&#39;t be found or resolved.&quot;));&lt;/code&gt;&lt;br /&gt;&lt;code&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }&lt;/code&gt;&lt;br /&gt;&lt;code&gt;&amp;nbsp; &amp;nbsp; }&lt;/code&gt;&lt;br /&gt;&lt;code&gt;&amp;nbsp; &amp;nbsp; catch (Exception ex)&lt;/code&gt;&lt;br /&gt;&lt;code&gt;&amp;nbsp; &amp;nbsp; {&lt;/code&gt;&lt;br /&gt;&lt;code&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; // Log the exception for debugging&lt;/code&gt;&lt;br /&gt;&lt;code&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; // _logger.LogError(ex, &quot;Error processing record for URL: {Url}&quot;, excelData[index].FirstUrl);&amp;nbsp;&lt;/code&gt;&lt;br /&gt;&lt;code&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; viewModel.ImportResponse[index].Errors.Add(new ValidationResult(&quot;Record couldn&#39;t be updated. Please validate the data again. &quot; + ex.Message));&lt;/code&gt;&lt;br /&gt;&lt;code&gt;&amp;nbsp; &amp;nbsp; }&lt;/code&gt;&lt;br /&gt;&lt;code&gt;}&lt;/code&gt;&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;strong&gt;&lt;span style=&quot;mso-ansi-language: EN-US;&quot;&gt;Technical Additions to the Final Code:&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ul style=&quot;margin-top: 0in;&quot;&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l1 level1 lfo2; tab-stops: list .5in;&quot;&gt;&lt;strong&gt;&lt;span style=&quot;mso-ansi-language: EN-US;&quot;&gt;Error Handling and Logging:&lt;/span&gt;&lt;/strong&gt;&lt;span style=&quot;mso-ansi-language: EN-US;&quot;&gt; I&#39;ve added a more specific error message if the _urlResolver.Route method doesn&#39;t find a page, which is crucial for real-world scenarios. Also, I&#39;ve commented out a placeholder for logging the exception (_logger.LogError), which is a best practice for debugging and monitoring in production environments.&lt;/span&gt;&lt;/li&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l1 level1 lfo2; tab-stops: list .5in;&quot;&gt;&lt;strong&gt;&lt;span style=&quot;mso-ansi-language: EN-US;&quot;&gt;Null Check for content:&lt;/span&gt;&lt;/strong&gt;&lt;span style=&quot;mso-ansi-language: EN-US;&quot;&gt; It&#39;s vital to check if _urlResolver.Route actually returns a PageData object. If the FirstUrl doesn&#39;t correspond to an existing page, content would be null, leading to a NullReferenceException.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;MsoNormal&quot; style=&quot;text-align: center;&quot; align=&quot;center&quot;&gt;&lt;hr style=&quot;color: #1b1c1d;&quot; /&gt;&lt;/div&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;strong&gt;&lt;span style=&quot;mso-ansi-language: EN-US;&quot;&gt;Lessons Learned&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;span style=&quot;mso-ansi-language: EN-US;&quot;&gt;This experience reinforced a couple of key lessons for me:&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;margin-top: 0in;&quot;&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l2 level1 lfo3; tab-stops: list .5in;&quot;&gt;&lt;strong&gt;&lt;span style=&quot;mso-ansi-language: EN-US;&quot;&gt;Prefer Direct Properties When Available:&lt;/span&gt;&lt;/strong&gt;&lt;span style=&quot;mso-ansi-language: EN-US;&quot;&gt; While the Property collection is powerful for dynamic or custom properties, always check if there are direct properties on the PageData (or ContentData) object for common functionalities. These direct properties often encapsulate the correct underlying logic and type handling.&lt;/span&gt;&lt;/li&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l2 level1 lfo3; tab-stops: list .5in;&quot;&gt;&lt;strong&gt;&lt;span style=&quot;mso-ansi-language: EN-US;&quot;&gt;Understand Optimizely&#39;s Internal Expectations:&lt;/span&gt;&lt;/strong&gt;&lt;span style=&quot;mso-ansi-language: EN-US;&quot;&gt; The &quot;ContentReference&quot; error was a strong hint that Optimizely had a specific type in mind, even if my string &lt;em&gt;looked&lt;/em&gt; like what was needed. Debugging these specific error messages often points to how Optimizely&#39;s APIs are designed internally.&lt;/span&gt;&lt;/li&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l2 level1 lfo3; tab-stops: list .5in;&quot;&gt;&lt;strong&gt;&lt;span style=&quot;mso-ansi-language: EN-US;&quot;&gt;Community and Documentation are Key (but not always exhaustive):&lt;/span&gt;&lt;/strong&gt;&lt;span style=&quot;mso-ansi-language: EN-US;&quot;&gt; While I initially struggled to find an exact solution for my specific error, the journey of looking through documentation and forums eventually led me to consider alternative approaches.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;span style=&quot;mso-ansi-language: EN-US;&quot;&gt;This task, while seemingly simple on the surface, provided a valuable opportunity to deepen my understanding of Optimizely&#39;s content model and property management. It&#39;s these kinds of challenges that truly help us grow as Optimizely developers!&lt;/span&gt;&lt;/p&gt;
&lt;div class=&quot;MsoNormal&quot; style=&quot;text-align: center;&quot; align=&quot;center&quot;&gt;&lt;hr style=&quot;color: #1b1c1d;&quot; /&gt;&lt;/div&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>            <guid>https://world.optimizely.com/blogs/amit-mittal/dates/2025/7/troubleshooting-optimizely-shortcuts-why-pageshortcutlink-threw-an-error-and-the-solution/</guid>            <pubDate>Wed, 09 Jul 2025 05:59:16 GMT</pubDate>           <category>Blog post</category></item><item> <title>Create Environment Banner that highlight current environment to editors!</title>            <link>https://world.optimizely.com/blogs/amit-mittal/dates/2023/8/create-environment-banner-that-highlight-current-environment-to-editors/</link>            <description>&lt;p&gt;Recently I got a request from editors, sometimes they forgot to check the browser URL and keep working, on wrong environment, and publish the content to wrong environment. for example, one of the editors was testing something on prep environment, and something came in between to small update to production, unintentionally he updated prop environment and thought he has done his job. So, editors requested if can we have banner on top of every page that indicate the current environment, It could help us to minimize the human error like this?&lt;br /&gt;&lt;img src=&quot;/link/512c7fa1c3624d79a20122fa90bca5e2.aspx&quot; width=&quot;1011&quot; height=&quot;89&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Let us see how I fulfill this requirement.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;First of all, I created a new component class in business folder like this.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp; &amp;nbsp; [Component(PlugInAreas = &quot;/episerver/cms/action&quot;,&amp;nbsp;&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; Categories = &quot;cms&quot;,&amp;nbsp;&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; WidgetType = &quot;yourCompanyName/environments/environmentHighlighter&quot;)&amp;nbsp;&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ]&lt;br /&gt;&amp;nbsp; &amp;nbsp; public class EnvironmentHighlighterComponent&lt;br /&gt;&amp;nbsp; {}&lt;br /&gt;There are three important part in component&lt;/p&gt;
PlugInAreas:- it defines where in cms the content of Component should display. there are various ways to get the value for PlugInAreas is to Use&amp;nbsp;&lt;br /&gt;EPiServer.Shell.PlugInArea class&lt;br /&gt;&lt;img src=&quot;/link/7ded8b45015a470fb27305d6a8798aac.aspx&quot; width=&quot;233&quot; height=&quot;102&quot; /&gt;&lt;br /&gt;But for my requirement I did not found any value that could be used for global menu area in EPiServer.Shell.PlugInArea. So, I decided to check what pre-defined Components are available into the system. Among various predefined Components Toolbar was most closed to my requirement&lt;br /&gt;&lt;img src=&quot;/link/41d516a5612240dbb565a29beda7011a.aspx&quot; width=&quot;291&quot; height=&quot;136&quot; /&gt;&lt;br /&gt;on further peek on the Toolbar definition, I found &quot;/episerver/cms/action&quot; PlugInAreas worked for me.&lt;br /&gt;&lt;img src=&quot;/link/0cd59044f34c4fceb57c5ac4ec177def.aspx&quot; width=&quot;299&quot; height=&quot;29&quot; /&gt;&lt;/li&gt;
&lt;li&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;ol&gt;
&lt;li&gt;Categories: I wanted to show the component for all cms pages, so I simply put &quot;cms&quot; as a category. other value could be &quot;dashboard&quot;.&lt;/li&gt;
&lt;li&gt;&amp;nbsp; WidgetType: this value belongs to various setings of your ClientResources, for example Editors, widgets etc. for our case we used&amp;nbsp;&lt;br /&gt;&quot;yourCompanyName/environments/environmentHighlighter&quot;&lt;/li&gt;
&lt;ol&gt;
&lt;li&gt;yourCompanyName is widget namespace name that will be used in module.config file.&lt;/li&gt;
&lt;li&gt;environments are folder path in ClientResources folder.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;environmentHighlighter is js file name where Dojo code is going to be written. like this&lt;/li&gt;
&lt;/ol&gt;
&lt;/ol&gt;
&lt;br /&gt;&lt;img src=&quot;/link/2a35eb54f38b40e1940674368ca3598f.aspx&quot; width=&quot;260&quot; height=&quot;121&quot; /&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Second part is update or create new module.config file at the root of the website and update like this&lt;/strong&gt;&lt;br /&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&amp;gt;&lt;br /&gt;&amp;lt;module&amp;gt;&lt;br /&gt;&amp;nbsp;&lt;strong&gt;&amp;nbsp;&amp;lt;dojoModules&amp;gt;&lt;/strong&gt;&lt;br /&gt;&lt;strong&gt;&amp;nbsp; &amp;nbsp; &amp;lt;add name=&quot;yourcompanyname&quot; path=&quot;Scripts/widgets&quot; /&amp;gt;&lt;/strong&gt;&lt;br /&gt;&lt;strong&gt;&amp;nbsp; &amp;lt;/dojoModules&amp;gt;&lt;/strong&gt;&lt;br /&gt;&amp;nbsp; &amp;lt;dojo&amp;gt;&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;lt;paths&amp;gt;&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;lt;add name=&quot;yourcomapanyname&quot; path=&quot;Scripts&quot; /&amp;gt;&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;lt;/paths&amp;gt;&lt;br /&gt;&amp;nbsp; &amp;lt;/dojo&amp;gt;&lt;br /&gt;&amp;lt;/module&amp;gt;&lt;/p&gt;
&lt;p&gt;in the above config file, we are trying to map widgets folder with ClientResources folder.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Third part is to create Dojo script file. in our case file name is environmentHighlighter.js file.&lt;/strong&gt;&lt;br /&gt;define([&lt;br /&gt;&amp;nbsp; &quot;dojo/_base/declare&quot;,&lt;br /&gt;&amp;nbsp; &quot;dijit/_WidgetBase&quot;,&lt;br /&gt;&amp;nbsp; &quot;dijit/_TemplatedMixin&quot;&lt;br /&gt;],&lt;br /&gt;&amp;nbsp; function (declare, _WidgetBase, _TemplatedMixin)&lt;br /&gt;&amp;nbsp; {&lt;br /&gt;&amp;nbsp; &amp;nbsp; return declare(&quot;yourcomapanyname/environments/environmentHighlighter&quot;,&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; [&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; _WidgetBase,&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; _TemplatedMixin&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; ],&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; {&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; templateString: dojo.cache(&quot;/EnvironmentHighlighter/Index&quot;)});&lt;br /&gt;&amp;nbsp; });&lt;/p&gt;
&lt;p&gt;there are two important parts in above script.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;declare statement: the value is same as WidgetType value.&lt;/li&gt;
&lt;li&gt;&amp;nbsp;templateString statement tell the dojo from where the html should come from, in our case EnvironmentHighlighter is a controller and Index is a method.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Four and final part is create controller, view and setup route, these parts are self-explanatory&lt;/strong&gt;&lt;br /&gt;Controller&lt;br /&gt;public class EnvironmentHighlighterController : Controller&lt;br /&gt;&amp;nbsp; &amp;nbsp; {&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; public ActionResult Index()&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; {&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; return PartialView();&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }&lt;br /&gt;&amp;nbsp; &amp;nbsp; }&lt;br /&gt;View:&lt;/p&gt;
&lt;p&gt;@{&lt;br /&gt;&amp;nbsp; &amp;nbsp; var currentEnvironment = Environments.CurrentEnvironment;&lt;br /&gt;&amp;nbsp; &amp;nbsp; var greenColor = &quot;#2cd31f&quot;;&lt;br /&gt;&amp;nbsp; &amp;nbsp; var orangeColor = &quot;#ff6a00&quot;;&lt;br /&gt;&amp;nbsp; &amp;nbsp; var colorScheme = currentEnvironment == Environments.Environment.Production.ToString() ? orangeColor : greenColor;&lt;br /&gt;}&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center;&quot;&amp;gt;&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;lt;div style=&quot;width:300px; margin:0 auto; background:@colorScheme; color:#FFF;&quot;&amp;gt;&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; You are working on @currentEnvironment environment&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;lt;/div&amp;gt;&lt;br /&gt;&amp;lt;/div&amp;gt;&lt;br /&gt;&lt;br /&gt;Routing:&amp;nbsp;&lt;br /&gt;RouteTable.Routes.MapRoute(&quot;EnvironmentHighlighter&quot;, &quot;environmenthighlighter/Index&quot;, new { controller = &quot;EnvironmentHighlighter&quot;, action = &quot;Index&quot; });&lt;br /&gt;&lt;br /&gt;That&amp;rsquo;s it!&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;</description>            <guid>https://world.optimizely.com/blogs/amit-mittal/dates/2023/8/create-environment-banner-that-highlight-current-environment-to-editors/</guid>            <pubDate>Mon, 07 Aug 2023 12:48:34 GMT</pubDate>           <category>Blog post</category></item></channel>
</rss>