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!