I finally made some headway! This is SUPER weird.
Using some code I found on GitHub (included below), I manually validated the token. For some reason, the role claims seem to be broken out one-by-one rather than included as a list in the "roles" claim (as it is on the front-end and in the documentation).
Under the ClaimsPrincipal.Claims property, there will be two claims like this:
{http://schemas.microsoft.com/ws/2008/06/identity/claims/role: Site.View}
Where the key is that schema definition.
I'm currently pushing to staging to see if the function binding is pulling back the same thing where each role has its own claim.
My validation code:
public static async Task<bool> ValidateRoles(HttpRequest req, IList<string> allowedRoles, ILogger log)
{
var accessToken = GetAccessToken(req);
var claimsPrincipal = await ValidateAccessToken(accessToken, log);
Claim roleClaim = claimsPrincipal.FindFirst("roles");
string[] roles = roleClaim?.Value?.Split(' ');
if (roleClaim == null || !(roles.Intersect(allowedRoles).Count() > 0))
{
log.LogError($"Denied user access based on roles: {String.Join(", ", roles)}");
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;
}
private static string GetAccessToken(HttpRequest req)
{
var authorizationHeader = req.Headers?["Authorization"];
string[] parts = authorizationHeader?.ToString().Split(null) ?? new string[0];
if (parts.Length == 2 && parts[0].Equals("Bearer"))
return parts[1];
return null;
}
private static async Task<ClaimsPrincipal> ValidateAccessToken(string accessToken, ILogger log)
{
var audience = AuthConfig.audience;
var clientID = AuthConfig.clientId;
var tenant = AuthConfig.tenant;
var tenantid = AuthConfig.tenantId;
var aadInstance = AuthConfig.aadInstance;
var authority = AuthConfig.authority;
var validIssuers = AuthConfig.validIssuers;
// Debugging purposes only, set this to false for production
Microsoft.IdentityModel.Logging.IdentityModelEventSource.ShowPII = true;
ConfigurationManager<OpenIdConnectConfiguration> configManager =
new ConfigurationManager<OpenIdConnectConfiguration>(
$"{authority}/.well-known/openid-configuration",
new OpenIdConnectConfigurationRetriever());
OpenIdConnectConfiguration config = null;
config = await configManager.GetConfigurationAsync();
ISecurityTokenValidator tokenValidator = new JwtSecurityTokenHandler();
// Initialize the token validation parameters
TokenValidationParameters validationParameters = new TokenValidationParameters
{
// App Id URI and AppId of this service application are both valid audiences.
ValidAudiences = new[] { audience, clientID },
// Support Azure AD V1 and V2 endpoints.
ValidIssuers = validIssuers,
IssuerSigningKeys = config.SigningKeys
};
try
{
SecurityToken securityToken;
var claimsPrincipal = tokenValidator.ValidateToken(accessToken, validationParameters, out securityToken);
return claimsPrincipal;
}
catch (Exception ex)
{
log.LogError(ex.ToString());
}
return null;
}