Overview#
One of the projects I worked on recently is a new .NET API service (let’s call it Callee) that runs on Azure using App Service. The client wants to implement a simple authentication solution to protect the API endpoints. The solution should only allow certain Azure app services, Azure Virtual Desktop instances and developers (let’s call them Caller(s)) under same Azure Subscription to access the Callee.
The following diagram illustrates the simplified authentication flow:
Consumer App Service dev->>guard: makes an API request
(with object ID of the Azure resource) guard->>guard: checks authentication settings alt allowed create participant producer as Protected API endpoint guard->>producer: relays API request destroy producer producer->>dev: β 200 OK else unauthorized guard->>dev: β 401 Unauthorized end
Terminologies#
We describe the protected .NET API service as the “Callee”, while the client(s) that consuming the API resource are the “Caller(s)”.
Term | Definition |
---|---|
Callee | The receiver or provider of the API request. It exposes the endpoint and handles the logic for fulfilling the request. |
Caller | The client or initiator of the API request. It makes a call to another service to request data or trigger an action. |
Configurations on Azure#
The overall idea is to set up the authentication settings on Callee. We only allow HTTP requests from Caller(s) that contain a bearer access token with specific properties. The access token should be a JWT with allowed combination(s) of aud
and oid
values.
Caller#
Create an App Service called e.g. poc-web-caller
.
Caller need to provide the identities and audiences to Callee in order to add them into allowed list.
For an Azure App Service#
- Go to Caller’s Azure App Service. In the left menu, choose Settings > Identity
- Turn on the system assigned managed identity:
- Keep the Object (principal) ID
- Alternatively, you can choose user-assigned managed identity Object (principal) ID and assign the identity created:
For an Azure Virtual Desktop instance#
You will need to find out the Object (Principal) ID of the AVD instance.
[!NOTE] Require administrator access You need administrator access to install Azure CLI
Install Azure CLI in PowerShell (run as administrator): https://learn.microsoft.com/en-us/cli/azure/install-azure-cli-windows?view=azure-cli-latest
winget install -e --id microsoft.AzureCLI
Start PowerShell as a regular user. Sign in to Azure and choose the correct subscription:
az login
Get the Object (principal) ID of the AVD instance:
az ad sp list --display-name "$Env:computername" --query "[0].id" -o tsv
For an Azure Account user using Visual Studio#
[!NOTE] This approach also applies to a Mac or Linux user having Azure CLI installed
You will need to find out the Object (Principal) ID of your active account. Just like getting the ID from an AVD, you need to install Azure CLI.
Sign in to Azure and get the Object (principal) ID of the signed in user:
```pwsh
az login
az ad signed-in-user show --output tsv --query id
Callee#
Create an App Service called e.g. poc-web-callee
.
App Registration#
[!NOTE] You need to create a new app registration if you don’t have one.
- Go to App Registration. Click New registration.
- In your new registration:
- Name: user-facing display name for this application
- Supported account types: We choose Default Directory only - Single tenant in this example
- Redirect URI: leave it empty
- Click Register
Azure App Service#
- Go to Callee’s Azure Web Service. In the left menu, choose Settings > Authentication
- Click Add provider. Choose Microsoft.
- Fill in the details:
- Choose a tenant for your application and its users: Workforce configuration (current tenant)
- App registration
- App registration type: Pick an existing app registration in this directory
- Select the app registration you’ve created in Name or app ID
- Client secret expiration: choose the desired secret lifespan
- Additional checks
- Client application requirement: Allow requests from any application
- Allowed client applications (allowed audiences):
- the app registration app ID you chose (for Azure app service)
https://management.core.windows.net/
(for localhost development)
- Identity requirement: Allow requests from specific identities
- Allowed identities: fill in one or some of the following items depends on your need:
- Caller’s user assigned managed identity Object (principal ID)
- Caller’s system-assigned managed identity Object (principal ID)
- (For localhost development) Azure Account’s Object (principal) ID you got in the Caller configuration mentioned in previous section.
- Tenant requirement: Allow requests only from the issuer tenant
- App Service authentication settings
- Restrict access: Require authentication
- Unauthenticated requests: HTTP 401 Unauthorized: recommended for APIs
- Token store: checked
- Click Add to add the identity provider
- Click the Edit button next to Authentication settings, or the edit icon next to the identity provider to change the settings once it’s created:
The table below summarizes the information we need to configure the Callee’s Authentication settings:
Environment | Object ID (oid) | Audience (aud) |
---|---|---|
Azure | App service’s managed identity | Callee’s app registration ID |
AVD | Machine’s managed identity | Callee’s app registration ID |
Developer | Personal access token | https://management.core.windoes.net/ |
Next, we will show some key code snippets on Caller. We will also create and deploy the sample Caller and Callee applications to show the authentication in action.
Source code#
A minimal example for caller and callee applications can be found in this GitHub:
A minimal example showing how to protect an Azure app service using App Registration and Managed Identity
Here are some key points I want to highlight:
Caller#
TokenCredential#
A TokenCredential
singleton can be created in app start:
// In Startup.cs or Program.cs.
// You should create different ChainedTokenCredential for different environments.
// This is a minimal example for demo purpose only.
builder.Services.AddSingleton<TokenCredential>(serviceProvider => {
var objId = "YOUR_MANAGED_IDENTITY_OBJECT_ID";
return new ChainedTokenCredential(
new AzureCliCredential(), // for development only
(string.IsNullOrEmpty(objId) ?
new ManagedIdentityCredential() : // system assigned managed identity
new ManagedIdentityCredential(
ManagedIdentityId.FromUserAssignedObjectId(objId)
) // user-assigned managed identity
)
);
});
You need to choose different credential provider under different environments.
Environment | Recommended Credential | Reason |
---|---|---|
Azure | ManagedIdentityCredential(ManagedIdentityId) | 1. An app service should have the managed identity. 2. You should provide it explicitly in order to avoid misuse of other managed identities. |
AVD (Azure Virtual Desktop) | ManagedIdentityCredential() | An AVD as a Azure resource under the same subscription should have a system assigned managed identity. |
Visual Studio with Azure Account | VisualStudioCredential() | Use the identity same as the user signed in in Visual Studio so that the Callee knows the Caller’s identity. |
Mac | AzureCliCredential | This is one of the clients you can use in Mac. |
Note on DefaultAzureCredential and ChainedTokenCredential#
I prefer using ChainedTokenCredential, which allows me to choose which credential(s) to use, and define the attempt sequence that fits my requirement.
You may also consider using DefaultAzureCredential in non-production environments. However, using this might give you unexpected result because:
- the attempt sequence might not in the desired order
- you may not know which credential is picked eventually, unless you configure and inspect the logs
For example, if a developer signed in to Azure account in Visual Studio on an Azure Virtual Desktop instance, DefaultAzureCredential
will attempt to authenticate with ManagedIdentityCredential
before VisualStudioCredential
. If the app want to use developer’s personal identity over machine’s identity, this will not work as expected.
Access Token#
The scope of the access token which Caller is going to generate is the Callee’s app ID. Get an access token and add it as the Bearer authorization token when making API request to the Callee:
// In Program.cs. You can also use the MVC pattern.
app.MapGet("/callee/protected-resource", async (IConfiguration configuration, TokenCredential credential) =>
{
var myScope = "CALLEE_APP_REGISTRATION_ID";
AccessToken token = await credential.GetTokenAsync(
new TokenRequestContext(
scopes: new[] { myScope }),
new CancellationTokenSource().Token
)
);
using var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token.Token);
// make an HTTP request to a protected API endpoint of the Callee...
}
Callee#
Nothing special about the callee. Make sure sure that Azure is configured properly and let the Caller(s) know your Azure App Registration ID.
Demo#
Checkout the minimal GitHub repository:
A minimal example showing how to protect an Azure app service using App Registration and Managed Identity
If you the following tools installed:
Callee#
- Open
poc-callee
in VS Code - In VS Code, open the Command Palette and select Azure: Deploy to Web App…
- Choose poc-callee, add config and choose the Azure App Service:
- Expected result for a successful deployment:
- Open Command Palette and select Azure App Service: Browse Website:
- You should see a webpage with a message
You do not have permission to view this directory or page
. That is expected because you don’t have a valid access token.
Caller#
- Open
poc-caller
in VS Code - Modify
appsettings.json
:- CalleeApi: the root url of the Callee e.g. https://{callee-app-name}-{random-hash}.{region}-01.azurewebsites.net/
- CalleeAppRegistrationId: the App Registration ID of the Callee
- ManagedIdentity: Caller’s user/system-assigned managed identity Object (Principal) ID
- Choose poc-caller, add the config and choose the Azure App Service:
- Expected result for a successful deployment:
- Choose Azure App Service: Browse Website in Command Palette. You should be able to see a Swagger UI.
- Call
GET /remote-ping
endpoint and you should see a 200 response. The response body should contains a greeting message and a token:
Local Development#
If you want to run Caller locally, and call the Callee API endpoints which hosting on Azure, you will need to:
- Get your personal Object (principal) ID (if you are using Azure CLI or signed in user in Visual Studio)
- Add the Object (principal) ID to Callee’s allowed identities
- Add
https://management.core.windows.net/
to Callee’s Allowed token audiences
Limitations#
This is a simple, all-or-nothing authentication solution for the Callee app. In other words, you can’t make certain endpoints public while the others keep protected. Also, there will be some code changes in order to let all Caller apps be able to get an Azure access token.
Conclusion#
In this post, we demonstrated how to secure Azure App Service API endpoints using Managed Identity, App Registration, and built-in Authentication settings. This approach enables access control across services and environments within the same Azure subscription without requiring custom authentication logic. While simple and effective, itβs best suited for internal APIs where all access can be gated uniformly.
References#
- https://learn.microsoft.com/en-us/dotnet/api/azure.identity.defaultazurecredential?view=azure-dotnet
- https://techcommunity.microsoft.com/blog/microsoft-security-blog/add-authentication-to-your-azure-app-service-or-function-app-using-microsoft-ent/4372492
- https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app
- https://jwt.io/
- https://learn.microsoft.com/en-us/azure/healthcare-apis/get-access-token?tabs=azure-cli
- https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/overview
- https://learn.microsoft.com/en-us/dotnet/api/microsoft.identity.client.appconfig.managedidentityid?view=msal-dotnet-latest
- https://learn.microsoft.com/en-us/azure/app-service/scenario-secure-app-authentication-app-service?tabs=workforce-configuration