Flutter CTO Report 2024
Get insights about Flutter directly from CTOs, CIOs, Tech Leads, and Engineering Managers!

Identity Management Solutions, Part III: How to migrate to Ory Kratos?

Paweł Frąckiewicz
Backend Developer at LeanCode
Sep 14th, 2024 • 15 min

Following up on our previous article in which we described different identity management solutions, in this one, we will focus on the process of migrating an application from ASP.NET Core Identity to Ory Kratos. Kratos is an open-source solution for implementing identity management outside the main application’s process. It can be used via the Ory Network, a fully hosted infrastructure, but it can be self-hosted as well. When migrating from ASP.NET Core Identity, a couple of steps should be followed to ensure a smooth transition between the two.

A little bit of theory

Before we delve into the migration, let’s start with some basics of Kratos that are required to understand the process.

Importing users

Sooner or later, when migrating from one identity management system to another, a need to migrate all existing user data will arise. Fortunately, Ory Kratos allows importing identities from other systems via its API.


For importing existing identities, one should use the same Admin API endpoint as for creating new ones. To import an identity, a POST request must be sent to https://{your-kratos-url}/admin/identities. An example of a valid request body may look like this:

{
  "schema_id": "preset://email",
  "traits": {
    "email": "example@email.com"
  },
  "credentials": {
    "password": {
      "config": {
        "password": "password123"
      }
    }
  }
}

The body of the request should specify an ID of the identity schema that the created identity will use. Then, all of the traits should be provided, as defined in the identity schema. As said previously, you can also specify metadata, but it is optional. Finally, the “credentials” field must be present and contain the password of a user whose identity is being imported.

Apart from the general “traits,” there are two major things that need further explanation for a proper import to take place - passwords and social connections.

Passwords

In the example above, a password has been provided as plain text. For security reasons, this is not recommended, and there is a strong chance that a developer does not have user passwords stored in plain text anyway. Because of this, Ory Kratos allows importing hashed passwords as well. Ory Kratos can accept and handle passwords hashed by one of the following algorithms, among others:

  • BCrypt
  • Argon2
  • MD5
  • SSHA, SSHA256, SSHA512
  • PBKDF2
  • SCrypt
  • FirebaseSCrypt

Out of these, BCrypt or Argon2 can be configured to serve as the main hasher of an Ory Kratos instance. If the password has been hashed by an algorithm other than the one configured to be the main one, Ory Kratos is able to compare and migrate the hash upon the user's first successful login. To achieve this, a hashed password must be specified in a specific format defined by Ory Kratos, which is dependent on the algorithm with which a password has been hashed.


For a simple example, let’s take a look at how to import a password that has been hashed with the MD5 algorithm. Let’s assume that the original password is password123. Then, an MD5 hash of it is 482c811da5d5b4bc6d497ffa98491e38. Ory Kratos specifies that MD5 hashes should be provided using the format:

$md5$<hash>

Where hash is an MD5 hash of the plain text password encoded to Base64. In our case, it will be SCyBHaXVtLxtSX/6mEkeOA==. The final version of the “credentials” field included in a request should then look like this:

"credentials": {
   "password": {
     "config": {
       "hashed_password": "$md5$SCyBHaXVtLxtSX/6mEkeOA=="
     }
   }
 }

Please keep in mind that this is only an example, and the MD5 algorithm has been deemed unsafe to use in production environments. Let’s take a look at a more complex example with the PBKDF2 algorithm, which is used to hash passwords in ASP.NET Identity.

Again, let’s assume that the original password was “password123”, and ASP.NET Identity created the following hash for it:

AQAAAAEAACcQAAAAEDceKqflaxHBUt4Bc1gIw2Q6RVp2BLnKNeFewAEiejA/Mh8O7YCA99SbkoTv/NtYcQ==

Ory Kratos documentation specifies the format that passwords encrypted using the PBKDF2 algorithm should be provided in: 
$pbkdf2-<algorithm>$i=<iteration>,l=<length>$<salt>$<hash>


ASP.NET Core Identity

Fortunately for us, ASP.NET Core Identity uses a parallel format for storing passwords. It differs in the encoding, but the overall idea is the same - you encode the algorithm and its parameters and then encode salt & hash directly in the storage. It also uses the same mechanism to derive keys, which is not weird, considering that both follow “best practices”.

ASP.NET Core Identity supports two algorithms:

  1. PBKDF2 with HMAC-SHA1, and
  2. PBKDF2 with HMAC-SHA512.

Plus, the parameters for the algorithms can be customized further by the user. This is fully compatible with Ory Kratos, and we only need to change the format.

Social connections

Ory Kratos makes it possible for users to sign in using third-party identity providers. Consequently, developers have a way to import social sign-in connections for their project. To achieve this, the ‘credentials'  field in the request has to include the ‘oidc’ field in which all the social connections of the identity being created should be described. For each provider, its ID, set in Ory Kratos social sign-in configuration, should be provided. Apart from that, the ID of the user on a given platform should be present as well. The ID of the user is usually the same as the “sub” claim received from an OIDC provider. An example of a request importing a social sign-in connection may look like this:


