Get 401 Unauthorised calling WebApi from another WebAp on behalf of api (not user)

1.1k Views Asked by At

We have a number of ASPNET Core Web Apis in Azure that we call on behalf of a User. That user has normally signed into an ASPNET Web Site, also in Azure.

We are introducing an Audit Service. That feels like it should be called on behalf of the calling service rather that the authenticated user.

  • The Audit Service has an associated App Registration in Azure AD
  • The Audit Service has a scope called "access_as_application" although having seen documentation about a ".default" scope I wasn't sure that i needed a scope
  • The calling application (ASPNET Core Web Site) has been added in the "Authorized client applications" section against the previously mentioned scope

In the calling application I am getting an access token for the app rather than the user by using GetAccessTokenForAppAsync.

var accessToken = await this.tokenAcquisition.GetAccessTokenForAppAsync(this.auditApiScope);
System.Diagnostics.Debug.WriteLine($"access token-{accessToken}");
this.httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
this.httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

Currently I am running the calling application and the audit service on my local development machine.

When I make the call to the audit service I am getting a 401 Unauthorized

var response = await this.httpClient.PostAsync($"{this.auditApiBaseAddress}v1/entries", requestContent);

UPDATE

I have added the Azure Ad App Id of the calling application as a knownClientApplication on the Audit Service, via the App Manifest. That did not prevent the 401

"knownClientApplications": [
        "7ac7f49d-e9fa-4e1b-95b2-03e0e1981f58"
    ],

UPDATE 2

I can see that the instance of the service running in Visual Studio is reporting a stack trace. It is referring to a IDW10201 issue.

System.UnauthorizedAccessException: IDW10201: Neither scope or roles claim was found in the bearer token. 
   at Microsoft.Identity.Web.MicrosoftIdentityWebApiAuthenticationBuilderExtensions.<>c__DisplayClass3_1.<<AddMicrosoftIdentityWebApiImplementation>b__1>d.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at Microsoft.Identity.Web.MicrosoftIdentityWebApiAuthenticationBuilder.<>c__DisplayClass14_0.<<CallsWebApiImplementation>b__1>d.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler.HandleAuthenticateAsync()
   at Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler.HandleAuthenticateAsync()
   at Microsoft.AspNetCore.Authentication.AuthenticationHandler`1.AuthenticateAsync()
   at Microsoft.AspNetCore.Authentication.AuthenticationService.AuthenticateAsync(HttpContext context, String scheme)
   at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

Any thoughts why?

2

There are 2 best solutions below

6
On BEST ANSWER

You should currently be performing server-to-server interaction, that is, no user involvement. So your server application needs to create an appRole, and then grant the app Role as an application permission to the client application.

First, you need to expose the api of the server application protected by Azure, which can be configured according to the following process:

Azure portal>App registrations>Expose an API>Add a scope>Add a client application

enter image description here

Then you need to create the appRole of the server application, and then grant that role as an application permission to the client application.

enter image description here

Next, go to client application>API permissions>Add a permission>My APIs>your api application.

enter image description here

Finally, you need to obtain an access token using the client credential flow where no user is logged in:

enter image description here

Parse the token:

enter image description here

0
On

Whilst I've marked Carl Zhao's contribution as the answer I found the screenshots a bit hard to follow so this is my attempt at making that a bit clearer.

In this scenario where we want authentication between Azure Ad registered application (client) and another Azure Ad registered application (Audit Service) scopes were not the solution. Rather than exposing a scope we needed to expose an appRole.

The steps required to expose and then request access to the app role were

  1. App Registrations -> Audit Service -> Manage -> App roles -> Create app role
  2. When creating the app role ensure the Allowed member type is "Applications"
  3. Now go to App Registrations -> YourClientApplication -> Api permissions -> Add a permission
  4. I expected the Audit Service to appear under "My APIs" in the "Request API permissions panel". I did not, the only way I could request permisison to the previously created AppRole was to enter the AppId of the Audit Service in the search box under "APIs my organization uses"
  5. Once I was able to select the audit service I selected "Application permissions" rather than "Delegated permissions" and then I selected the specific role

Once the client application had been granted access we needed to write code get to an access token. Using Mictosoft.Identity.Web library

var accessToken = await this.tokenAcquisition.GetAccessTokenForAppAsync(this.auditApiScope);
System.Diagnostics.Debug.WriteLine($"access token-{accessToken}");
this.httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
this.httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

Note the call to GetAccessTokenForAppAsync not GetAccessTokenForUserAsync. GetAccessTokenForAppAsync still requires a scope however as already stated a custom scope is not needed. The scope is ".default" so the string passed to that call in our case was https://ourdomain/audit-service/.default" which is our App ID URI plus ".default"