Azure application-to-application authentication with Managed Identity
Recently I worked on a project where we built integration services to move data between Azure applications over HTTP. An overly simplified diagram would look like below
The simplest way to authenticate the integration service with the API endpoints is to include tokens in the HTTP requests. However, managing these tokens, especially when there are many integration end-points creates a lot of overhead. The team would need to keep track of which tokens are used by which apps so that they can be refreshed if needed.
That was before we discovered a more manageable solution — Azure Managed Identity. It is a built in feature of Microsoft Azure. It allows applications to authenticate — without needing a password — with Azure services such as SQL Server and Key Vault. The coolest thing is that Managed Identity works between Azure applications as well. In this article, we will go from a very simple authentication scenario to a more complex one where role authorisation is required.
Basic application-to-application authentication
I have prepared a sample on GitHub. It is a very simple solution consisting of two API endpoints ManagedIdentitySample.Server
and ManagedIdentitySample.Client
. They both have one endpoint each - the WeatherForecastController
. The only difference is that the Server
requires authentication and the Client
doesn’t. The flow looks like this:
- The user sends a request to the
Client
to get the weather forecast. - The client uses the nuget package
Microsoft.Azure.Services.AppAuthentication
to get the JWT (JSON web token). - The
Client
includes the token in the HTTP request to retrieve the weather forecast from theServer
. - Finally, the weather forecast is returned to the user.
Azure Active Directory application (AD app)
First, we need to create an Auzre AD app for the Server to authenticate its clients. In Azure PowerShell, run:
# Connect to Azure AD
Connect-AzureAD -Credential -TenantId "Your tenant Id"# Create the AD app
New-AzureADApplication -DisplayName "ManagedIdentitySample.Server AD Auth"
The result should look like this:
Take note of the AppId because we will use it in later steps.
ManagedIdentitySample.Server
The Server
uses two nuget packages to handle authentication and authorisation:
Microsoft.AspNetCore.Authorization
Microsoft.AspNetCore.Authentication.JwtBearer
JWT authentication is configured as below:
The only missing pieces are the tenant ID and client ID which are in appsettings.json:
{
"Auth": {
"TenantId": "Your tenant ID",
"ClientId": "ManagedIdentitySample.Server AD Auth app ID"
}
}
ManagedIdentitySample.Client
In the Client
, we install the Microsoft.Azure.Services.AppAuthentication
nuget package. It provides everything we need to exchange and refresh JWT.
We register a named HTTP client with the JWT in Authorization
header.
Inside appsettings.json
, we need to specify the Server
address and the AD application ID:
{
"WeatherBaseApi": "https://the-server-address.azurewebsites.net",
"AdAppId": "ManagedIdentitySample.Server AD Auth app ID"
}
Deployment
After deploying both the Server
and Client
to Azure, the final step is to enable Manged Identity for the Client
. This can be done inside the Identity setting section:
Navigating to the Client
, the weather forecast should return:
Really? Is that all?
Yes, that is everything required to enable authentication between two Azure applications hosted in the same tenant. This is because by default the Azure AD app accepts anyone/any application within the same tenant.
Debugging locally
We might run into scenarios where we need to debug the Client
locally against a Server
hosted on Azure. Leaving the Client
as is, AzureServiceTokenProvider.GetAccessTokenAsync
will not work. Luckily, the fix is pretty straight forward.
To do this, we need to create another Azure AD app that acts as the proxy for local development:
# Connect to Azure AD
Connect-AzureAD -Credential -TenantId "Your tenant Id"# Create the AD app
$Object = New-AzureADApplication -DisplayName "ManagedIdentitySample.Client local dev"# Create a secret
New-AzureADApplicationPasswordCredential -ObjectId $Object.ObjectId
Take note of the application Id and secret as we will use them later:
Secondly, we need to build a connection string for local dev. The full document can be found here.
RunAs=App;AppId={AppId};TenantId={TenantId};AppKey={ClientSecret}
- The connection string can be passed directly into the
AzureServiceTokenProvider
constructor. - The token can also be stored as an environment variable and will magically be used. This is recommended to eliminate the risk of accidentally checking in the connection string to source control.
Authorisation
In the real world scenario, we would like to only allow certain users or services to access our protected API, even though they are from the same organisation. To do this, we need to go to the Enterprise application behind the Azure AD app:
Under the Properties section, make sure that the “User assignment required?” is enabled.
Once this setting takes effect which may take a few minutes, the Client
won’t be able to retrieve weather information anymore. We will receive an error similar to:
Application 'Client App ID' is not assigned to a role for the application 'Server App ID'.
Note: we might need to restart the Client
as AzureServiceTokenProvider
caches the access token.
This means we need to explicitly give the Client
a role in the Server
. At the time of writing this article, the Azure portal UI didn’t support it yet for application-to-application but it can be done from the Azure PowerShell.
We will first need the Object Id from when we enabled Managed Identity for the client previously. Then, run the below script in Azure PowerShell:
# Connect to Azure AD
Connect-AzureAD -Credential -TenantId "Your tenant Id"# Grant access
New-AzureADServiceAppRoleAssignment -ObjectId "Client object Id" -PrincipalId "Client object Id" -Id 00000000-0000-0000-0000-000000000000 -ResourceId "ManagedIdentitySample.Server AD Auth app Object ID"# Empty GUID stands for Default access
# Note, we need the Object Id from the Server app, instead of App Id like previously
Going back to the client, the weather forecast end-point should work again.
What if we want role based authorisation?
Yes, it can be done too.
First of all, we need to create roles in our Azure AD application. Again, Azure Portal UI doesn’t support it yet. It needs to be done via the application manifest
The application manifest is an JSON document. The roles are defined under appRoles
element. Let’s add the Read
role
{
"allowedMemberTypes": [
"User",
"Application"
],
"description": "Able to read weather forecase",
"displayName": "Read",
"id": "47fbb575-859a-4941-89c9-0f7a6c30beac",
"isEnabled": true,
"lang": null,
"origin": "Application",
"value": "Read"
}
Let’s go back to the Server
code and add an Authorize
attribute to the weather forecast end-point and re-deploy it.
[ApiController]
[Route("[controller]")]
[Authorize(Roles = "Read")]
public class WeatherForecastController : ControllerBase
At this point, the Client
again stops working, this time with a 403 error.
This can be fixed easy with the same PowerShell script that we used before to grant Default Access to the Client
app. This time, we use the role ID instead of an empty GUID.
# Grant access
New-AzureADServiceAppRoleAssignment -ObjectId "Client object Id" -PrincipalId "Client object Id" -Id 47fbb575-859a-4941-89c9-0f7a6c30beac -ResourceId "ManagedIdentitySample.Server AD Auth app Object ID"
# The Id the the role Id that we created previously
Restart the Client
to force a refresh token, then it should work again.
In conclusion, in this article we explored how to use Azure Managed Identity for application-to-application authentication. We also learnt how to work with Azure AD role based authorisation.