ASP.NET Core 2.1 Windows Authentication with DB Roles for Authorization Part 1
I call this part 1 because I haven't figured it all out yet, so if you have a solution to how I can use the Authorize Attribute in a controller, please let me know in the comments.
EDIT
See part 2 for the solution I came up with: Part 2
EDIT
See part 2 for the solution I came up with: Part 2
In this project I am injecting database user roles as claims into the windows authentication security principal claims. The role provider that existed in the full .net environment no longer exists, and the new approach is to use claims. Claims can include roles, but they can also be policy based. For example, lets say you have a section of your site that someone needs to be a certain age to view, you can create a claim that verifies the users birth date before authorizing this area. This gives you a lot more flexibility than regular roles do. The problem with using windows authentication is that by default the claims list are groups populated by AD. The procedure below adds application roles to that list of claims. The AD roles are still there, the database roles are just added to them.
Since ASP.NET Core is capable of running on multiple environments, authorization is handled by different middle ware providers. The authentication is being provided by IIS and is being passed to the .net core application.
Data
The data folder holds the classes used for the dbcontext.
SecurityContext.cs This class is used to create a db context that can be used by ASP.NET core and is setup for dependency injection in the startup.cs file. This context sets up the relationship between UserInformations and UserRoles and defines the many to many relationship between the two using the modelbuilder.
SecurityDbContextFactory.cs This class wraps the dbContext in a factory, so you can access the database directly from a test project, by passing in a connection string.
Model
The classes in this folder define the model for the security users.
UserInformation.cs This class is used to define the user information which is associated with the user's lan id.
public class UserInformation
{
[Key]
public int UserInformationId { get; set; }
[MaxLength(25),Display(Name = "LAN ID")]
public String LanId { get; set; }
[MaxLength(25), Display(Name = "First Name")]
public string FirstName { get; set; }
[MaxLength(25), Display(Name = "Last Name")]
public string LastName { get; set; }
[MaxLength(100)]
public string Email { get; set; }
public bool Enabled { get; set; }
public virtual ICollection<UserInformationUserRole> UserInformationUserRoles { get; set; }
}
UserRole.cs This class is used to define the application roles.
public class UserRole
{
[Key]
public int UserRoleId { get; set; }
[MaxLength(25)]
public string Name { get; set; }
[MaxLength(100)]
public string Description { get; set; }
public virtual ICollection<UserInformationUserRole> UserInformationUserRoles { get; set; }
}
UserInformationUserRole.cs This class us used to define the many to many relationship between user information and user role.
public class UserInformationUserRole
{
public int UserInformationId { get; set; }
public UserInformation UserInformation { get; set; }
public int UserRoleId { get; set; }
public UserRole UserRole { get; set; }
}
I added a method to the UserInformationRepository.cs class to get the user back with all the roles.
public UserInformation GetUserWithRoles(string UserLanId)
{
return db.Set<UserInformation>().Where(u => u.LanId == UserLanId)
.Include(ur => ur.UserInformationUserRoles)
.ThenInclude(r => r.UserRole).SingleOrDefault();
}
MyClaimsTransformer
public class MyClaimsTransformer : IClaimsTransformation
{
private readonly IUnitOfWorkSecurity _unitOfWork;
public MyClaimsTransformer(IUnitOfWorkSecurity unitOfWork)
{
_unitOfWork = unitOfWork;
}
// Each time HttpContext.AuthenticateAsync() or HttpContext.SignInAsync(...) is called the claims transformer is invoked. So this might be invoked multiple times.
public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
var identity = principal.Identities.FirstOrDefault(x => x.IsAuthenticated);
if (identity == null) return principal;
var user = identity.Name;
if (user == null) return principal;
//Get user with roles from repository.
var dbUser = _unitOfWork.UserInformations.GetUserWithRoles(user);
// Inject DbRoles into Claims list
foreach (var role in dbUser.UserInformationUserRoles.Select((r=>r.UserRole)))
{
var claim = new Claim(ClaimTypes.Role, role.Name);
identity.AddClaim(claim);
}
return new ClaimsPrincipal(identity);
}
}
In the ConfigureServices section of the Startup.cs file add the following line.
services.AddScoped<IClaimsTransformation, MyClaimsTransformer>();
Then I added the attribute to my controller
[Authorize(Roles = "Administrator")]
When I run my application I get the following error:
An unhandled exception occurred while processing the request. InvalidOperationException: No authenticationScheme was specified, and there was no DefaultForbidScheme found. Microsoft.AspNetCore.Authentication.AuthenticationService.ForbidAsync(HttpContext context, string scheme, AuthenticationProperties properties)
In the startup.cs I added the following to the services
services.AddAuthentication(IISDefaults.AuthenticationScheme);
This got rid of the error, but no matter what I get a 403 error.
You don't have authorization to view this page. HTTP ERROR 403
When I watch the return value from MyClaimsTransformer, I can see the role of administrator has been added to the list of claims, but no matter what I get a 403 error.
If I use the following syntax in my view it works at the view level:
@if (User.HasClaim("http://schemas.microsoft.com/ws/2008/06/identity/claims/role", "Administrator"))
{
<li><a asp-area="" asp-controller="UserInformationAdmin" asp-action="Index">Admin</a></li>
}
I have to specify the entire schema URL for the role though.
I am really not sure how to debug the attribute in the controller. These work on an AOP pattern, but since it is a built in class, I am not sure where I could place a watch to see what variables are being populated and their values.
Since I am able to apply the filter in the view, I think I am getting really close to having this setup correctly. I was going to wait until I had this finished before posting an article on it, but then I thought maybe one of my readers would have a suggestion. If you have a suggestion please post it in the comments. Once I have this figured out, I will update this post with a solution, so keep checking back.
Comments
Post a Comment