Search code examples
c#asp.net-identitycouchbaseidentityserver3

Why does my Couchbase custom storage provider for ASP.NET Identity not persist changes?


Context

I'm implementing a single sign-on server where users, roles, claims etc. will be persisted in Couchbase. So far my steps have been:

  1. Implement IdentityServer3 in an MVC web application (using the v2.5.4 NuGet package).
  2. Implement IdentityServer3.AspNetIdentity to use Identity as the backing store for users and other entities (using the v2.0.0 NuGet package).
  3. Implement IdentityManager (1.0.0-beta5-5 NuGet package) and IdentityManager.AspNetIdentity (1.0.0-beta5-1 NuGet package) for a simple back-end user management UI.
  4. Implement the couchbase-aspnet-identity Identity custom storage provider from Couchbase Labs. This is included as source (from GitHub) in my project as it was only ever released to NuGet as an incomplete developer preview which has since been withdrawn.

When steps 1-3 were implemented everything worked perfectly. Users were stored via EF in the usual AspNetUsers table in a LocalDB instance (the default implementation for IdentityServer3.AspNetIdentity), and I could use the IdentityManager UI to add, edit and delete users.

Step 4 involved implementing custom store classes which are basically taken straight from the couchbase-aspnet-identity project on GitHub with some minor local changes (e.g. implementing the UserStore.Users getter which throws NotImplementedException in the original code).

The problem

Now when I create a user it's stored in Couchbase as expected. If I edit a custom field added to my custom ApplicationUser class (e.g. FirstName or Age), the change is persisted correctly. So far so good.

However, if I edit the user's password, e-mail address or phone number (in the IdentityManager UI), the following happens:

  • IdentityManager UI shows a success message saying the change was successful (see below)
  • However, the edited field does not reflect the change (also seen below)
  • The change hasn't been persisted in Couchbase, so obviously when the user is re-loaded the change is not shown

Stepping through the code I discovered that multiple calls are made to my UserStore.UpdateAsync method:

public async Task UpdateAsync(T user)
{
    await _bucket.UpdateAsync(user.Id, user);
}

On each call, the user is correctly persisted to Couchbase. So in fact the problem isn't that changes aren't persisted at all, it's that they're persisted and then overwritten with the original values.

For example, if I change the user's phone number the method is called three times. Changing it from empty to 123, the relevant fields in my user object are as follows:

Call 1

  • PhoneNumber: 123 (the new value entered in the UI)
  • PhoneNumberConfirmed: false

Call 2

  • PhoneNumber: 123
  • PhoneNumberConfirmed: true

Call 3

  • PhoneNumber: null (the original value)
  • PhoneNumberConfirmed: false (the original value)

(The object also has a different SecurityStamp value each time but otherwise the object values are identical.)

The same thing happens when I change the e-mail address or password. (For the password there are only two calls, but in each case the last call resets all fields to their original values.)

The question

What is causing the extra call to UserStore.UpdateAsync() and how do I fix it?

Unfortunately I can't step any higher up the call stack: I think UserStore is called from UserManager, which is part of Microsoft.AspNet.Identity.Core, for which I don't have source.

Extra details

My UserStore class implements all the optional interfaces discussed here, i.e. it looks like this:

public class UserStore<T> :
    IUserLoginStore<T>,
    IUserClaimStore<T>,
    IUserRoleStore<T>,
    IUserSecurityStampStore<T>,
    IQueryableUserStore<T>,
    IUserPasswordStore<T>,
    IUserPhoneNumberStore<T>,
    IUserStore<T>,
    IUserLockoutStore<T, string>,
    IUserTwoFactorStore<T, string>,
    IUserEmailStore<T>
    where T : IdentityUser
{
    // ...
}

Here's a screenshot of the IdentityManager UI when I've just changed the phone number to 123. (Note the success message but the empty Phone field.)

enter image description here


Solution

  • I managed to get to the bottom of this. The fact that it worked with the default EF implementation (step 3 in my question) but failed when I swapped this out for the Couchbase storage provider meant I initially suspected the Couchbase provider was faulty (especially as it was based on developer preview code).

    In fact the issue is in the IdentityManager.AspNetIdentity package, specifically the method AspNetIdentityManagerService.SetUserPropertyAsync():

    public virtual async Task<IdentityManagerResult> SetUserPropertyAsync(string subject, string type, string value)
    {
        TUserKey key = ConvertUserSubjectToKey(subject);
        var user = await this.userManager.FindByIdAsync(key);
    
        // [...]
    
        var metadata = await GetMetadataAsync();
        var propResult = SetUserProperty(metadata.UserMetadata.UpdateProperties, user, type, value);
        if (!propResult.IsSuccess)
        {
            return propResult;
        }
    
        var result = await userManager.UpdateAsync(user);
        if (!result.Succeeded)
        {
            return new IdentityManagerResult(result.Errors.ToArray());
        }
    
        return IdentityManagerResult.Success;
    }
    

    If you step far enough into the SetUserProperty() call you get to AspNetIdentityManagerService.SetPhone() which looks like this:

    public virtual IdentityManagerResult SetPhone(TUser user, string phone)
    {
        var result = this.userManager.SetPhoneNumber(user.Id, phone);
    
        // [...]
    }
    

    Because we're only passing the user ID into SetPhoneNumber(), the UserManager asks the UserStore for another instance of the user (via a call to UserStore.FindByIdAsync()), so the PhoneNumber property is never updated on the instance that was passed into SetUserProperty(). Therefore when we reach the call to userManager.UpdateAsync(user) in SetUserPropertyAsync(), we're passing in a stale object without the changes applied.

    Presumably EF ensures the two instances used here are one and the same, but other providers don't. I've compared the Couchbase provider with other more mature implementations (e.g. AspNet.Identity.Mongo) and the Couchbase code looks sound.