Use managed identities to configure authentication and authorization among Azure Functions

Akihiro Nishikawa
Microsoft Azure
Published in
6 min readMay 17, 2023

The original article was published in Japanese.

First of all

I had an opportunity to talk at JDConf 2022 in “lightening talk” part.

My talk is available on YouTube, but I didn’t check the YouTube recording after the event. One year later, I saw that some people commented on my talk. So, I am posting this entry as a supplement for it. The code created for this talk is available in the following repository.

Managed Identities

Managed Identities allow us to authenticate/authorize access to resources from specified resources without user credentials. The following document gives us an overview of Managed Identities, and some use cases, such as how to configure RBAC for a storage service, are also covered in subsequent pages.

Both App Services and Azure Functions are, of course, Azure resources. This allows called applications to authorize access with the managed identities of the callers, even for HTTP-based access between them.

To do this, a bearer token should be added to the Authorization header in the same way as normal REST API authentication. In this post, I will describe how to obtain the bearer token from the managed identity as a reminder.

Configuration

The following diagram describes the relationship between caller and callee applications.

In this article, both caller and callee applications are implemented using Azure Functions. The application runtime is Java (JDK 17) on Linux and C# (.NET 6) on Windows. C# to C# and Java to Java should work fine, and I’d like to verify interoperability between polyglot applications.

Application platform

In this post, Azure Functions are used, but Azure App Service is also fine because the same authentication scheme (Easy Auth) is used.

1. Create Function Apps

“Caller” Function Apps (func-cs01 and func-j01) can invoke either “callee” Function Apps (func-cs02 or func-j02) with query parameter.

# Caller created in C#
https://func-cs01.azurewebsites.net/api/call-from-cs?type=[j02|c02]
# Caller created in Java
https://func-j01.azurewebsites.net/api/call-from-java?type=[j02|c02]

Responses from callee apps include HTTP status code and the response from func-*02. The following example is a response from func-cs01 calling func-cs02.

{
"response": "This is responded by function [auth-cs] in func-cs02. It means HTTP triggered function executed successfully.",
"status": 200
}

2. Enable Managed Identities on caller applications

Managed Identities should be enabled on caller applications (func-cs01 and func-j01). Either user-assigned or system-assigned managed identities are fine. In this post, I have used system-assigned managed identities.

When generating managed identities, we tend to configure RBAC for callee applications with the managed identities of caller applications. However, RBAC configuration is not required since it is fine if caller applications are authenticated against Azure AD.

3. Configure authentication

“Easy Auth” scheme should be enabled on func-cs02 or func-j02 (Easy Auth scheme is not required for configuration on func-*01).

In this post, Azure Active Directory is used as the Identity Provider. From Azure Portal, please select [Settings] > [Authentication] > [Add Identity Provider] and select “Microsoft” from the “identity provider” drop-down list.

After selecting an identity provider, several configuration options will appear. The configurations on this page are listed below.

  • App registration type: Create new app registration
  • Supported account types: Current tenant — Single tenant
  • Restrict access: Require authentication
  • Unauthenticated requests: HTTP 401 Unauthorized: recommended for APIs
  • Microsoft Graph permissions: User: leave default setting [user.read]

After clicking Add button, the following page will appear, which contains the App ID. I recommend that you write down the ID that will be used when obtaining a bearer token with the managed identity. We can also see the ID later.

At this point, if either func-cs02 or func-j02 is called without an authorization header, the callee application responds 401 (Unauthorized) . func-cs02 returns HTTP status with error message, while func-j02 does not. This response is not customized in each function app.

Response from func-cs02 without authorization header
Response from func-j02 without authorization header

Codes

a) Caller (func-cs01 and func-j01)

We need to implement codes to obtain the bearer token from its managed identity and attach the token to Authorization header.

In case of C#, the following code should be added.

// Application ID of callee
string audienceId;

// audienceId is the app ID of func-*02
string accessToken = await new AzureServiceTokenProvider().GetAccessTokenAsync(audienceId);
HttpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken);

In case of Java, the following code should be added. Since I am using system-assigned managed identities in this post, there is no need to specify Client ID using clientId("clientId"). If using user-assigned managed identities, Client ID should be specified with the method, of course.

// [Scope] api://{Application ID}/.default

// Specify Application ID of func-*02
TokenRequestContext tokenRequestContext = new TokenRequestContext().addScopes("api://<Application ID>/.default");

// If also doing test in local environment, use DefaultAzureCredential
DefaultAzureCredential dfaultAzureCredential = new DefaultAzureCredentialBuilder().build();
String accessToken = defaultAzureCredential.getToken(tokenRequestContext).map(AccessToken::getToken).block();

// ManagedIdentityCredential also works
ManagedIdentityCredential managedIdentityCredential = new ManagedIdentityCredentialBuilder().build();
String accessToken = managedIdentityCredential.getToken(tokenRequestContext).map(AccessToken::getToken).block();

b) Callee (func-cs02 and func-j02)

If JWT verification is not required, there is no need to add code, since “Easy Auth” works fine. If JWT verification is required, however, the bearer token in HTTP header should be verified.

Note that all HTTP header keys in Java function apps areall lowercase. The following screenshot is taken from Log Stream of func-j02. The red, green, and orange underlined parts represent Object ID of func-j01, App ID of func-j01, and Audience ID (api://{app ID of func-j02}) respectively.

Log steam of func-j02 (when func-j02 was called from func-j01).

The following screenshot was taken from Log Stream of func-cs02. As you can see, HTTP header keys are not limited to lowercase.

Log steam of func-j02 (when func-cs02 was called from func-j01).

That’s it. Let us do test!

Test

[1] calling func-cs02 via func-cs01

[2] calling func-j02 from func-cs01

[3] calling func-cs02 from func-j01

[4] calling func-j02 from func-j01

Conclusion

In this post, I described how to configure authentication with managed identity. Key takeaways are …

  • Managed identities can be used for not only RBAC but also
    authentication.
  • Password-less authentication scheme frees our efforts from
    credential management.
  • When using managed identity in Functions/App Services, we
    can add a bearer token extracted from the managed identity to
    authorization header, like OAuth 2.0 and OIDC client.

I hope this post helps you.

--

--

Akihiro Nishikawa
Microsoft Azure

Cloud Solution Architect @ Microsoft, and JJUG (Japan Java Users Group) board member. ♥Java (JVM/GraalVM) and open-source technologies. All views are my own.