Friday, October 6, 2023

Calling an ASP.NET Core Web API Secured with Microsoft Entra ID using Azure Managed Identity

In this post, let's see how we can call an ASP.NET Core Web API Secured with Microsoft Entra ID (known as Azure AD) using an Azure Managed Identity.

This is our scenario: we are going to have 2 APIs.
  1. Internal API: This API is going to expose a GET: /claims endpoint. This endpoint will return ClaimsPrinciple Claims and will require Authorization. It's secured with Microsoft Entra ID.
  2. Public API: This API is going to expose a GET: /claims endpoint, but this DO NOT require any Authorization. This API will have an Azure User-Assigned Managed Identity assigned and this endpoint will call the Internal APIs' GET: /claims using the Managed Identity.
I have created the following already.
  • A User-Assigned Managed Identity with the name: mi-miauth-demo
  • Two brand new Azure App Services: app-miauth-internal-api and app-miauth-internal-api. To the Public API, I have assigned the User-Assigned Managed Identity: mi-miauth-demo.
Assign User-Assigned Managed Identity to Public API
Now let's start.

Creating App Registration


The first step is creating an App Registration in Microsoft Entra ID. You can do so by going into Microsoft Entra ID -> App registrations ->     + New registration. Give it a name, (in my case it's Managed Identity Auth Demo) and select an account type. In this case, for simplicity, I have selected Accounts in this organizational directory only. And then click on Create.

Once the App Registration is created (behind the scenes along with the App Registration, a new Enterprise Application will get created with the same name as in App Registration), I am clicking on Expose an API tab and adding the Application ID URI.
App Registration -> Set Application ID URI
I am proceeding with the default which looks something like: api://xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.

Now go to App roles tab and create a new App role.
App Registration -> Create app role
So at this stage, we are basically done with the App Registration.

Configuring Enterprise Application (Service Principal)


Now we need to assign the Managed Identity access to the app role we created above. For that, I am running the following script from Windows PowerShell.
# Login to Azure and setting the subscription
Connect-AzAccount
Set-AzContext -SubscriptionId "<SubscriptionId>"

# Invoking Connect-MgGraph before any commands that access Microsoft Graph,
# Requesting scopes that we require during our session
$tenantID = '<TenantId>'
Connect-MgGraph -TenantId $tenantId -Scopes 'Application.Read.All', 'Application.ReadWrite.All', 'AppRoleAssignment.ReadWrite.All', 'Directory.AccessAsUser.All', 'Directory.Read.All', 'Directory.ReadWrite.All'
 
# App Registration Name
$appRegistrationName = 'Managed Identity Auth Demo'

# Install Microsoft.Graph Module if required using below command
# Install-Module Microsoft.Graph

 
# Retrieving Service Principal Id
$servicePrincipal = (Get-MgServicePrincipal -Filter "DisplayName eq '$appRegistrationName'")
$servicePrincipalObjectId = $servicePrincipal.Id

# Retrieving App role Id that the Managed Identity should be assigned to
$appRoleName = 'MI.Access'
$appRoleId = ($servicePrincipal.AppRoles | Where-Object {$_.Value -eq $appRoleName }).Id
 
# Managed Identity's Object (principal) ID.
$managedIdentityObjectId = '<ManagedIdentityObjectId>'
 
# Assign the managed identity access to the app role.
New-MgServicePrincipalAppRoleAssignment `
    
-ServicePrincipalId $servicePrincipalObjectId `
    -PrincipalId $managedIdentityObjectId `
    -ResourceId $servicePrincipalObjectId `

    -AppRoleId $appRoleId
Once the commands are completed, go to the relevant Enterprise Application -> Users and groups. Make sure you can see the Role assignment we just did.
Enterprise Application -> Users and groups
Now we are done with the Enterprise Application configuration.

Coding


Now let's write some code for our APIs starting with the Internal API.
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Identity.Web; using System.Security.Claims;

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

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

builder.Services.AddAuthorizationBuilder();

WebApplication app = builder.Build();

app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();

app.MapGet("/claims", (ClaimsPrincipal claimsPrincipal) =>
{
    return claimsPrincipal.Claims
        .Select(
claim => new
        (
            claim.Type,
            claim.Value
        ))
        .ToList();
})
.RequireAuthorization();

app.Run()
And update the appsettings.json adding AzureAd section as follows. Make sure to replace placeholders with your values.
{
  ...
  "AzureAd": {
    "Instance""https://login.microsoftonline.com/",
    "Domain""<Domain>",
    "TenantId""<TenantId>",
    "ClientId""<ClientId>" //Application (Client ID) of the App Registration
  }
}
Now Internal API is all good. I am deploying the code changes.

We are almost there now. Let's step is calling the above endpoint from our public API endpoint.
using Azure.Core;
using Azure.Identity;
using System.Net.Http.Headers;
using System.Text.Json.Nodes;
 
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

WebApplication app = builder.Build();

app.UseHttpsRedirection();

app.MapGet("/claims"async () =>
{
    DefaultAzureCredential credential = new();
    TokenRequestContext tokenRequestContext = new(new[]
    {
        "api://xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxx/.default" // Application ID URI
    });
    AccessToken accessToken = await credential.GetTokenAsync(tokenRequestContext);

    HttpClient httpClient = new()
    {
        DefaultRequestHeaders =
        {
            Authorization = new AuthenticationHeaderValue("Bearer", accessToken.Token)
        }
    };

    string result = await httpClient.GetStringAsync("https://app-miauth-internal-api.azurewebsites.net/claims"); return JsonNode.Parse(result);
});

app.Run();
Public API is all good and I have deployed the code changes.

Now to the fun part, let's test the endpoints. Our expectation is the endpoint in Internal API should return 401 and the endpoint in Public API should return 200 with Claims of the Managed Idenity.
Result
Just like that, we are getting the expected output. Note: You can see the ClaimPrinciple has the Role "MI.Access" we assigned, so you can extend your authorization policies.

Hope this helps.

Happy Coding.

Regards,
Jaliya

No comments:

Post a Comment