2

I've implemented a custom User Store for ASP.NET Identity by following the example set here. That all works fine, except for this:

I need access to data about the currently logged in user in my user store. Normally, you'd access that by accessing

HttpContext.Current.User

Now, once auser has logged in, if he user then goes to the Manage controller (e.g. to try and change his/her password), when ASP.NET identity looks up the user again by calling

CustomUserManager.FindByIdAsync(string userId)

HttpContext.Current is empty altogether (that's prior to rendering the page). So, how do I get information about the HttpContext in this scenario? The user is properly logged in, so how do I figure out which user has been logged in?

@edit.. the problem is in CustomUserStore.. here's a bit of it

   public class CustomUserStore<TUser> : IUserStore<TUser>, IUserLoginStore<TUser>, IUserClaimStore<TUser>, IUserPasswordStore<TUser>, IUserSecurityStampStore<TUser>, IUserEmailStore<TUser>, IUserPhoneNumberStore<TUser>, 
    IUserLockoutStore<TUser, string>, IUserTwoFactorStore<TUser, string>//, IQueryableUserStore<TUser> 
    where TUser: CustomUser<string>, IUser<string>
{

    string storageFile = @"c:\temp\aspnetusers.json";
    List<TUser> users;

    public CustomUserStore()
    {
        if (File.Exists(storageFile))
        {
            string contents = File.ReadAllText(storageFile);
            users = JsonConvert.DeserializeObject<List<TUser>>(contents);
            if (users == null)
                users = new List<TUser>();
        }
        else
            users = new List<TUser>();
    }

    #region IUserStore implementation

    public Task<TUser> FindByIdAsync(string userId)
    {
        string sessionId = HttpContext.Current?.Session?.SessionID;
        return Task.FromResult<TUser>(users.FirstOrDefault(u => u.Id == userId));
    }

    public Task<TUser> FindByNameAsync(string userName)
    {
        string sessionId = HttpContext.Current?.Session?.SessionID;
        return Task.FromResult<TUser>(users.FirstOrDefault(u => string.Compare(u.UserName, userName, true) == 0));
    }

    #endregion
}

and it's in the FindByAsync method where HttpContext.Current can be empty.

It happens in the Index method of the AccountController when the model is created

   var model = new IndexViewModel
        {
            HasPassword = HasPassword(),
            PhoneNumber = await UserManager.GetPhoneNumberAsync(userId),
            TwoFactor = await UserManager.GetTwoFactorEnabledAsync(userId),
            Logins = await UserManager.GetLoginsAsync(userId),
            BrowserRemembered = await AuthenticationManager.TwoFactorBrowserRememberedAsync(userId)
        };

And it's the FindById request in the HasPassword method that causes the problem

private bool HasPassword()
    {
        var user = UserManager.FindById(User.Identity.GetUserId());
        if (user != null)
        {
            return user.PasswordHash != null;
        }
        return false;
    }

The other 4 requests to the user manager all have a filled out HttpContext.Current. So it appears that it's calls to UserManager that cause the issue.

user3566056
  • 224
  • 1
  • 12
  • 1
    Can you show the code where your `HttpContext.User` is empty? Most likely there is no Http request at that time, hence your problem – trailmax Aug 04 '17 at 10:03
  • Okay, having identified where the problem happens, the solution is obvious bug begs another question. If I rewrite the above to use an async method for HasPassword, and use UserManager.FindByIdAsync, then HttpContext.Current always has a value. But why is that not the case when calling sync methods on the UserManager? Smells like a bug to me.. perhaps UserManager internally calls the Async method and does not restore the context? Time to go check out the source code – user3566056 Aug 04 '17 at 10:21

1 Answers1

1

Having identified the exact source of the problem, it's easy enough to fix.

Add this async emthod to check if the user has a password:

private async Task<bool> HasPasswordAsync()
    {
        var user = await UserManager.FindByIdAsync(User.Identity.GetUserId());
        if (user != null)
        {
            return user.PasswordHash != null;
        }
        return false;
    }

And in the Index method, use the new async methode

var model = new IndexViewModel
    {
        HasPassword = await HasPasswordAsync(),
        PhoneNumber = await UserManager.GetPhoneNumberAsync(userId),
        TwoFactor = await UserManager.GetTwoFactorEnabledAsync(userId),
        Logins = await UserManager.GetLoginsAsync(userId),
        BrowserRemembered = await AuthenticationManager.TwoFactorBrowserRememberedAsync(userId)
    };

But, why does the synchronous method call break things? You'd imagine the sync call would run into the standard context where HttpContext.Current should be available.

I have a more custom User Store in my real project where I run into this problem a lot more frequently.. guess I need to check if contains a lot more synchronous access to UserManager methods.

user3566056
  • 224
  • 1
  • 12