Emulate Managed Identities during development
Ever used Managed Identities in Azure? You should, it’s a great way to get rid off passwords in your code and configuration. In this post I’ll show you how managed identities actually work. And how I built a small app that should help you use the ManagedIdentityCredential
in Docker and or during development.
TL:DR
Don’t want to be bothered with all the technical details of how the Managed Identity Client actually works, but just want to see how to use it in docker? Jump to Emulating the token endpoint.
What are Managed Identities?
I won’t go into what managed identities are or how to use it. Check out these resources if you want to know more:
- Managed identities for Azure Resources
- Secretless applications: Use Azure Identity SDK to access data
- Azure Identity Client SDK
Managed identities are a way for applications running in Azure to “magically” get tokens for all resources in Azure that the provisioned identity has access to.
Different Token Credentials
Microsoft has several Token Credentials, I’ll explain some of them briefly.
ChainedTokenCredential
The ChainedTokenCredential allows you to chain several Token credentials together. They will be used in the order you added them, and the first token credential that returns a token will be used. Since you can pick the order this can be configured exactly the way you want.
DefaultAzureCredential
You can see the DefaultAzureCredential as a ChainedTokenCredential with some useful defaults set. If you use it without any options it will use the following credentials (in this order). Using the options in the constructor you can exclude some of them. Once you start excluding several of them, you might want to look into the ChainedTokenCredential since that might speedup your code as it won’t have to do the discovery of the right source.
- EnvironmentCredential
- WorkloadIdentityCredential
- ManagedIdentityCredential
- AzureDeveloperCliCredential
- SharedTokenCacheCredential
- VisualStudioCredential
- VisualStudioCodeCredential
- AzureCliCredential
- AzurePowerShellCredential
- InteractiveBrowserCredential
ManagedIdentityCredential
The ManagedIdentityCredential, is one of the credentials in the DefaultAzureCredential and it’s one used when you’re using Managed Identities in Azure. It allows you to get a token from one of the methods available in Azure, and it will pick the first one available.
You can check the source of the ManagedIdentityCredential here. Which will then call the ManagedIdentityClient, which will then automatically select any of the available ManagedIdentitySource
s.
CloudShell Managed Identity Source
I went through all the sources and found that the CloudShellManagedIdentitySource just uses one environment variable, namely MSI_ENDPOINT
. And the cloud shell is the only point where I can directly access that endpoint to see what it actually does.
# Show the value of the environment variable
$env:MSI_ENDPOINT
# the output was http://localhost:50342/oauth2/token
In the source code of the CloudShellManagedIdentitySource I found that it then does a http request that looks like:
POST http://localhost:50342/oauth2/token
Metadata: true
Content-Type: application/x-www-form-urlencoded
...
resource=http%3A%2F%2Fmanagement.azure.com
I must admit at this point I also tried setting the MSI_ENDPOINT
in my test app to some requestinspector url, just to see what happens. And as expected my local app tried to get a token from this external endpoint.
So I tried to get a token in the CloudShell to see what the response should look like.
# Change the endpoint to the result from the message above
$r = iwr http://localhost:50342/oauth2/token -Method 'POST' -Headers @{'Metadata' = 'true'} -Body 'resource=https%3A%2F%2Fmanagement.azure.com'
$r.Content
This results in a message like the one below, my guess is that all managed identity endpoints respond with a similar message.
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Ii1LSTNROW5OUjdiUm9meG1lWm9YcWJIWkdldyIsImtpZCI6Ii1LSTNROW5OUjdiUm9meG1lWm9YcWJIWkdldyJ9.eyJhdWQiOiJodHRwczov___remove__for__security___.XvOBGhCw8Qr___remove__for__security___w",
"refresh_token": "",
"expires_in": "3822",
"expires_on": "1691056808",
"not_before": "1691052614",
"resource": "https://management.core.windows.net/",
"token_type": "Bearer"
}
So to summerize the working of the Managed Identity Client, it seems to work like this:
- Find first available Managed Identity Source based on environment variables.
- If just
MSI_ENDPOINT
is found, it will pick the CloudShellManagedIdentitySource - This source uses the endpoint and the resource you want a token for, which is just the bare application uri you can set for all your app registrations in the Azure Portal.
- The resource is posted, url form endcoded, to the url specified in
MSI_ENDPOINT
together with theMetadata: true
header (this header is important or it does not work). - The MSI Endpoint returns a json message with the resource and the access token.
For those watching closely, the resource in the response is different than the resource I requested. Even though the Azure portal UI does not allow this, it’s perfectly possible to se multiple App Uri’s for a single app through the manifest editor in the portal and/or the api. My guess is that https://management.core.windows.net/
is the primary App Uri and https://management.azure.com
is one of the secondary App Uri’s.
Emulating the token endpoint
The MSI token endpoint is just a pre-defined way to get a token for some api. Which API, is send as an FORM encoded message.
I say emulating the token endpoint because this method will use some sort of credentials to get the token. Which credentials is up to you.
We now know which endpoints we need, I added them to my Assertion.Proxy. You can choose which credentials you wish to use by picking the correct endpoint.
Install and run this global tool
dotnet tool install --global Smartersoft.Identity.Client.Assertion.Proxy --version 0.7.0
az-kv-proxy
This will start a web api on you local machine, with swagger support. Open the documentation at http://localhost:5616/swagger/index.html
as specified in the console output.
Pick any of the MSI
endpoints, which are modeled to emulate the token endpoint as expected by the CloudShellManagedIdentitySource.
Use DefaultAzureCredential in Docker
We want to build and deploy all our apps as docker containers to not be bound to specific deployment methods. To not have any surprises we like to debug our applications in docker as well. None of the credentials seem to work in docker, not even the DefaultAzureCredential. This is an issue for us, since we don’t like passwords in our development environments.
For simplicity I’ll be using the /api/Msi/Forward
endpoint, that just forwards the request to Azure using the DefaultAzureCredential, but since the assertion proxy is running as your local user, it does have access to some credentials like VisualStudioCredential
or AzureCliCredential
and even InteractiveBrowserCredential
.
Just use the ManagedIdentityCredential
or the DefaultAzureCredential
in your application that you want to run in docker and set the environment variable MSI_ENDPOINT
to http://host.docker.internal:5616/api/msi/forward
.
You can do this in Visual Studio by editing the launch settings, if you want use this during debugging.
{
"profiles": {
"ConsoleApp1": {
"commandName": "Project",
"environmentVariables": {
"MSI_ENDPOINT_External": "https://requestinspector.com/inspect/01h6rc1cjvr9ktkca8n31nvfp5",
"MSI_ENDPOINT": "http://localhost:5616/api/Msi/forward"
}
},
"Docker": {
"commandName": "Docker",
"environmentVariables": {
"MSI_ENDPOINT": "http://host.docker.internal:5616/api/Msi/forward"
}
}
}
}
Sample app running in Docker
You don’t have to believe me that this actually works, just create a new console app in Visual studio, change the code to the code below (install Azure.Identity
). And right click the project to add docker support.
Then you only have to change the environment variable.
using Azure.Identity;
namespace ConsoleApp1;
internal class Program
{
static async Task Main(string[] args)
{
Console.WriteLine("Hello, World from Docker!");
var credentials = new ManagedIdentityCredential();
var tokenResult = await credentials.GetTokenAsync(new Azure.Core.TokenRequestContext(new[] { "https://management.azure.com/.default" }));
Console.WriteLine("Token Length: " + tokenResult.Token.Length);
var splitToken = tokenResult.Token.Split('.');
Console.WriteLine("Token header: {0}", splitToken[0]);
Console.WriteLine("Token payload: {0}", splitToken[1]);
Console.WriteLine();
Console.WriteLine("This is an actual token from Azure.Identity.ManagedIdentityCredential.");
}
}
Emulate token endpoint in Azure Functions
I recon it would not be very hard to emulate the MSI endpoint in Azure Functions. That would allow you to actually use a managed identity (the one provisioned for the Azure Functions App) during development. And truly test your app as if it was using managed identities.
Then you would be able to set the MSI_ENDPOINT
variable to the address of your function, hopefully at least protected with a functions key and some IP restriction. I won’t be providing the exact code, if you start messing with these kind of solutions there is bound to be a security issue. And I don’t want to be responsible.
This might also enable multi tenant scenarios, which happens to be the only thing missing in building applications for multiple tenants. Something like provision this Azure Functions app with a managed identity, and send us the url you can get for this specific function. Again, not recommending this!
Series: Managed Identity
- Using Managed Identity without Azure
- Access Azure AD protected API with managed Identity
- Using a managed identity as a client credential
- Proof of concept: Multi tenant managed identity
- Configure SQL with managed identity
- Emulate Managed Identities during development
- How do Federated credentials in GitHub Actions actually work
- Azure SQL and Entra ID authentication, tips from the field
Conclusion
This post shows you how to trick the Azure.Identity
SDK into getting a token from a different endpoint then the actual token endpoint used for .
The Assertion.Proxy is built for DEVELOPMENT PURPOSES only! Please DO NOT start using it in production! It isn’t a good idea to expose an endpoint that allows getting tokens for various Api’s if you do not manage access to it correctly.