Introduction

In Azure, two commonly used ways exist to authenticate and authorize Web APIs to access other Web APIs without user interaction.

Client credentials flow

The Client Credentials flow is an OAuth 2.0 authorization flow that allows a client application to access protected resources on behalf of itself without user involvement. In this flow, the client application authenticates directly with the authorization server using its own credentials (client ID and client secret) to obtain an access token. This access token is then used to authorize requests to access protected resources from the resource server.

The Client Credentials flow differs from other OAuth 2.0 flows, such as the Authorization Code flow or Implicit flow, in that it does not involve user authentication. Instead, it focuses on authenticating and authorizing the client application itself. This flow is typically used when the client application needs to access resources or perform operations that do not require user-specific permissions.

To configure and execute the service Tech stack and Azure services that you need,

Follow these steps

Step 1. Create App Registrations for API1 and API2. To proceed, it is necessary to create two new app registrations, one for API1 and another for API2. Here are the following steps should be followed:

Step 2. Configuring API2 to call API1 using Client Credentials

To establish communication between API2 and API1 utilizing the Client Credentials flow, the following method can be employed:

Obtaining an Access Token using the GetAccessTokenAsync method. The method implementation is as follows:

public async Task<string> GetAccessTokenAsync()
{
    var scopes = new[] { $"api://CLIENTID-API1/.default" };
    
    var authority = $"https://login.microsoftonline.com/{_configuration["AzureAd:TenantId"]}/oauth2/v2.0/token";
    // Make sure you inject IConfiguration _configuration to read AppSettings.json values
    var app = ConfidentialClientApplicationBuilder
        .Create(_configuration["AzureAd:ClientId"])
        .WithClientSecret(_configuration["AzureAd:ClientSecret"])
        .WithAuthority(authority)
        .Build();
    
    var result = await app.AcquireTokenForClient(scopes)
        .ExecuteAsync();
    
    return result.AccessToken;
}

Calling the Records Web API To make a call to the Records Web API from API2, the CallRecordsWebAPI() method can be utilized. The method implementation is as follows:

public async Task<IActionResult> CallRecordsWebAPI()
{
    // To obtain the access token
    string accessToken = await GetAccessTokenAsync();
    
    HttpClient client = new HttpClient();
    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
    
    HttpResponseMessage response = await client.GetAsync("https://localhost:5151/api/Records"); // Calling API1
    if (response.IsSuccessStatusCode)
    {
        var content = await response.Content.ReadAsStringAsync();
        List<Record>? record = JsonSerializer.Deserialize<List<Record>>(content)?.ToList();
        return View(record); // Just added this view to check the response from API1. You can ignore 
    }
    else
    {
        _logger.LogWarning("response" + response);
        Console.WriteLine("response" + response);
        
        var problemDetails = new ProblemDetails
        {
            Title = "An error occurred",
            Detail = Response.Headers.ToString(),
            Status = (int)Response.StatusCode // Internal Server Error
        };
       

Appsettings.JSON properties

{ 
"AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "{mydomain}.onmicrosoft.com",
    "TenantId": "XXXXXXXXXXXXXXXXXXXXXX", // Get tenant ID from the overview page of AAD
    "ClientId": "CLIENTID-API2",
    "ClientSecret": "CLIENTID-API2"
  }
}

Step 3. Configure API1 in the code

The code snippet in Program.cs file to authenticate the requests and add a role policy

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd2"));

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AppRoleAccess", policy =>
    {
        policy.RequireAuthenticatedUser();
        policy.RequireRole("access_as_application_API1"); //Added this role policy to Authorize only the request sent from API2. Other requests from different APIs will be unauthorized. 
    });
});

Azure AD properties from the App settings file

"AzureAd2": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "{mydomain}.onmicrosoft.com",
    "TenantId": "XXXXXXXXXXXXXXXXXXXXXXXXX",
    "ClientId": "CLIENTID-API1",
    "ClientSecret": "CLIENTSECRET-API1"
  }

Records controller code snippet to return the success response based on the role policy "access_as_application_API1

    [Authorize(Policy = "AppRoleAccess")] // This is where you verify the role policy from API2
    [ApiController]
    [Route("api/[controller]")]
    public class RecordsController : ControllerBase
    {
        [HttpGet]
        public ActionResult<IEnumerable<Record>> GetRecords()
        {
            var records = new List<Record>();

            // Generate a list of 10 sample records
            for (int i = 1; i <= 10; i++)
            {
                var record = new Record
                {
                    Id = i,
                    Name = $"Record {i}",
                    Description = $"Description of Record {i}",
                    Date = DateTime.UtcNow
                };

                records.Add(record);
            }

            return Ok(records);
        }
    }

    public class Record
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Description { get; set; }
        public DateTime Date { get; set; }
    }

Here you will see the final output response in this way.

Client Credentials flow

That's it. Now you can authenticate and authorize using API to API by using the Client Credentials without user involvement. In addition, we have added the role policy which to enable fine-grained access control, allowing client applications to have specific permissions based on assigned roles that simplify API governance and management, ensuring secure and controlled access to resources based on predefined roles.

See you in the next post! Stay tuned :-)