20

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
  • 1,622
  • 1
  • 18
  • 31
  • Use a sniffer like wireshark or fiddler and compare the working Postman with the non working c#. First check the version of TLS being used. If they are the same the compare headers in first request. Make the c# look like the Postman results. – jdweng Oct 04 '20 at 10:51
  • Not sure what help this would do? Postman is not working. I'm only using Postman. The issue is not on the wire. The issue seem to be that the access_token is lacking some information. – 1iveowl Oct 04 '20 at 10:56
  • check version of TLS. You are probably getting an error because you are using TLS 1.0/1.1. The industry 5 years ago decided to eliminate TLS 1.0/1.1 due to security issues. In June this year Microsoft pushed a security update to disable TLS 1.//1.1 on servers. So client now have to request TLS 1.2/1.3. Since you do not specify the TLS version it defaults to the version of Net your using and the version of windows you are using. Also make sure you are using latest API. Old APi's could be using older version of TLS. – jdweng Oct 04 '20 at 13:36
  • I can confirm that the communication is using TLS 1.2, but I still don't see why this matters, since the communication between Postman and my ASP.NET Core 3.1 api, both running on my local Windows 10 bld 19042.541, is working just fine. The issue is not on the wire. – 1iveowl Oct 04 '20 at 13:57

5 Answers5

22

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://learn.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

Endre86
  • 269
  • 2
  • 6
  • Avoid using links to other answers, instead you can summarize and add the steps, info or whatever it's needed to answer the question. – juagicre Nov 11 '20 at 14:50
  • 1
    @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. – Endre86 Nov 12 '20 at 15:45
  • This solves our problem. Thank you! – VJPPaz Nov 02 '21 at 10:31
  • This solved my problem! – Birtija Nov 22 '21 at 13:05
  • Would have been nice if the error message mentioned this! – karoberts Mar 21 '22 at 20:30
4

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.

1iveowl
  • 1,622
  • 1
  • 18
  • 31
  • 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 – svyat1s Oct 04 '20 at 15:50
  • 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 Oct 04 '20 at 16:44
  • 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. – 1iveowl Oct 04 '20 at 16:51
  • 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 – svyat1s Oct 04 '20 at 16:53
  • 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. – 1iveowl Oct 04 '20 at 16:58
  • 2
    in the controller you should specify not a Roles but the policy you have created `[Authorize(Policy = "Role")]` – svyat1s Oct 04 '20 at 16:59
1

Just add DefaultMapInboundClaims to your API service config

public void ConfigureServices(IServiceCollection services)
{
    JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
}
svyat1s
  • 868
  • 9
  • 12
  • 21
  • 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. – 1iveowl Oct 04 '20 at 15:41
  • 2
    ahh sorry, I have been researching that behaviour in the project and haven't refresh the page, so I've missed your updates – svyat1s Oct 04 '20 at 15:42
  • Sry about that, made an assumption I shouldn't have made. – 1iveowl Oct 04 '20 at 16:39
1

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.)

StinkySocks
  • 812
  • 1
  • 13
  • 20
0

The reason is that your are making request with default scope scope=api%3A%2F%2{client_id}%2F.default which doesn't include scope claim in the access_token you should use specific scope that you are registered for your ASP.NET Core 3.1 API project when you have exposing that API in the Azure Portal.

svyat1s
  • 868
  • 9
  • 12
  • 21
  • 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 Oct 04 '20 at 13:05
  • This is what the documentation says: [Client credentials grant flow and /.default](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent#client-credentials-grant-flow-and-default) (updated with the correct link) – 1iveowl Oct 04 '20 at 13:08
  • 1
    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, – svyat1s Oct 04 '20 at 13:28
  • 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 Oct 04 '20 at 13:41