Newsletter
Subscribe if you want to know how to build digital products
What do you do in IT?
By submitting your email you agree to receive the content requested and to LeanCode's Privacy Policy

How to Migrate from ASP.NET Core Identity to Ory Kratos?

Paweł Frąckiewicz - Backend Developer at LeanCode
Paweł Frąckiewicz - Backend Developer at LeanCode
Sep 14, 2024 • 15 min
Indentity Management Migrating to Ory Kratos
Paweł Frąckiewicz - Backend Developer at LeanCode
Paweł Frąckiewicz
Backend Developer at LeanCode
Indentity Management Migrating to Ory Kratos

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.

Ory Kratos defines identity as a set of data describing a user who can sign in to an application. It is vital to understand that, during migration, you have to basically map every “user” from ASP.NET Core Identity to an identity matching the schema.

An identity schema consists of numerous fields describing a specific person, like their name and surname, phone number, email address, etc. However, it is easy to imagine a scenario where an application has different types of users with different data associated with them. Let’s imagine a case of an online shop that distinguishes between two types of accounts: customers and employees. It is probable that the identity data of a customer will be different than that of an employee. For instance, an employee may have an employee number the customer doesn’t have. Ory Kratos provides an elegant way to define different types of users and the data each type holds using a concept called identity schemas.

Each identity schema implements the JSON Schema Standard and defines the fields that can be stored as a part of identity. Two types of data can be stored as a part of identity. The first type is called traits. They define identity information, which can be modified by both the user they relate to and the admin. Traits are usually only informative and are not used by any part of the application concerned with protecting resources. 

The second type of data, called metadata, should be used for that. A metadata is an information which can only be modified by the admin and not by the user. Because of that, it allows data to be stored, which can then be used to grant or deny access to specific resources, like the user's role. Metadata is further divided into public and admin types. The user cannot modify public metadata, but is returned to them in session details. Conversely, admin metadata, as the name suggests, can be viewed only by the admin and is hidden from users altogether.

When defining identity schemas, each of them must specify an identifier. It is a field that a user must provide during sign-up and serves as a unique identifier of the user during subsequent sign-ins. Usually, it is an email address or a username. An important caveat is that identifiers must be unique across all schemas. For example, one schema could define the email address to be an identifier, and the other specifies the username for the purpose. Then, if someone creates an identity with the username “example@email.com” using the second schema, a person with that email won’t be able to sign up using the first one. Also, a developer can specify a set of fields for each identity schema that can be used for identity verification or recovery for actions such as password retrieval via email.

Moreover, Ory Kratos requires that exactly one of the identity schemas is designated as a default one. It is used when a new user registers using a self-service flow.

Because Ory Kratos allows users to sign up using a third-party identity provider such as Google, Apple, etc., there is a need to define a way in which the claims received from an external provider are mapped to the default identity schema. This can be achieved by creating a Jsonnet mapper that extracts and maps received claims to the identity traits. A separate mapper should be created for each supported external identity provider. If some traits defined in the schema are not present in the payload received from the identity provider, the user is redirected to the registration page and prompted to fill them out before an identity is added to Ory Kratos.

Finally, it is worth keeping in mind that even though identity schemas are a powerful mechanism, allowing for much freedom when it comes to defining what kind of user data is stored, their configuration may prove difficult, especially at the beginning. Ory Kratos has some identity schema presets that can be used for quick prototyping or even some simple production scenarios. For a more complex one, additional effort will be required to configure all the identity schemas to fulfill the business logic requirements.

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 LeanCode
Jakub Fijałkowski
Head of Backend at LeanCodejakub.fijalkowski@leancode.pl
Jakub Fijałkowski, Head of Backend at LeanCode
Rate this article
Star 1Star 2Star 3Star 4Star 5
5.00 / 5 Based on 4 reviews

You may also like

What is Identity and Access Management (IAM)? 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 Compared: Ory Kratos vs Firebase Auth vs Supabase vs Keycloak vs Auth0

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