{
 "schema_id": "preset://email",
 "traits": {
   "email": "docs-oidc@example.org"
 },
 "credentials": {
   "oidc": {
     "config": {
       "providers": [
         {
           "provider": "github",
           "subject": "12345"
         },
         {
           "provider": "google",
           "subject": "12345"
         }
       ]
     }
   }
 }
}

ASP.NET Core Identity

Unfortunately, ASP.NET Core Identity does not standardize how social connections should be modeled. Moreover, ASP.NET Core favors using social sign-ins directly, without an intermediary “user” in the database. This is great for smaller apps if you don’t need to maintain a fully-fledged user profile, but if you need to have that, and we would say that most of the apps need that, then you are out of luck - you have to come up with your solution.


We at LeanCode have a solution for that (for ASP.NET Core Identity + IdentityServer4 combo), but every app will have different ways of storing that.

Identifiers

There’s one other thing missing - identifiers. Since we’re migrating data from a running system, we probably should preserve the ID of every existing user. Unfortunately, this is not as straightforward as it sounds.


There are two problems here. First, ASP.NET Core Identity does not limit what the identifier is - it can be Guid, it can be a random string, it can be a database-generated identifier (e.g., MongoDB’s ObjectId), or any value that can be stored in the database basically. Kratos requires the ID to be a valid UUID. The other problem is that Kratos does not allow you to set an ID when importing an identity. One could say that the latter problem is more limiting, but I would do that.

Fortunately, there is a workaround - we can store the imported ID in admin metadata, as described earlier. For migrated users, the app would use the ID from metadata; for new users registered after the migration, the app could use the Kratos-assigned metadata or an identity-modifying webhook could be configured so that the imported ID is also set for new users.

This requires some work on the app side and might introduce split-brain-like problems if someone mistakes one ID with the other, but we don’t think there is any other choice.

Migration

Knowing all that, the migration process is quite straightforward - for every user stored in ASP.NET Core Identity, we need to call the import endpoint with transformed data. The only hard thing is password hash reencoding. Then, the authentication middleware needs to be replaced with one that understands Kratos tokens, and that is basically it. This covers only the backend side of integration - one still needs to change how the frontend authenticates with Kratos.

Let’s go through the steps necessary to do the migration.

Step 1 - read all users

First, we need a UserManager from our app (or at least one that is able to talk to our app) or at least a DbContext with users. For the sake of this tutorial, let’s assume it’s the former:

UserManager<User> userManager;


We will also need IdentityApi from Ory.Client NuGet package:

IdentityApi identityApi;

Then, we need to iterate over all the users - we need to migrate them one by one, so there is no point in downloading all of them at once (some batching would be nice, but let’s skip that):

