Client Credentials Flow - Call Web API to API without user interaction

Introduction

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

  • Managed Identity(Admin consent required) - earlier Post
  • Client Credentials Flow (Admin consent required) 

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,

  • Azure Active Directory service
    • 2 Create App (SPN) registrations
  • Admin consent is required 
  • 2 .NET Core Web API projects.

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:

  • For API1
    • Generate an SPN (Service Principal Name) named "API1-webapi-poc-ad" and make a note of the associated client ID. For instance -  CLIENTID-API1
    • Generate a client secret under the "Certificates & Secrets" section and record it for future reference. ClientSecret - For instance - CLIENTSECRET-API1
    • In the Manifest section, update the "accessTokenAcceptedVersion" to 2 (V2 version - in numbers).
    • Create an app role named "access_as_application_API1" under the "App Roles" section.Client Credentials flow
  • For API2
    • Create an SPN named "API2-poc-webapi-ad" and take note of the client ID. For instance - CLIENTID-API2
    • Generate a client secret and retain it. For instance - CLIENTSECRET-API2
    • Update the "accessTokenAcceptedVersion" to 2 (V2 version) in the Manifest section.
    • Under the API permissions, add a permission button by selecting "API1-webapi-poc-ad" in "My APIs" and navigate to the "application permissions" blade.
    • Choose to select the role "access_as_application_API1" and grant admin consent.Client Credentials flow

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