I have to manage our user's roles in our local database, not in Azure AD. But, I also need policy-based authorization for controllers because we have both admin and customer areas.
To handle this, I added an authorization filter that loads the user's role from either Session or the database, adds an Identity to the Principal, and then moves along. This Identity adds an appropriate Role Claim.
Before leaving the authorization filter, the IsInRole returns true as expected, and there are two Identities.
My authorization filter looks like this:
public class MyAuthFilter : IAsyncAuthorizationFilter
{
private readonly IUserService userService;
public MyAuthFilter(IUserService userService)
{
this.userService = userService;
}
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
var user = context.HttpContext.User;
if (user.Identity.IsAuthenticated)
{
AuthUserViewModel authUserViewModel;
var sessionViewModelJson = context.HttpContext.Session.GetString(user.AzureObjectId());
if (string.IsNullOrEmpty(sessionViewModelJson))
{
authUserViewModel = await ConstructSessionViewModel(context);
}
else
{
authUserViewModel = JsonConvert.DeserializeObject<AuthUserViewModel>(sessionViewModelJson);
}
user.AddIdentity(authUserViewModel?.Role);
}
}
private async Task<AuthUserViewModel> ConstructSessionViewModel(AuthorizationFilterContext context)
{
var user = context.HttpContext.User;
var parsedObjectId = Guid.Parse(user.AzureObjectId());
var findUserResult = await userService.FindByAzureObjectId(new FindByAzureObjectIdRequest
{
AzureObjectId = parsedObjectId
});
if (findUserResult.Success)
{
var userModel = findUserResult.User;
var viewModel = new AuthUserViewModel
{
AzureObjectId = parsedObjectId,
UserId = userModel.Id,
SchoolId = userModel.SchoolId.GetValueOrDefault(),
Name = userModel.Name,
Email = userModel.Email,
PhoneNumber = userModel.PhoneNumber,
Role = userModel.Role
};
context.HttpContext.Session.SetString(user.AzureObjectId(), JsonConvert.SerializeObject(viewModel));
return viewModel;
}
return null;
}
}
That AddIdentity extension method looks like this:
public static void AddIdentity(this ClaimsPrincipal principal, string role)
{
if (string.IsNullOrWhiteSpace(role) || principal.IsInRole(role))
{
return;
}
switch (role)
{
case Roles.School:
principal.AddIdentity(new SchoolIdentity());
break;
case Roles.Admin:
principal.AddIdentity(new AdminIdentity());
break;
}
}
and in this case, the SchoolIdentity is what gets added, and it looks like this:
public class SchoolIdentity : ClaimsIdentity
{
public SchoolIdentity()
{
AddClaim(new SchoolPortalClaim());
}
}
and finally, the SchoolPortalClaim looks like this:
public class SchoolPortalClaim : Claim
{
public SchoolPortalClaim() : base(ClaimTypes.Role, "School")
{
}
public SchoolPortalClaim(BinaryReader reader) : base(reader)
{
}
public SchoolPortalClaim(BinaryReader reader, ClaimsIdentity subject) : base(reader, subject)
{
}
protected SchoolPortalClaim(Claim other) : base(other)
{
}
protected SchoolPortalClaim(Claim other, ClaimsIdentity subject) : base(other, subject)
{
}
public SchoolPortalClaim(string type, string value) : base(type, value)
{
}
public SchoolPortalClaim(string type, string value, string valueType) : base(type, value, valueType)
{
}
public SchoolPortalClaim(string type, string value, string valueType, string issuer) : base(type, value, valueType, issuer)
{
}
public SchoolPortalClaim(string type, string value, string valueType, string issuer, string originalIssuer) : base(type, value, valueType, issuer, originalIssuer)
{
}
public SchoolPortalClaim(string type, string value, string valueType, string issuer, string originalIssuer, ClaimsIdentity subject) : base(type, value, valueType, issuer, originalIssuer, subject)
{
}
}
The issue comes when the policy executes:
services.AddAuthorization(options =>
{
options.AddPolicy(Policies.School,
policy => policy.RequireAssertion(
context => context.User.IsInRole(Roles.School)));
});
The context.User does not have the Identity that was added by the authorization filter.
How do I get this to move downstream?
The Controller in question looks like this:
[Area(Areas.School)]
[Authorize(Policy = Policies.School)]
public class HomeController : BaseController
{
public HomeController(IUserService userService) :
base(userService)
{
}
public IActionResult Index()
{
return RedirectToAction("Index", "Presentation", new {Area = "School"});
}
}