user907290
user907290

Reputation:

How do I loosely couple the Blazor Identity scaffold with my own Database Context?

I've created a Blazor Server App with the option to scaffold an identity system. This created an Entity Framework IdentityDbContext with a number of tables to manage user logins and settings. I decided to keep my own DbContext separate from this so that I could replace either of the contexts later, if necessary.

What I would like to do is have a User entity in my own custom dbcontext, and in it store a reference to the user id of the scaffolded IdentityDbContext entity. I would also like to ensure that I don't have to query the db for the custom entity every time the user opens a new page.

I've been looking around StackOverflow trying to find good suggestions of how to approach this, but I'm still not sure how to start. So I have a few questions:

  1. Is my approach a sensible one?
  2. How do I find a permanent id number or string to couple with on the UserIdentity?
  3. Should I store my custom user entity in some sort of context so I don't have to query it all the time? If so, how?

All help is greatly appreciated!

Upvotes: 0

Views: 276

Answers (1)

Neil W
Neil W

Reputation: 9162

It looks like your requirement is to store custom information about the current user above and beyond what is stored in Identity about the current user.

For simpler use cases you can create your own User class derived from IdentityUser and add additional properties on there and let Identity take care of all persistence and retrieval.

For more complex use cases you may follow the approach you have taken, whereby you create your own tables to store user related information.

It seems that you have taken the second approach.

Is my approach a sensible one?

I think so. Burying lots of business-specific context about the user in the Identity tables would tightly bind you to the Identity implementation.

How do I find a permanent id number or string to couple with on the UserIdentity?

IdentityUser user = await UserManager<IdentityUser>.FindByNameAsync(username);
string uniqueId = user.Id;

// or, if the user is signed in ...
string uniqueId = UserManager<IdentityUser>.GetUserId(HttpContext.User);

Should I store my custom user entity in some sort of context so I don't have to query it all the time? If so, how?

Let's say you have a class structure from your own DbContext that stores custom information about the user, then you can retrieve that when the user signs in, serialize it, and put it in a claim on the ClaimsPrincipal. This will then be available to you with every request without going back to the database. You can deserialize it from the Claims collection as needed and use it as required.

How to ...

Create a CustomUserClaimsPrincipalFactory (this will add custom claims when the user is authenticated by retrieving data from ICustomUserInfoService and storing in claims):

public class CustomUserClaimsPrincipalFactory 
    : UserClaimsPrincipalFactory<ApplicationUser, IdentityRole>
{
    private readonly ICustomUserInfoService _customUserInfoService;

    public CustomUserClaimsPrincipalFactory(
        UserManager<ApplicationUser> userManager,
        RoleManager<IdentityRole> roleManager,
        IOptions<IdentityOptions> optionsAccessor,
        ICustomUserInfoService customUserInfoService) 
            : base(userManager, roleManager, optionsAccessor) 
    {
        _customUserInfoService= customUserInfoService;
    }

    protected override async Task<ClaimsIdentity> GenerateClaimsAsync(
        ApplicationUser user)
    {
        var identity = await base.GenerateClaimsAsync(user);

        MyCustomUserInfo customUserInfo = 
            await _customUserInfoService.GetInfoAsync(); 


        // NOTE:
        // ... to add more claims, the claim type need to be registered
        // ... in StartUp.cs : ConfigureServices
        // e.g 
        //services.AddIdentityServer()
        //    .AddApiAuthorization<ApplicationUser, ApplicationDbContext>(options =>
        //    {
        //        options.IdentityResources["openid"].UserClaims.Add("role");
        //        options.ApiResources.Single().UserClaims.Add("role");
        //        options.IdentityResources["openid"].UserClaims.Add("my-custom-info");
        //        options.ApiResources.Single().UserClaims.Add("my-custom-info");
        //    });
        List<Claim> claims = new List<Claim>
        {
            // Add serialized custom user info to claims
            new Claim("my-custom-info", JsonSerializer.Serialize(customUserInfo))
        };
        identity.AddClaims(claims.ToArray());

        return identity;
    }
}

Register your CustomUserInfoService in Startup.cs (your own service to get your custom user info from the database):

services.AddScoped<ICustomUserInfoService>(_ => new CustomUserInfoService());

Register Identity Options (with your CustomUserClaimsPrincipalFactory and authorisation in Startup.cs. NOTE: addition of "my-custom-info" as a registered userclaim type. Without this your code in CustomUserInfoService will fail to add the claim type "my-custom-info":

services.AddDefaultIdentity<IdentityUser>(options =>
    {
        options.SignIn.RequireConfirmedAccount = false;
        options.User.RequireUniqueEmail = true;
    })
    .AddRoles<IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddClaimsPrincipalFactory<CustomUserClaimsPrincipalFactory>();

services.AddIdentityServer()
    .AddApiAuthorization<ApplicationUser, ApplicationDbContext>(options =>
    {
        options.IdentityResources["openid"].UserClaims.Add("role");
        options.ApiResources.Single().UserClaims.Add("role");
        options.IdentityResources["openid"].UserClaims.Add("my-custom-info");
        options.ApiResources.Single().UserClaims.Add("my-custom-info");
    });

You can then retrieve your custom user info from claims, without returning to database, by using:

MyCustomUserInfo customUserInfo =
    JsonSerializer.Deserialize<MyCustomUserInfo>( 
        HttpContext.User.Claims
            .SingleOrDefault(c => c.Type == "my-custom-info").Value);

Upvotes: 1

Related Questions