DEV Community

Alejandro Brozzo
Alejandro Brozzo

Posted on

WebAPI to WebAPI calls authenticated by Azure

The motivation

Have you ever needed to have a web API call a second web API, but not on behalf of the logged user but as the web API itself? Usually this happens for daemon applications (apps that run on the background instead of under the control of an interactive user, you can find a better definition of daemon apps on wikipedia).

At work we are adding a process to our existing web application that requires a lot of CPU usage. We think that it is a good idea to have a separate processing module to do these tasks, as they can spike CPU usage to 100% and we don't want the rest of the users to suffer from this. We decided to place this module on an Azure environment, and I was assigned the task to set up a mock API to evaluate different implementation methods and authorization requirements. I do not have much experience using Azure (nor any other cloud services provider) so I suspected it would take me some time. I was right. The pushing of the API was simple, but the authorization using Azure Active Directory took me a while to figure out…, so I decided to write my experience to try and help other people in a similar position.

The setup

In order for a web API to call another web API there must be two web APIs (duh). For reference, I will call the API that you need to call (in my case the processing, CPU hogging API) the "called API". The calling API I shall name "client API". "Client" is a standard name given to an app that needs to call another app and is the OAuth grant flow name, so it is quite appropriate.

Let's start this then (knuckle sound).

Step 1: register both apps

The first thing you need to know is that you must register both APIs in Azure AD, regardless of where you are going to host them. In fact, you may be using your local IIS to run them both. Azure AD will be used by the called API to grant access permission to the client API.

Go to Azure and select Azure Active Directory, but be sure to be on a directory that you have admin grants to… or at least that you can ask the admin for granting permissions. If you do not have admin grants in your current directory, you may be able to create a new one and you will be admin on that one (this was exactly my case). You use the "Create tenant" button for that but I won't explain how to do so.

Now that you know that you are an admin and have selected your directory, you can go the menu option "App registrations" and select "New Registration". Give it a name and choose the supported account type that fits you best. You can leave the Redirect URI empty. You don't need it although depending on how you are creating the registration you may be forced to add one. Repeat for the other API.

Step 2: Give your client API a secret

Select the registration of your client API (the caller) and select the menu option "Certificates & secrets". There select "New client secret" and fill the fields. Remember to copy the assigned secret as you will never be able to see it again.

What you have just done is give your client application a unique key that will be used when telling Azure AD who the app is. Think about it as a passport number of sorts. Keep in mind that for a production app, you should use Certificates rather than Secrets, but the configuration itself is pretty much the same.

Step 3: Expose the called API

Now go back to your app registration list and select your called API, and then select the menu option "Expose an API". There, you need to first "Add a scope". A scope is a confusing name for a set of permissions, as defined by the OAuth standard. This will be used by your client API's controllers to indicate the roles a requesting party needs to execute the code. I am kind of assuming you already know this, so I won't go any deeper than that. I will mention though that you will be asked to set the "App ID URI", which should be globally unique and is basically a prefix of all scopes for this API. The default is "api://(clientId)" but you can change it to something a little clearer.

Ok, now that you have defined a scope, you also need to authorize the client applications that can use your API, so on the very same screen where you added the scope you should see a "Add a client application" button. Click on it and you shall be presented with a two-field form. On the Client ID field, enter the client API's secret that you generated on step 2. And on the Authorized scopes field, tick the scope we just created earlier on this step.

What we just did is letting Azure AD know which client apps have rights to reach the called API, and what scope to give them. If we wanted to expose the API to other clients, then we would repeat steps 1 and 2 as many times as client APIs we had, and add all of the generated secrets here (each app will have a different secret) with the scope assigned to them. Of course, you could add more scopes and assign different ones to each client API.

Step 4: Create an app role on the called API

We will now create an app role on the called API. Any client API that attempts to use this API will need that role assigned, it is not enough to have the secret and scope assigned. Go ahead and click on "Create app role" and fill the fields on the presented form. The name and values will be used by the called API on the code you write yourself, so enter whatever you want. However, on the allowed member types, be sure to select Applications (or Both) as it will be an application and not a user that will be consuming the API.

Step 5: Add a permission to the client API

Go back to your API registrations and select your client API. On the menu select API permissions and then click on "Add a permission". On the popping screen you will have to choose between "Microsoft APIs", "APIs my organization uses" and "My APIs". Either of the last two options should include your called API, select it. Now it will ask you what type of permission it requires: Delegated (i.e., logged as a user) or Application. If you are following closely you should have guessed that we need Application permissions, so select that and you should be able to see the permission we just created on our previous step. Select it and confirm.

Once the permission appears on the list, you'll notice it will have a warning status as you need to grant admin consent. Next to the "Add permission" button there should be another button that says "Grant admin consent for [your directory name]". If it is disabled, then you are not an admin and cannot do it yourself (I told you so earlier, but did you listen?). Grant that consent or ask someone with power to do so.

Step 6: Include the required settings on both your APIs' code

Congratulations! You have successfully configured Azure AD with a client flow to allow the client API to access your called API. In hindsight, "called API" wasn't such a great name, but I couldn't think of anything better.

