IDW10201: Neither scope or roles claim was found in the bearer token

10,636

Solution 1

The video "Implementing Authorization in your Applications with Microsoft identity platform - june 2020" outlines that the missing piece is this flag JwtSecurityTokenHandler.DefaultMapInboundClaims = false; which need to be set in startup.cs - e.g:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMicrosoftIdentityWebApiAuthentication(Configuration);

    // By default, the claims mapping will map claim names in the old format to accommodate older SAML applications.
    //'http://schemas.microsodt.com/ws/2008/06/identity/clains/role' instead of 'roles'
    // This flag ensures that the ClaimsIdentity claims collection will be build from the claims in the token
    JwtSecurityTokenHandler.DefaultMapInboundClaims = false;


    // Notice that this part is different in the video, 
    // however in this context the following seems to be 
    // the correct way of setting the RoleClaimType:
    services.Configure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options =>
    {
        // The claim in the Jwt token where App roles are available.
        options.TokenValidationParameters.RoleClaimType = "roles";
    });

    [... more code ...]
}

Alternative 1

It is also possible to set authorization for the whole app like this in startup.cs:


services.AddControllers(options =>
{
    var policy = new AuthorizationPolicyBuilder()
        .RequireClaim("roles", "access_as_application")
        .Build();
    options.Filters.Add(new AuthorizeFilter(policy));
});

Alternative 2

It is also possible to use a policy like this:

services.AddAuthorization(config =>
{
    config.AddPolicy("Role", policy => 
        policy.RequireClaim("roles", "access_as_application"));
});

Now this policy can be used on a controller request like this:

[HttpGet]
[Authorize(Policy = "Role")]
public async Task<string> Get()
{
    return "Hello world!";
}

More in the documentation: Policy based role checks.

Solution 2

This might help if you are planning on not using build in scopes or roles. You can enable "access-control list" authentication using my example for Azure B2C below. Here are some links to the official documentation.

https://github.com/AzureAD/microsoft-identity-web/wiki/web-apis#user-content-web-apis-called-by-daemon-apps-using-client-credential-flow

https://docs.microsoft.com/en-us/dotnet/api/microsoft.identity.web.microsoftidentityoptions.allowwebapitobeauthorizedbyacl?view=azure-dotnet-preview

Add the following to your AD configuartion: "AllowWebApiToBeAuthorizedByACL": true

Example:

"AzureAdB2C": {
    "Instance": "https://xxx.b2clogin.com/",
    "ClientId": "xxxx",
    "Domain": "xxx.onmicrosoft.com",
    "SignUpSignInPolicyId": "xxx",
    "AllowWebApiToBeAuthorizedByACL": true
  },

For what ACL/Access-control list means: ACL: https://en.wikipedia.org/wiki/Access-control_list

Solution 3

Just add DefaultMapInboundClaims to your API service config

public void ConfigureServices(IServiceCollection services)
{
    JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
}

Solution 4

When I received this error, "IDW10202", it was because of this line of code in the Controller.

HttpContext.ValidateAppRole("MyAppRole");

