4

I'm trying to create a fairly simple intranet application that will use Active Directory for authentication, and will use the AspNetRoles table to check if the user is in a certain application role. This app is just an in-house lottery where some users can create events/contests that other users can then submit an entry to the contest. I'm thinking of starting out with 2 basic roles:

  • Administrator - Can perform CRUD operations on "Event" or "Contest" entities
  • Contestant - Can perform GET operations on "Contest" entities, and can create new "Entry" entities.

Here's where I'm stuck: I've got Windows Authentication working in the sense that from a controller, I can do a User.Identity.Name and see my domain login name. Furthermore, I can verify that an account belongs to a domain group by doing User.IsInRole("Domain Users"). If I want to avoid creating new AD groups for each role in my application (let's say design changes down the road require additional roles), how can I use Authorization on controllers to check against Application Roles?

Here's an example controller I want to use:

[Route("api/[controller]")]
[Authorize(Roles = "Contestant")]
public class EventTypesController : Controller
{
    private IRaffleRepository _repository;
    private ILogger<EventTypesController> _logger;

    public EventTypesController(IRaffleRepository repository, ILogger<EventTypesController> logger)
    {
        _repository = repository;
        _logger = logger;
    }

    [HttpGet("")]
    public IActionResult Get()
    {
        try
        {
            var results = _repository.GetAllEventTypes();
            return Ok(Mapper.Map<IEnumerable<EventTypeViewModel>>(results));
        }
        catch (Exception ex)
        {
            _logger.LogError($"Failed to get all event types: {ex}");
            return BadRequest("Error occurred");
        }
    }
}

In my Startup.cs, in ConfigureServices, I'm wiring up Identity as follows:

services.AddIdentity<RaffleUser, ApplicationRole>()
            .AddEntityFrameworkStores<RaffleContext>();

My RaffleUser class is really just the default implementation of IdentityUser:

public class RaffleUser : IdentityUser
{

}

My ApplicationRole class is also just the default implementation of IdentityRole. I also tried seeding some data in a seed class:

if (!await _roleManager.RoleExistsAsync("Administrator"))
{
    var adminRole = new ApplicationRole()
    {
        Name = "Administrator"
    };
    await _roleManager.CreateAsync(adminRole);
    await _context.SaveChangesAsync();
}

if (await _userManager.FindByNameAsync("jmoor") == null)
{
    using (var context = new PrincipalContext(ContextType.Domain))
    {
        var principal = UserPrincipal.FindByIdentity(context, "DOMAIN\\jmoor");
        if (principal != null)
        {
            var user = new RaffleUser()
            {
                Email = principal.EmailAddress,
                UserName = principal.SamAccountName
            };

            await _userManager.CreateAsync(user);
            await _context.SaveChangesAsync();

            var adminRole = await _roleManager.FindByNameAsync("Administrator");
            if (adminRole != null)
            {
                await _userManager.AddToRoleAsync(user, adminRole.Name);
                await _context.SaveChangesAsync();
            }
        }
    }
}

The data makes it to the tables, but it just seems like at the controller level, I need to convert the authenticated user to an IdentityUser. Do I need some middleware class to do this for me? Would that be the best way to make authorization reusable on all controllers?

2 Answers 2

2

First, I ended up creating a custom ClaimsTransformer that returns a ClaimsPrincipal populated with UserClaims and RoleClaims (after refactoring my app, I decided to go with policy-based authorization, and the access claim can be added at either the role or user level):

public async Task<ClaimsPrincipal> TransformAsync(ClaimsTransformationContext context)
{
    var identity = (ClaimsIdentity)context.Principal.Identity;
    var userName = identity.Name;
    if (userName != null)
    {
        var user = await _userManager.FindByLoginAsync("ActiveDirectory", userName);
        if (user != null)
        {
            identity.AddClaims(await _userManager.GetClaimsAsync(user));
            var roles = await _userManager.GetRolesAsync(user);
            identity.AddClaims(await GetRoleClaims(roles));
        }
    }
    return context.Principal;
}

private async Task<List<Claim>> GetRoleClaims(IList<string> roles)
{
    List<Claim> allRoleClaims = new List<Claim>();
    foreach (var role in roles)
    {
        var rmRole = await _roleManager.FindByNameAsync(role);
        var claimsToAdd = await _roleManager.GetClaimsAsync(rmRole);
        allRoleClaims.AddRange(claimsToAdd);
    }
    return allRoleClaims;
}

I wired that up in the Startup.cs:

services.AddScoped<IClaimsTransformer, Services.ClaimsTransformer>();

I also went with Policy-based authorization:

services.AddAuthorization(options =>
{
    options.AddPolicy("Administrator", policy => policy.RequireClaim("AccessLevel", "Administrator"));
    options.AddPolicy("Project Manager", policy => policy.RequireClaim("AccessLevel", "Project Manager"));
});

So, users or roles can have a claim set with a name of "AccessLevel" and a value specified. To finish everything off, I also created a custom UserManager that just populates the User object with additional details from ActiveDirectory during a CreateAsync.

Sign up to request clarification or add additional context in comments.

Comments

1

You need to add a DefaultChallangeScheme to use Windows authentication. This is how i do, but if someone has a better solution i am all ears :)

I use the following setup in my current application.

services.AddIdentity<ApplicationUser, ApplicationRole>()
            .AddEntityFrameworkStores<SecurityDbContext>()
            .AddDefaultTokenProviders();

services.AddAuthentication(options =>
{
            options.DefaultChallengeScheme = IISDefaults.AuthenticationScheme;
});

Then i put in my application claims in a transformer.

services.AddTransient<IClaimsTransformation, ClaimsTransformer>();

I hope this will get you in the right direction.

3 Comments

oops. Didnt see the date of your question. I hope you solved it long ago.
I haven't tested all my controllers thoroughly, but it does look like this accomplishes what I was looking for. I'll post the alternative I came up with, which is also using the newer policy-based authorization.
Also use 'options.DefaultScheme = IISDefaults.AuthenticationScheme;'

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.