This one's got some moving parts, so I'll try to boil it down as much as possible.
I have to apps working together: A frontend written in React and a backend written in C# Azure Functions (.NET Core 3.1).
Both apps are registered correctly in our AD tenant. The Azure Functions instance is set up to only accept requests from the front-end React app. I have roles defined in both applications through the Enterprise Applications blade. In the frontend, I can see all roles assigned to the user on their idToken's roles claim. For the sake of argument, let's say I've assigned Site.View and Site.Administer to myself (I've assigned these roles in BOTH applications, not just the frontend). Both of these are visible on the frontend.
Now, let's segue to my Functions setup. They're all HTTP-triggered, and I'm using the ClaimsPrincipal binding to automatically get the ClaimsPrincipal from the incoming Bearer Token.
When I call the backend, everything looks pretty hunky-dory from the frontend... most of the time. I was, however, getting some super-random 401 Unauthorized responses. After a whole bunch of debugging, I finally figured out the issue. It seems that the roles claim on the backend only contains one role: The role most recently assigned to the user. So, for example, if I assigned myself Site.Administer AND THEN assigned myself Site.View, only Site.View would show up. If I did it the other way around, I'd only have Site.Administer.
I'm at a loss here. Is it just that the ClaimsPrincipal binding isn't compatible with v2 tokens? I've included some relevant code snippets below:
ValidateRoles code:
// Global definition
public static bool ValidateRoles(ClaimsPrincipal principal, IList<string> allowedRoles, ILogger log)
{
Claim roleClaim = principal.FindFirst("roles");
string[] roles = roleClaim.Value.Split(' ');
if (roleClaim == null || !(roles.Intersect(allowedRoles).Count() > 0))
{
throw new UnauthorizedAccessException("The current user does not have the roles required to access this endpoint.");
}
log.LogInformation($"Authorizing user based on roles: {String.Join(", ", roles)}");
return true;
}
//Definition I'm using for the current group of endpoints
public static readonly IList<string> Roles = new List<string> {
Globals.Roles.AmenitiesAdmin,
Globals.Roles.GlobalAdmin,
};
public static bool ValidateRoles(ClaimsPrincipal principal, ILogger log)
{
return Globals.ValidateRoles(principal, Roles, log);
}
Example Azure Function:
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "amenities/addAmenityRules")] HttpRequest req,
ClaimsPrincipal principal,
ILogger log)
{
try
{
AmenityEndpointGlobals.ValidateRoles(principal, log);
}
catch (UnauthorizedAccessException e)
{
log.LogError($"Denied user access based on roles: {principal.FindFirst("roles")}");
return new UnauthorizedObjectResult(new Dictionary<string, string> { { "error", e.Message } });
}
catch (Exception e)
{
log.LogError(e.Message);
return new StatusCodeResult(StatusCodes.Status500InternalServerError);
}
// other code
}