But do not uncork that champagne (or sparkling wine if your organization is not that big) just yet. You still need to code logic or at least put some setting in both your APIs, otherwise all those settings will only block you but not allow to use them.

Called API

Let's start with the called API. I am using a .NET 5.0 API because I am cool and use al the latest tech. If you are using any version of .Net Core I am sure (guessing, actually) it is the same code. For a .NET 4.8 or earlier, or for other language… I dunno, figure it out, blog about it, and then let me know, I'll add a link to your post.

As I was saying, on your called API you will need to add code and settings. Let's start with the "appsettings.json" file that you should have gotten when you created the project (what? You have not created a project yet? Then create one as ASP.NET Core Web Application, then select API. No auth is OK since we will add this manually, but you may choose an auth scheme for simplicity).

On "appsettings.json" you need to add the settings to talk to Azure AD. Something like this:

"AzureAd": {
  "Instance":   "",
  "Domain":   "",
  "TenantId": "abcdef12-3456-7890-abcd-001122334455",
  "ClientId": " fedcba21-0123-blaf-9876-667788990011"
Enter fullscreen mode Exit fullscreen mode

Please use your own Tenant (directory) Id and Application (client) Id that you are shown when selecting your called API on the App Registrations list.

Then on Startup.cs you will need the following code, which use the nuget packages Microsoft.Identity.Web and Microsoft.AspNetCore.Authentication.JwtBearer:

public void ConfigureServices(IServiceCollection services)
  … // the rest of your configuration here
Enter fullscreen mode Exit fullscreen mode

Note that the "AzureAd" json key matches the parameter on GetSection.

Also on the same file you need this:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
  app.UseEndpoints(endpoints =\> { endpoints.MapControllers(); });
Enter fullscreen mode Exit fullscreen mode

The thing you should be looking at is app.UseAuthentication and app.UseAuthorization. You need both of these.

Then you are ready to add the "[Authorize]" attribute to either your controllers or a method within a controller. This ensures that in order to access your controller or method, the client API must have a valid token.

Client API

Finally, the code needed on the client API. This has a similar "appsettings.json" configuration that needs to be set, but with a couple extra entries:

"AuthInfoAzureAd": {
  "Instance": "{0}",
  "Domain": "",
  "TenantId": "12345678-9012-3456-abcd-ef1230456789",
  "ClientId": "22554433-abcd-7890-3210-abcdef012345",
  "ClientSecret": "hush-hush-generated-by-AzureAd",
  "BaseAddress": "https://localhost:44340",
  "Scope": "api://your-called-api-url/.default"
Enter fullscreen mode Exit fullscreen mode

A few more things to note here:

  1. This info will not be used on your ConfigureServices method, but rather when calling the downstream API.
  2. Instance has a "{0}" at the end that will be used to concatenate there the tenant id
  3. In my case the Domain and TenantId match on both projects. Not sure if this is necessary. In any case, your app registration has this info
  4. We are now adding a ClientSecret entry. This is the client Id that you generated on step 2
  5. The base address is where your API is hosted. Here is just localhost, and that works fine.
  6. The Scope is the one that you generated on step 3. If you don't remember what it was named, you can go to the Expose API menu under your called API, there is a handy "copy to clipboard" button there.

Now for the meat of the code you need, the client API code (requires Microsoft.Identity.Client):

IConfidentialClientApplication app = ConfidentialClientApplicationBuilder.Create(config.ClientId)
  .WithAuthority(new Uri(config.Authority))

string[] scopes = new string[] { config.Scope };

// get a token to access your called API
AuthenticationResult result = await app.AcquireTokenForClient(scopes).ExecuteAsync();
if (result != null)
  var httpClient = new HttpClient();
  var apiCaller = new ProtectedApiCallHelper(httpClient);
  var defaultRequestHeaders = httpClient.DefaultRequestHeaders;
  if (defaultRequestHeaders.Accept == null || !defaultRequestHeaders.Accept.Any(m =\> m.MediaType == "application/json"))
    httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
  defaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", result.AccessToken);
  // call the API endpoint
  HttpResponseMessage response = await httpClient.GetAsync($"{config.BaseAddress}/the-secured-endpoint/you-are-trying-to-access");
  string returnValue = string.Empty;
  if (response.IsSuccessStatusCode)
    returnValue = await response.Content.ReadAsStringAsync();
    returnValue = response.StatusCode.ToString();
    string content = await response.Content.ReadAsStringAsync();
    // Note that if you got reponse.Code == 403 and reponse.content.code == "Authorization\_RequestDenied"
    // this is because the tenant admin has not granted consent for the application to call the Web API
    this.logger.LogError($"Content: {content}");
Enter fullscreen mode Exit fullscreen mode

And this is pretty much it. Do handle errors and all that jazz, this is not production ready code.

As a caveat, I did a lot of reading and trial and error, so maaaybe there might be a missing step somewhere. Let me know if so and I'll try to remember what that was and add it for the next lost developer.

Useful links

I'll paste here a bunch of links where I read stuff from, in no particular order. Included is a GitHub repo with some code that you will recognize, especially the client API part.

Discussion (0)