(This was the only result returned by Google, so placing this comment here for anyone's benefit. Apologies if a bit off topic.)

Share:
10,636
1iveowl
Author by

1iveowl

BY DAY: Microsoft Principal Engineer, Azure FastTrack for ISVs and Start-ups. BY WEEKEND: Programmer: Azure, .NET 5, C#, Reactive Extensions and whatever I can get my hands on and the time for. GitHub LinkedIn NuGet Packages

Updated on June 16, 2022

Comments

  • 1iveowl
    1iveowl almost 2 years

    I have a ASP.NET Core 3.1 project like this sample: Sign-in a user with the Microsoft Identity Platform in a WPF Desktop application and call an ASP.NET Core Web API.

    I'm using Identity web version 1.0 and Azure AD, single-tenant application.

    I've edited the manifest adding appRoles since I'm requesting an application token only, and not a user token:

    [... more json ...]
    "appId": "<guid>",
    "appRoles": [
        {
            "allowedMemberTypes": [
                "Application"
            ],
            "description": "Accesses the application.",
            "displayName": "access_as_application",
            "id": "<unique guid>",
            "isEnabled": true,
            "lang": null,
            "origin": "Application",
            "value": "access_as_application"
        }
    ],
    "oauth2AllowUrlPathMatching": false,
    [... more json ...]
    

    I've also enabled the idtyp access token claim, to specify that this is an application token.:

    [... more json ...]
    "optionalClaims": {
        "idToken": [],
        "accessToken": [
            {
                "name": "idtyp",
                "source": null,
                "essential": false,
                "additionalProperties": []
            }
        ],
        "saml2Token": []
    [... more json ...]
    

    The following request is made with Postman. Please notice the use of /.default with the scope, which is mentioned in the documentation in relation to the client credentials grant flow.

    POST /{tenant_id}/oauth2/v2.0/token HTTP/1.1
    Host: login.microsoftonline.com
    Content-Type: application/x-www-form-urlencoded
    
    scope=api%3A%2F%2{client_id}%2F.default
    &client_id={client_id}
    &grant_type=client_credentials
    &client_secret={secret_key}
    

    The request returns an access_token which can be viewed with jwt.ms and looks like this, where actual data have been replaced by placeholders for security reasons.:

    {
      "typ": "JWT",
      "alg": "RS256",
      "x5t": "[...]",
      "kid": "[...]"
    }.{
      "aud": "api://<client_id>",
      "iss": "https://sts.windows.net/<tenant_id>/",
      "iat": 1601803439,
      "nbf": 1601803439,
      "exp": 1601807339,
      "aio": "[...]==",
      "appid": "<app id>",
      "appidacr": "1",
      "idp": "https://sts.windows.net/<tenant_id>/",
      "idtyp": "app",
      "oid": "<guid>",
      "rh": "[..].",
      "roles": [
        "access_as_application"
      ],
      "sub": "<guid>",
      "tid": "<guid>",
      "uti": "[...]",
      "ver": "1.0"
    }
    

    I notice that the token above does not include scp. This seem correct as this is an application token and not a user token. Instead it includes `”roles”´ as appropiate for an application token.

    The access_token can now be used as bearer in a Postman Get:

    GET /api/myapi
    Host: https://localhost:5001
    Authorization: Bearer {access_token}
    

    The reponse to this request is 500 internal error. I.e. something is wrong. The access_token looks like a corrent application token, so the error seems to be on the ASP.NET Core 3.1 controller side.

    The ASP.NET Core 3.1. project hosting the custom API, has a startup.cs which includes the following code:

    services.AddMicrosoftIdentityWebApiAuthentication(Configuration);
    
    // This is added for the sole purpose to highlight the origin of the exception.
    services.Configure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options =>
    {
        var existingOnTokenValidatedHandler = options.Events.OnTokenValidated;
        
        options.Events.OnTokenValidated = async context =>
        {
            if (context.Principal.Claims.All(x => x.Type != ClaimConstants.Scope)
                && context.Principal.Claims.All(y => y.Type != ClaimConstants.Scp)
                && context.Principal.Claims.All(y => y.Type != ClaimConstants.Roles))
            {
                // This where the exception originates from:
                throw new UnauthorizedAccessException("Neither scope or roles claim was found in the bearer token.");
            }
        };
    });
    

    The appsettings.json for the project includes:

    "AzureAD": {
        "Instance": "https://login.microsoftonline.com/",
        "Domain": "mydomain.onmicrosoft.com",
        "ClientId": "<client_id>",
        "TenantId": "<tenant_id>",
        "Audience": "api://<client_id>"
    },
    

    ... and the controller looks like this:

    [Authorize]
    [Route("api/[controller]")]
    public class MyApiController : Controller
    {
        [HttpGet]
        public async Task<string> Get()
        {
            return "Hello world!";
        }
    }
    

    The underlying cause of the 500 internal error is that this exception is thrown: IDW10201: Neither scope or roles claim was found in the bearer token. exception.

    UPDATE:

    (Please see the answer below for even more details).

    This video on "Implementing Authorization in your Applications with Microsoft identity platform - june 2020" suggests that the missing piece is this flag JwtSecurityTokenHandler.DefaultMapInboundClaims = false; which need to be set in startup.cs - e.g:

    public void ConfigureServices(IServiceCollection services)
    {
        // By default, the claims mapping will map clain names in the old format to accommodate older SAML applications.
        //'http://schemas.microsodt.com/ws/2008/06/identity/clains/role' instead of 'roles'
        // This flag ensures that the ClaimsIdentity claims collection will be build from the claims in the token
        JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
        
        [...more code...]
    
    
  • 1iveowl
    1iveowl over 3 years
    Are you sure about that. If I change the scope when requesting the token I get this error instead: "AADSTS70011: The provided request must include a 'scope' input parameter. The provided value for the input parameter 'scope' is not valid. The scope api://{client_id}/access_as_application is not valid.\r\nTrace ID"
  • 1iveowl
    1iveowl over 3 years
    This is what the documentation says: Client credentials grant flow and /.default (updated with the correct link)
  • svyat1s
    svyat1s over 3 years
    have you properly config your Azure app for your client, incliding adding api permission for your api://{client_id}/access_as_application scope ? Because it's strange situation you access_token should contain either scope or role claims and azure isn't issuing scope claim because of .default scope and it seems that you web api app has no permissions/roles in azure and that's why role claims aren't issued too,
  • 1iveowl
    1iveowl over 3 years
    Yes. I'm updated the manifest and now I'm actually getting the Roles, but I'm still getting the same exception. Please see the updates in the original post.
  • 1iveowl
    1iveowl over 3 years
    If you want to be helpful explain why this doesn't work [Authorize(Roles = "access_as_application")]. You can see the details in my answer to the question.
  • svyat1s
    svyat1s over 3 years
    ahh sorry, I have been researching that behaviour in the project and haven't refresh the page, so I've missed your updates
  • svyat1s
    svyat1s over 3 years
    it's probably because that your roles isn't actually roles but claims try to add policy like policy.RequireClaim("roles", "access_as_application")), and than use that policy in your controller
  • 1iveowl
    1iveowl over 3 years
    Sry about that, made an assumption I shouldn't have made.
  • 1iveowl
    1iveowl over 3 years
    Didn't work. Did this: services.AddControllers(options => { var policy = new AuthorizationPolicyBuilder() .RequireClaim("roles", "access_as_application") .Build(); options.Filters.Add(new AuthorizeFilter(policy)); })
  • 1iveowl
    1iveowl over 3 years
    Well it sort of did work, in the sense that it set the requirement of the access_as_application globally for the whole app. However, it still fails if I put the [Authorize(Roles = "access_as_application")] attribute on the controller itself.
  • svyat1s
    svyat1s over 3 years
    hmm strange for me it works services.AddAuthorization(config => { config.AddPolicy("Role", policy => policy.RequireClaim("roles", "access_as_application")); }); but your solution is good too
  • 1iveowl
    1iveowl over 3 years
    Agree, your code works for the whole app, just like mine. I think it's doing the same thing. Are you able to use the attribute on the controller only: [Authorize(Roles = "access_as_application")], cause I'm not.
  • svyat1s
    svyat1s over 3 years
    in the controller you should specify not a Roles but the policy you have created [Authorize(Policy = "Role")]
  • juagicre
    juagicre over 3 years
    Avoid using links to other answers, instead you can summarize and add the steps, info or whatever it's needed to answer the question.
  • Endre86
    Endre86 over 3 years
    @juagicre I do not think linking to official documentation is the same as "linking to other answers". Also, I think it is important to read the official documentatio, specially when changing the default behavior of your applications security. The documentation has a much greater chance to be updated before this SO answer. Lastly, I did write how to enable what I suggested in the post.
  • VJPPaz
    VJPPaz over 2 years
    This solves our problem. Thank you!
  • Birtija
    Birtija over 2 years
    This solved my problem!
  • karoberts
    karoberts about 2 years
    Would have been nice if the error message mentioned this!