Migrate users from Identity Server v3 (ASP.NET Core) to Auth0

I was originally looking for some guidance along the lines of this question regarding importing users from an Identity Server v3 Microsoft SQL Server database to Auth0 with the User Import/Export extension.

Specifically I wanted to transmit the password hash from Identity Server to Auth0 so our users could continue logging in with the same password after we migrated authentication providers.

Here’s my documentation of the process I used to generate the JSON import file in the format required for the User Import/Export extension as I wasn’t able to find it anywhere else - although I gained a significant amount of information from Andrew Lock’s blog post Migrating passwords in ASP.NET Core Identity with a custom PasswordHasher which covers the composition of the Identity Server password hash in great detail.

My method “ImportUsersAsync” takes a list of IdentityUsers as a parameter
(these are the users from Identity Server that we want to import), maps them to a list of Auth0 users, then serializes that list to JSON and writes the JSON to a file stream.

 public async Task<FileStreamResult> ImportUsersAsync(List<IdentityUser> users)
        {            
            List<Auth0User> usersToImport = new List<Auth0User>();

            foreach (var user in users)
            {
                var auth0User = new Auth0User()
                {
                    Id = user.Id.ToString(),
                    Email = user.Email,
                    Name = (string.IsNullOrEmpty(user.Name) ? user.Email : user.Name + (string.IsNullOrEmpty(user.Surname) ? "" : " " + user.Surname)),
                    EmailConfirmed = user.EmailConfirmed
                };

The password hash is stored in the database as a Base64 encoded string. “Hash” is a misleading term, for the field because only one part of the string is actually the password hash; converting the string to a byte array it comprises:

  • Identity Server version (1st byte) - whether the password hash comes from a v2 (value = 0) or v3 (value = 1) Identity Server
  • KDF (2nd - 5th byte) - for Identity Server v3 en enum value for the key derivation function, e.g. “HMACSHA256” = 1
  • Iterations (6th - 9th byte) - the number of iterations required to generate the hash
  • Salt Size (10th - 13th byte) - the size of the salt (always 16)
  • Salt (14th - 29th byte) - the salt
  • Hash (30th - 61st byte) - finally, the password hash

I’m indebted to Rui Figuereido for documenting all of this in his blog post “Anatomy of an ASP.NET Identity PasswordHash

Here’s the code to pull out all the constituent parts from the Base64 encoded string:

// Convert the stored Base64 password to bytes
                byte[] decodedPasswordHash = Convert.FromBase64String(user.PasswordHash);

                int identityVersion = decodedPasswordHash[0];
                byte[] byteArray = new byte[4];

                Array.Copy(decodedPasswordHash, 1, byteArray, 0, 4);
                Array.Reverse(byteArray);
                uint key = BitConverter.ToUInt32(byteArray);

                Array.Copy(decodedPasswordHash, 5, byteArray, 0, 4);
                Array.Reverse(byteArray);
                uint iterations = BitConverter.ToUInt32(byteArray);

                Array.Copy(decodedPasswordHash, 9, byteArray, 0, 4);
                Array.Reverse(byteArray);
                uint saltSize = BitConverter.ToUInt32(byteArray);

                byteArray = new byte[16];
                Array.Copy(decodedPasswordHash, 13, byteArray, 0, 16);
                string salt = Convert.ToBase64String(byteArray).Replace('=', ' ').Trim(' ');

                byteArray = new byte[32];
                Array.Copy(decodedPasswordHash, 29, byteArray, 0, 32);
                string hash = Convert.ToBase64String(byteArray).Replace('=', ' ').Trim(' ');

Note how the byte arrays containing unsigned ints need to be reversed with

Array.Reverse(byteArray);

This is explained in Rui’s blog post.

Once we have the constituent hash parts they need to be merged into a PHC string format. The only parts we need are the number of iterations, salt and hash:

 var pbkdf2String = ($"$pbkdf2-sha256$i={iterations},l=32${salt}${hash}");

Now I can use that string in a POCO I’ve created to store the custom password hash in the format required by the JSON file - and that POCO is a property of the Auth0User class:

auth0User.CustomPasswordHash = new CustomPasswordHash()
                {
                     Algorithm = "pbkdf2",
                     Hash = new Hash()
                     {
                         Value = pbkdf2String,
                         Encoding = "utf8"
                     }
                 };

Then serialize the user list and create the JSON file:

var jsonFile = $"UserImport-{DateTime.Now:yyyy-MM-dd-HH-mm}.json";
var jsonContent = JsonSerializer.Serialize(usersToImport);

MemoryStream memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(jsonContent));
           
return new FileStreamResult(memoryStream, "text/json");

Hope that helps someone!

2 Likes

This was EXTREMELY helpful and allowed us to move our users from our Asp.net authentication system to Auth0.

The one hiccup we had that we managed to figure out is the pbkdf2String needed to reference sha512, not sha256.

 var pbkdf2String = ($"$pbkdf2-sha512$i={iterations},l=32${salt}${hash}");

Our system is built on dotnet 7, and it was only after checking out the aspnet core password hash source code that I thought to change the cypher. Once that change was made, everything worked great!

Thanks for putting this post together, we would have been lost without it.

1 Like