A couple of weeks ago we were looking at some stuff with managed identity and functions and I forgot about a couple of things so I wanted to make some notes for myself to clarify the scenarios and the configurations for each of them as I found some of the documentation a little painful to follow.

For my scenario I have an api in Azure APIM which I want to protect with JWT validation and then it will just return the list of claims. I will then have some code for a function App which I will deploy to 3 separate function apps. The policies for APIM and the code for the functions are at the bottom of the article.

I have also created 2 user assigned managed identities in the resource group to represent the 2 user assigned identities I want to use and I have added a system assigned managed identity for scenarios 1 and 4 to those function apps.

Scenario 1 – Basic use of system assigned identity

In scenario 1 I expect that the 2 functions for using the user assigned identity will fail because I havent assigned them to the function app.

The DefaultCredentialFunction function works fine because I gave the function app a system assigned managed identity.

The key thing here is that when using the below code snippet it will automatically use the system assigned managed identity.

var creds = new DefaultAzureCredential();
var token = await creds.GetTokenAsync(new TokenRequestContext(new[] { scope }));

This scenario is pretty straightforward and the one most people will be using.

Scenario 2 – Basic use of User Assigned Identity

In scenario 2 I have not got a system assigned managed identity on the function app but I have associated both user assigned identities with the function app.

DefaultCredentialFunction will fail this time because there is no system assigned identity which the default credential will pick up.

The 2 other functions UserAssigned_User1 and UserAssigned_User2 will now both work thou (they didnt in scenario 1) because the 2 functions use client id’s associated with my 2 user assigned identities but this time those 2 user assigned identities are both associated with the function app. The difference in the code between the functions that use the default identity and explicitly specifying a managed identity is the key bit here and in the code snippets at the bottom you will see the use of ManagedIdentityCredential class rather than DefaultAzureCredential as shown below.

var creds = new ManagedIdentityCredential(userAssignedIdentityClientId);
var token = await creds.GetTokenAsync(new TokenRequestContext(new[] { scope }));

Scenario 3 – Using a user assigned identity as the default credential

In scenario 2 we know that DefaultCredentialFunction failed and this was the scenario I couldnt remember how it needed to work. I want the default credential to pick up one of the user assigned identities and not need me to have a system assigned identity.

In this function app I have both user assigned identities associated with the function app like in scenario 2 which means that the functions UserAssigned_User1 and UserAssigned_User2 both still work like before but the change I made is to add an app setting called AZURE_CLIENT_ID which has the value using the client id matching the user assigned identity that I want to be the default. Adding this app setting means DefaultCredentialFunction function now works too and uses the user assigned identity I told it to use.

Scenario 4 – Combining User Assigned Identity and System Assigned Identity

In scenario 4 I had the same functions deployed and associated both user assigned identities with the function app and also associated a system assigned identity with it.

Functions UserAssigned_User1 and UserAssigned_User2 both continue to work as before using the respective user assigned identities because I specify the client id to use in the code when I setup to get a token.

DefaultCredentialFunction will this time use the system assigned managed identity for the function app. There is no need to specify the AZURE_CLIENT_ID setting.

Conclusion

Hopefully the above notes make it a little clearer which combination of code and configuration you use for each scenario. Its pretty cool that you can easily set the client id via app settings to use a user assigned identity by default and also that you can easily combine the use of system and user assigned identities within the same app.

Code Snippets

Below are the code snippets I used in the scenarios I was checking.

APIM Policy

In the APIM policy I am using a basic jwt validation to ensure the token is from my tenant and relates to the app registration I created to represent the api.


<policies>
    <inbound>
        <base />
        <validate-jwt header-name="Authorization" failed-validation-httpcode="401" require-scheme="Bearer">
            <openid-config url="https://login.microsoftonline.com/{my-tenant-id}/v2.0/.well-known/openid-configuration" />
            <audiences>
                <audience>0e8692de-3d4f-4eef-b0b7-40ac9b75f269</audience>
            </audiences>
            <issuers>
                <issuer>https://sts.windows.net/{my-tenant-id}/</issuer>
            </issuers>
        </validate-jwt>
        <return-response>
            <set-status code="200" />
            <set-body>@{

                string parsedToken = "error";
                string tokenHeader = context.Request.Headers.GetValueOrDefault("Authorization", "");
                if (tokenHeader?.Length > 0)
                {
                    Jwt jwt;
                    if (tokenHeader.TryParseJwt(out jwt))
                    {
                        foreach(var claim in jwt.Claims)
                        {
                            parsedToken += claim.Key + ":" + string.Join("-",claim.Value) + ";";
                            parsedToken += "\r\n";
                        }
                    }
                }

                var message = "The following claims are in the token: \r\n" + parsedToken;
                return message;
            }</set-body>
        </return-response>
    </inbound>
    <backend>
        <base />
    </backend>
    <outbound>
        <base />
    </outbound>
    <on-error>
        <base />
    </on-error>
</policies>

Function 1 – Using Default Credential

In this function I use the DefaultAzureCredential and then get a token with it for the scope relating to my API and then add this to the authorization header.

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Azure.Identity;
using System.Net.Http.Headers;
using System.Net.Http;
using Azure.Core;

namespace MSI_FunctionApp
{
    public static class DefaultCredentialFunction
    {
        [FunctionName("DefaultCredentialFunction")]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
            ILogger log)
        {
            log.LogInformation("C# HTTP trigger function processed a request.");

            var scope = "0e8692de-3d4f-4eef-b0b7-40ac9b75f269/.default";

            var creds = new DefaultAzureCredential();
            var token = await creds.GetTokenAsync(new TokenRequestContext(new[] { scope }));
            using (HttpClient client = new HttpClient())
            {
                client.DefaultRequestHeaders.TryAddWithoutValidation("Ocp-Apim-Subscription-Key", FuncSettings.subscriptionKey);
                client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.Token);
                var result = await client.GetAsync(FuncSettings.url);
                var responseMessage = await result.Content.ReadAsStringAsync();

                return new OkObjectResult(responseMessage);
            }

        }
    }
}

Function 2 & 3 – Using a managed identity with a supplied client id

This function is pretty much the same as using the default credential but I replace the DefaultAzureCredential with a ManagedIdentityCredential and then supply the client id I want to use.

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Azure.Identity;
using System.Net.Http.Headers;
using System.Net.Http;
using Azure.Core;

namespace MSI_FunctionApp
{
    public static class UserAssigned_User1
    {
        //Use the client id for user assigned identity 1
        [FunctionName("UserAssigned_User1")]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
            ILogger log)
        {
            log.LogInformation("C# HTTP trigger function processed a request.");

            var scope = "0e8692de-3d4f-4eef-b0b7-40ac9b75f269/.default";
            var userAssignedIdentityClientId = "0ca7dee5-706b-470a-af57-a288aa4054b9";

            var creds = new ManagedIdentityCredential(userAssignedIdentityClientId);
            var token = await creds.GetTokenAsync(new TokenRequestContext(new[] { scope }));
            using (HttpClient client = new HttpClient())
            {
                client.DefaultRequestHeaders.TryAddWithoutValidation("Ocp-Apim-Subscription-Key", FuncSettings.subscriptionKey);
                client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.Token);
                var result = await client.GetAsync(FuncSettings.url);
                var responseMessage = await result.Content.ReadAsStringAsync();

                return new OkObjectResult(responseMessage);
            }

        }
    }
}

 

Buy Me A Coffee