Migrating from ASP.NET Identity 2

Thanks for the approval, love the stack! I’ve done a lot of reading up on Authentication with ServiceStack:

All of the documentation I’ve read a few times, and am wondering if there is a better way than my band-aid implementation for authentication? It feels a little rough around the edges. I’ve got production data (around 70 tables, roughly 3k users), all of which reference AspNetUsers (legacy ASP.NET Identity / Auth system) - 40 FK’s.

What I’ve done is extend (read: migrate) the AspNetUsers table to include all properties needed by the UserAuth object, and my T4 Generated POCO (AspNetUser) now extends UserAuth:

[Alias("AspNetUsers")]
public class AspNetUser : UserAuth
{ ... }

This feels a little off here, because now my ServiceModel project references ServiceStack.Auth, but moving on…

I wrote a custom AuthProvider, inheriting from CredentialsAuthProvider, that uses an ASP.NET Core DI Injected IPasswordHasher that I register in the IoC (that way I can re-use the legacy “PasswordHash” check), if the check fails I pass back to the base.TryAuthenticate method:

public class CustomAuthProvider : CredentialsAuthProvider
    {
        public override bool TryAuthenticate(IServiceBase authService, string userName, string password)
        {
            var dbFactory = authService.ResolveService<IDbConnectionFactory>();
            
            var passwordHasher = authService.ResolveService<IPasswordHasher<AspNetUser>>();

            using (var db = dbFactory.Open())
            {
                var user = db.Single<AspNetUser>(x => x.Email == userName || x.UserName == userName);

                if (user == null)
                    return false;

                var result = passwordHasher.VerifyHashedPassword(user, user.PasswordHash, password);

                switch (result)
                {
                    case PasswordVerificationResult.Success: //Same Version Identity
                    case PasswordVerificationResult.SuccessRehashNeeded: // Legacy Version Hash (which we will likely get coming from pre ASP.NET Core Identity)
                        return true;
                    default:
                        return base.TryAuthenticate(authService, userName, password); //delegate to default hash/salt check from ServiceStack
                }
            }
        }

        public override IHttpResult OnAuthenticated(IServiceBase authService, IAuthSession session, IAuthTokens tokens, Dictionary<string, string> authInfo)
        {
            return base.OnAuthenticated(authService, session, tokens, authInfo);
        }
    }

Is there a better way? This works, when I use a legacy password, it authenticates, when I use the wrong legacy password, it fails.

Where are roles stored (the master list, I see that there is a UserAuthRole table)?

If you need to work with an existing table then you’d need to implement a custom AuthProvider as you’ve done. Note for resolving IOC dependencies you should instead use TryResolve, e.g:

var dbFactory = authService.TryResolve<IDbConnectionFactory>();

As ResolveService is only meant for Resolving ServiceStack Services. Services are also resolved from the IOC which is why it works now, but that API may not work in future as it’s specifically for resolving Services.

Where are roles stored (the master list, I see that there is a UserAuthRole table)?

It’s up to the AuthProvider to populate the AuthUserSession Roles/Permissions, the UserAuthRole is a support table can be used by Auth Repositories that prefer to maintain Roles/Permissions in a separate table, e.g. OrmLiteAuthRepository when registered with UseDistinctRoleTables=true. As you’re using an existing DB Table and Custom AuthProvider, you’re not using an existing AuthRepository so it’s up to your CustomAuthProvider to populate the Users Roles/Permissions on the AuthUserSession. If you don’t maintain existing Roles/Permissions you can use the existing UserAuthRole class for your Table if you’d like, but it’s not required.

Ahh ok gotcha, thank you for the reply. Is it typical for me to expose the AspNetUser : UserAuth? Wouldn’t clients of my ServiceModel.dll file now have to take a dependency on ServiceStack.Auth as well?

Now that I’m looking at all this again, it looks like there are several paths I could take:

  1. Copy all users (including legacy hash password) to UserAuth table (and change all foreign keys) - this would be a very backward-incompatible way (a legacy site + new site could not exist). Use the code from above to continue authenticating with legacy hash or new hash.
  2. Use what I have above, ensuring to migrate users “role” assignments to the new system (UserAuthRole table) upon first sign in. Making sure to properly populate the session in OnAuthenticated as well. This would allow the rest of ServiceStack to take over role management. Backwards-compatible wise this would be a little strange (what happens if a user gets a new role in the legacy system?)
  3. Use what I have above plus write a full CustomAuthRepository to point to my role tables. This would ensure full compatibility and allow users to “try” the beta site while in transition.

Other questions:

What is the cardinality of UserAuth > UserAuthRole? Is that a one to many? Is the Role and Permission column a singular value or a blob of json?

The only dependency your DTOs should reference is the dep/logic-free ServiceStack.Interfaces, this ensures maximum interoperability of your Services as your clients don’t depend on any server logic.

I wouldn’t go for a mixed solution, i.e. either migrate completely to ServiceStack’s UserAuth tables so you can use the built-in CredentialsAuthProvider and OrmLiteAuthRepository or retain your existing db tables and use your CustomAuthProvider. If you have existing Roles/Permissions have your CustomAuthProvider populate them when they create the UserSession or if you prefer you can use a CustomUserSession and provide a custom implemention for the HasRole/HasPermission APIs which is what ServiceStack uses to determine if the User has the Role/Permission.

It’s 1:M with the Role/Permission columns containing the name of the role. The only Role used in ServiceStack is the Admin role which is used to allow access to protected features, e.g. Un/Assign Roles, Request Logging, Metadata Debug, etc.

I don’t know if this will help anyone in the future, but if anyone stumbles on this post, I detailed everything I went through in a blog post. My primary goal was to allow legacy users (created in asp.net identity) to login to the new (ServiceStack) system, and new users created in SS Auth to login to the old system.

1 Like

@la2texas Awesome post thx for sharing! Also great to see you’re making use of the new IPasswordHasher interface. If you’re migrating from ASP.NET Identity v3 the password hashes will be the same so ideally you shouldn’t need a Custom PasswordHasher. I wouldn’t throw for the Version property, it represents the 1st byte format marker for the password hash. For v2 it’s 0x00 and for v3 it’s 0x01. ServiceStack uses this in its Config.FallbackPasswordHashers to be able to migrate to new Password format whilst still support being able to login using an older format.

No problem!

Oh that makes sense, I’ve dug through that and wondered what that byte was about.

So if I were to instead remove my CustomPasswordHasher, and new up the default SS PasswordHasher with Version = 0x00, would password hashing work as expected with my old V2 user hashes?

We’ve only implemented a Password Hasher for V3, if you were to implement a Password Hasher for V2 and add it to Config.FallbackPasswordHashers ServiceStack will fallback to attempting to Authenticate with the fallback impl and if the attempt was successful it will reshash the password with the current IPasswordHasher (V3).

This requires participation from the AuthRepository which uses ServiceStack APIs for verifying passwords:

if (userAuth.VerifyPassword(password, out var needsRehash))
{
    this.RecordSuccessfulLogin(userAuth, needsRehash, password);
    return true;
}

Which is used by all ServiceStack’s AuthRepositories, if you’re using a Custom AuthRepository it will also need to use the existing APIs.

Ahh ok gotcha. So for my use case (I don’t want any rehashing to occur until the legacy app is decommissioned), what I’ve done will suffice for now.

1 Like