await foreach (var user in userManager.Users.AsAsyncEnumerable())
{

Step 2 - map the user

Now, we need to map the ASP.NET Core Identity’s user to the CreateUser request. In the most basic case, this will look as follows:

var body = new Ory.Client.Model.ClientCreateIdentityBody(
    // The schema that the user will be created with, needs to be adjusted
    schemaId: "preset://email",
    // This is the mapping of all the user properties from the ASP.NET Core Identity to Kratos traits.
    // It should probably be adjusted to your needs.
    traits: new Dictionary<string, object> { ["email"] = user.Email ?? "", },
    // This maps user addresses (e-mail) to Kratos verifiable addresses - adjust it to your needs.
    verifiableAddresses:
    [
        new(
            via: "email",
            value: user.Email ?? "",
            verified: user.EmailConfirmed,
            status: user.EmailConfirmed ? "completed" : "pending"
        )
    ],
    metadataAdmin: new { imported_id = user.Id, }
);

Step 3 - map password and OIDC data

Then, we need to decode the password from ASP.NET Core Identity and encode it the way Kratos demands. This is a slightly more involved process, but it boils down to this code:

if (user.PasswordHash is { } ph)
{
    body.Credentials = new(password: new(new(ReencodePasswordHash(ph))));
}
 // Here, one can add OIDC data to `body.Credentials.Oidc` if needed.


The user might not have the password set (e.g., they registered with OIDC only), so we need to handle that. Apart from that, we can migrate the OIDC credentials, but as stated previously, it will depend on the solution used in the real codebase and will vary greatly.

Decoding and encoding helpers

There are two versions of the hash - v2 and v3. Both are somewhat undocumented, but fortunately, we have access to the code.

Kratos needs a couple of parameters to store the hash:

  1. The algorithm to derive subkeys. In our case, this will always be PBKDF2.
  2. The hashing algorithm is used as part of the derivation process. In our case, this will be either SAH1- or SHA512-based HMAC.
  3. The number of iterations used in PBKDF2.
  4. Salt and subkey.

We need to store it somewhere, so let’s introduce a record:


record Password(byte[] Salt, byte[] Subkey, string ShaVersion, uint IterationCount);


Then, we can decode the hash. First, v2. It basically uses a predefined hash and a predefined number of iterations, so we only have to copy data here and there:


private static Password DecodeV2(byte[] passwordHash)
{
    var salt = passwordHash[1..17];
    var subkey = passwordHash[17..];
    return new Password(salt, subkey, "sha1", 1000);
}

V3 is slightly harder to work with, as it stores some parameters in the hash directly - the hash can be generated with salt of varying length, with configurable iterations and hashing algo, but nothing too complex:


private static Password DecodeV3(byte[] passwordHash)
{
    var prf = (KeyDerivationPrf)BinaryPrimitives.ReadUInt32BigEndian(passwordHash[1..]);
    var iterCount = BinaryPrimitives.ReadUInt32BigEndian(passwordHash[5..]);
    var saltLength = (int)BinaryPrimitives.ReadUInt32BigEndian(passwordHash[9..]);
    var salt = passwordHash[13..(13 + saltLength)];
    var subkey = passwordHash[(13 + saltLength)..];
    return new Password(salt, subkey, ToName(prf), iterCount);
}

Now, we can encode the password according to the required format:

string Encode(Password password)
{
    return $"$pbkdf2-{password.ShaVersion}$i={password.IterationCount},l
{password.Subkey.Length}${Base64Encode(password.Salt)}${Base64Encode(password.Subkey)}";
}

Base64Encode encodes the data as base64 but trims the padding, as it is not used by Kratos (and would fail if provided with that).

Lastly, we need to select the proper branch according to the version of the hash. This should be done based on the first byte of the hash (which, in the DB, is base64-encoded, so we need to decode that also):

string ReencodePasswordHash(string passwordHash)
{
    var bytes = Convert.FromBase64String(passwordHash);
    var decoded = bytes[0] switch
    {
        0x00 => DecodeV2(bytes),
        0x01 => DecodeV3(bytes),
        _ => throw new NotSupportedException()
    };
    return Encode(decoded);
}

Step 4 - migrate

To finish the migration, we also need to create the user in Kratos. This can be done with a single call to the API since we prepared everything beforehand:

await identityApi.CreateIdentityAsync(body);

And that’s it!

Step 5 - adjust authentication

Last but not least, adjusting authentication mechanisms in your app is also necessary. Since ASP.NET Core comes with built-in authentication mechanisms, it is necessary to extend them. Unfortunately, there is no official integration, but we prepared one ourselves. To use it, you extend the AddAuthentication calls like this:

public override void ConfigureServices(IServiceCollection services)
{
    services
        .AddAuthentication()
        .AddKratos(options =>
        {
            options.NameClaimType = KnownClaims.UserId;
            options.RoleClaimType = KnownClaims.Role;
            options.ClaimsExtractor = (s, o, c) =>
            {
                // Every identity is a valid User
                c.Add(new(o.RoleClaimType, "user"));
            };
        });
    services.AddKratosClients(builder =>
    {
        // Kratos public endpoint
        builder.AddFrontendApiClient("");
        // Kratos admin endpoint
        builder.AddIdentityApiClient("");
    });
}


You can read more about it in our docs.


Summary

The migration process from ASP.NET Core Identity to Ory Kratos is straightforward, provided that you know where to look. Some things (like identifiers) require more work, but overall, the process is quite seamless and can be easily automated.

We’ve shown the code necessary to do the migration. We’ve also prepared an app that does the migration for you. Consider it a template - it needs some adjustments to avoid losing your data (it only migrates emails and passwords!), and the configuration might not be adequate for your deployment. Still, it has all the necessary scaffolding to do the migration. Fill in the missing things, and you’ll be good to go!

Meet our expert

Let's schedule a talk!

Jakub Fijałkowski
Head of Backend at LeanCodejakub.fijalkowski@leancode.pl
Jakub Fijałkowski, Head of Backend at LeanCode
Rate this article
5.00 / 5 Based on 1 reviews

You may also like

Identity Management Solutions, Part I: Introduction

With the changing landscape of identity management, at LeanCode we faced the challenge of selecting a new identity management solution that would be our default for the coming years. We want to share with you the whole journey. Find out more about our approach to this task.
Choosing Indentity and Access Management solution

Identity Management Solutions, Part II: Firebase Auth, Supabase, Keycloak, Auth0 & Ory Kratos

Selecting the best identity management solution is hard. Many options are available on the market, each with its own pros and cons. We compared the most obvious choices with the less popular ones. See our comparison of Firebase Auth, Supabase, Keycloak, Auth0 & Ory Kratos.
Choosing Indentity and Access Management solution

Sharing Logic Across Multiple Platforms

When a project grows, sooner or later, a need for sharing logic across multiple clients and a server arises. At LeanCode, we completed a project that extensively employed logic sharing successfully. In this article, we highlight the steps and architecture decisions of the picked solution.
Sharing Logic Across Multiple Platforms - Solution by LeanCode