Search code examples
azure-ad-msalmicrosoft-identity-platformmicrosoft-identity-web

Adding claims for roles in a token obtained using MSAL?


I am trying to set up role authorization for our web API controllers so that I can use decorators like [Authorize(Roles="ROLE_ADMIN,ROLE_MANAGER")]. One challenge is that the roles are in custom tables of the application's database. I can connect to the database and successfully add the roles to the claims with the code below. When debugging I can see the roles appended to the Claims. The claim looks like http://schemas.microsoft.com/ws/2008/06/identity/claims/role: ROLE_ADMIN and shows that the issuer is LOCAL_AUTHORITY. I feel like we're on the right track.

builder.Services.Configure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options =>
{
    options.Events = new JwtBearerEvents
    {
        OnTokenValidated = async context =>
        {
            var claimsIdentity = context.Principal.Identity as ClaimsIdentity;
            var userEmail = context.Principal.FindFirstValue("preferred_username");

            using var dbContext = new SmpDbContext(new DbContextOptionsBuilder<SmpDbContext>()
                .UseSqlServer(connectionString).Options);

            var appUser = await dbContext.AppUsers.FirstOrDefaultAsync(u => u.Email == userEmail);
            if (appUser != null)
            {
                var roles = await dbContext.Roles.Where(r => r.Users.Contains(appUser)).ToListAsync();

                foreach (var role in roles)
                {
                    claimsIdentity.AddClaim(new System.Security.Claims.Claim(ClaimTypes.Role, role.Authority));
                }
            }
        }
    };
});

However, whenever I call one of the APIs I get the below error. If I comment out the options.Event block the error does not occur. Am I invalidating the token when adding these claims, thus causing Graph and other Azure service using the token to fail? If that is the case, are there any other ways to add the "Roles" within the web api project? I can successfully add policies in builder.Services.AddAuthorization(), but I am hoping to be able to utilize [Authorize(Roles="role1,role2,role3")] instead of resorting to [Authorize(Policy="JustThis")].

Status Code: 0
Microsoft.Graph.ServiceException: Code: generalException
Message: An error occurred sending the request.

 ---> Microsoft.Identity.Web.MicrosoftIdentityWebChallengeUserException: IDW10502: An MsalUiRequiredException was thrown due to a challenge for the user. See https://aka.ms/ms-id-web/ca_incremental-consent. 
 ---> MSAL.NetCore.4.55.0.0.MsalUiRequiredException: 
    ErrorCode: user_null
Microsoft.Identity.Client.MsalUiRequiredException: No account or login hint was passed to the AcquireTokenSilent call. 
   at Microsoft.Identity.Client.Internal.Requests.Silent.SilentRequest.ExecuteAsync(CancellationToken cancellationToken)
   at Microsoft.Identity.Client.Internal.Requests.RequestBase.RunAsync(CancellationToken cancellationToken)
   at Microsoft.Identity.Client.ApiConfig.Executors.ClientApplicationBaseExecutor.ExecuteAsync(AcquireTokenCommonParameters commonParameters, AcquireTokenSilentParameters silentParameters, CancellationToken cancellationToken)
   at Microsoft.Identity.Web.TokenAcquisition.GetAuthenticationResultForWebAppWithAccountFromCacheAsync(IConfidentialClientApplication application, ClaimsPrincipal claimsPrincipal, IEnumerable`1 scopes, String tenantId, MergedOptions mergedOptions, String userFlow, TokenAcquisitionOptions tokenAcquisitionOptions)
   at Microsoft.Identity.Web.TokenAcquisition.GetAuthenticationResultForUserAsync(IEnumerable`1 scopes, String authenticationScheme, String tenantId, String userFlow, ClaimsPrincipal user, TokenAcquisitionOptions tokenAcquisitionOptions)
    StatusCode: 0 
    ResponseBody:  
    Headers: 
   --- End of inner exception stack trace ---

Solution

  • I was seeing the same issue where custom claims added into the bearer token was not being recognized by the controller attributes. My solution was to create a new Identity:

    private static ClaimsIdentity _identity = new ClaimsIdentity();
    

    After adding the claims to this identity I added the new identity to the original bearer token:

    _identity.AddClaim(new Claim(ClaimTypes.Role, "MyCustomRole", ClaimValueTypes.String, "https://login.microsoftonline.com/00000000-0000-0000-0000-000000000000/v2.0"));
    context.Principal.AddIdentity(_identity);
    

    Now my custom claims work when I use them in the controller attribute: [Authorize(Roles = "MyCustomRole")] Hope this helps.