17
votes

I am creating an enterprise intranet ASP.NET Core MVC application. I want my users to authenticate using Active Directory and I want user authorizations (claims) stored in ApplicationDbContext.

I assume that I need to use Microsoft.AspNetCore.Identity and Microsoft.AspNetCore.Identity.EntityFrameworkCore to accomplish my goals. What is the best practice for storing ASP.NET Core Authorization claims when authenticating against Active Directory?

The following code will give me access to the current windows user security context (current logged in user), from within the pipeline. Somehow I need to map the user with associated Microsoft.AspNetCore.Identity claims?

 app.Use(async (context, next) =>
 {
      var identity = (ClaimsIdentity) context.User.Identity;
      await next.Invoke();
 });

Thanks in advance for the help!

2
Hi @Will, has it been solved?Ssss
@Will the HttpContext.User should have identity and user details for you to identify and map the user to identitylazy

2 Answers

4
votes

I don't think there is a best practice around this, but you do have many options. Perhaps you could elaborate a little on what you're trying to achieve? Here's a few pointers that may help you sort some of this out. Just a question...what are you using to authenticate against AD under the hood? If you use IdentityServer or something similar then I would recommend leaning towards option 4.

Option 1 - Store Claims as an ApplicationUser property

First off, claims are simply key-value pairs. This means that you could create a structure such as:

public class UserClaim 
{
    public string UserName {get; set;}
    public string Key {get; set;}
    public string Value {get; set;}
}

This would allow you to store the claims in your ApplicationUser class.

Option 2 - Storing Claims Using UserManager

Better yet, you could leverage the built in identity components by injecting a UserManager<ApplicationUser> userManager into the class where you wish to add the user claims, and then call AddClaim() with the appropriate values. Because of the DI system in Core, you're free to do this in any class that will be activated by the runtime.

Option 3 - Storing Claims in Their Own Table

An other approach would be to augment the UserClaim class with a UserName property, then use the unique identifier of the principle (User.Identity.Name). Then you could store that in a DbSet<UserClaim> in your ApplicationDbContext and fetch the claims by user name.

Option 4 - Don't Store the Claims

If you just need to access the claims, and not store them (I'm not sure of your intentions from your question) then you might be best just to access the User property if you're in a controller, provided you're using an authentication service that hydrates the claims correctly.

The User is decorated with the claims that belong to the user that signed in to your app and is available on every controller.

You can otherwise obtain the ClaimsPrinciple and access the claims of the user in that manner. In that case (outside of a controller) what you would do is to add an IHttpContextAccessor accessor to the constructor of your class and navigate to the HttpContextAccessor.HttpContext.User property, which is again a ClaimsPrinciple.

0
votes

This was my approach:

Implement the MyUser Class

public class MyUser: IdentityUser
{
}

Use ASP.Net Core DbContext in a separate "security" DBContext (I prefer bounded contexts responible for their own functionality)

public class SecurityDbContext : IdentityDbContext<MyUser>
{
    public SecurityDbContext(DbContextOptions<SecurityDbContext> options)
        : base(options)
    { }
}

Create a claims transformer. This is used to add the claims from the store into the ClaimsIdentity (The User object in your HttpContext)

public class ClaimsTransformer : IClaimsTransformer
{
    private readonly SecurityDbContext _context;
    private readonly UserManager<MyUser> _userManager;

    public ClaimsTransformer(SecurityDbContext context, UserManager<MyUser> userManager)
    {
        _context = context;
        _userManager = userManager;
    }

    public Task<ClaimsPrincipal> TransformAsync(ClaimsTransformationContext context)
    {
        var identity = context.Principal.Identity as ClaimsIdentity;
        if (identity == null) return Task.FromResult(context.Principal);

        try
        {
            if (context.Context.Response.HasStarted)
                return Task.FromResult(context.Principal);

            var claims = AddClaims(identity);
            identity.AddClaims(claims);
        }
        catch (InvalidOperationException)
        {
        }
        catch (SqlException ex)
        {
            if (!ex.Message.Contains("Login failed for user") && !ex.Message.StartsWith("A network-related or instance-specific error"))
                throw;
        }

        return Task.FromResult(context.Principal);
    }

    private IEnumerable<Claim> AddClaims(IIdentity identity)
    {
        var claims = new List<Claim>();
        var existing = _userManager.Users.Include(u => u.Claims).SingleOrDefault(u => u.UserName == identity.Name);
        if (existing == null) return claims;

        claims.Add(new Claim(SupportedClaimTypes.ModuleName, Constants.ADGroupName));
        claims.AddRange(existing.Claims.Select(c => c.ToClaim()));

        return claims;
    }
}

You will need to add a few things into your startup.cs class, I have only added the pertinent ones here:

public void ConfigureServices(IServiceCollection services)
{
    services.AddIdentity<FreightRateUser, IdentityRole>(config =>
            {
                config.User.AllowedUserNameCharacters =
                    "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@\\";
            config.User.RequireUniqueEmail = false;
            config.Cookies.ApplicationCookie.LoginPath = "/Auth/Login";
        })
        .AddEntityFrameworkStores<SecurityDbContext>();
}

Don't forget the Configure method

public async void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    app.UseIdentity();
}

Finally you'll need some View to add the appropriate User/claims to the Identity tables, I have a controller with a couple of Actions (only showing Create here):

    [AllowAnonymous]
    public IActionResult CreateAccess()
    {
        var vm = new AccessRequestViewModel
        {
            Username = User.Identity.Name
        };
        return View(vm);
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    [AllowAnonymous]
    public async Task<IActionResult> CreateAccess(AccessRequestViewModel viewModel)
    {
        if (User == null || !User.Identity.IsAuthenticated) return View("Index");

            var newUser = new MyUser
            {
                UserName = viewModel.Username
            };
            var x = await _userManager.CreateAsync(newUser);
            if (!x.Succeeded)
                return View(ModelState);

            var myClaims = new List<Claim>();
            if (viewModel.CanManageSecurity)
                myClaims.Add(new Claim(SupportedClaimTypes.Security, "SITE,LOCAL"));
            if (viewModel.CanChangeExchangeRates)
                myClaims.Add(new Claim(SupportedClaimTypes.ExchangeRates, "AUD,USD"));
            if (viewModel.CanChangeRates)
                myClaims.Add(new Claim(SupportedClaimTypes.Updates, "LOCAL"));
            if (viewModel.CanManageMasterData)
                myClaims.Add(new Claim(SupportedClaimTypes.Admin, "SITE,LOCAL"));

            await _userManager.AddClaimsAsync(newUser, myClaims);
        }
        return View("Index");
    }

When the user is saved and you display the authenticated page again (Index) the claims transformer will load the claims into the ClaimsIdentity and all should be good.

Please Note, this is for the current HttpContext user, with will want to do the create for other users in the Create, as you obviously don't want the current user giving themselves access.