Azure Single Sign-On Case Study #2: Unmasking authentication in Function Apps

Explore the details of configuring Single Sign-On for Azure Function Apps and discover the potential security risks associated with custom Application ID URIs.

Natalia Trojanowska-Korepta 2024.06.13   –   12 min read

Recently, I encountered a fascinating vulnerability. Even though our client’s Function App wasn’t configured to be a multi-tenant application, I was able to gain access to it using a token issued by a different Azure tenant. I instantly became curious: how did this happen? Did they implement the authorization mechanism from scratch, or is it a cloud misconfiguration? Of course, I hoped for the latter, as it would make my case study much more interesting. How easy (or how difficult) would it be to make the same mistake?  

Starting with an attempt to replicate the vulnerable setup, my research led me to some interesting discoveries that I would like to share with you.  

In this article, you will discover when a globally unique Application ID URI is not unique after all and how to exploit this subtle detail to gain access to insecure Function Apps in other tenants. Here, I will also: 

  • Talk about Function App Single Sign-On mechanism, 
  • Discuss different configuration options of a Function App, 
  • Demonstrate a possible misconfiguration of a vulnerable Azure Function App, 
  • Examine App ID intricacies in App Registration. 

A tale of an insecure Function App 

During a penetration test, I discovered a Function App that performed some operations on users in Microsoft Entra ID. The authorization was based on JWT Bearer tokens.  

To acquire a token, the backend of an application would call the Microsoft OAuth endpoint using the client credentials flow. The client ID and client secret were securely stored in a key store. The token was issued for a resource called api://insecure-function-app.  

To verify whether the access control mechanism had been implemented correctly, I decided to create an App Registration with the same App ID URI (api://insecure-function-app) in my Azure tenant.  

Then, I created a new client secret and used it to send an HTTP request to the Microsoft OAuth endpoint: 

POST /[my tenant ID]/oauth2/v2.0/token HTTP/1.1 
Host: login.microsoftonline.com 
Content-Type: application/x-www-form-urlencoded 
 
client_id=[my client ID]&client_secret=[my client
secret]&grant_type=client_credentials&scope=api://insecure-function-
app/.default 

In the response I received an access token. To my surprise, I was able to send authorized requests to the vulnerable Function App belonging to another tenant! 

How did this misconfiguration happen? We will get to that, but first, we have some more topics to discuss. 

Function App Single Sign-On 

By default, a new Function App HTTP Trigger uses a function authorization level (we will explore it in my next article), which provides some protection against unwanted access. However, API keys are generally not considered a sufficient security measure. Instead, we should use OAuth. In the case of server-to-server communication, it will be the client credentials flow, which allows us to exchange client ID and client secret for a time-constrained JWT. It is not only a more secure solution, but also a much more flexible one, allowing you to manage digital identities with their accesses more easily and more granularly. 

It is possible to set up Single Sign-On authentication in Function App > Settings > Authentication

There are various options to choose from, but in this article, I will focus on the native Microsoft identity provider.  

I set up authentication using the default settings, which are: 

App registration typeCreate new app registration
Supported account typesCurrent tenant – Single tenant
Client application requirement Allow requests only from this application itself
Identity requirementAllow requests from any identity
Tenant requirementAllow requests only from the issuer tenant 

Afterward, I also changed the authorization level of my HTTP trigger to anonymous—otherwise, you would have to supply both the key and the Bearer token. 

The default Microsoft authorization configuration for Function Apps can be considered secure. My Function App can now only be called using a token generated for the specific App Registration in my tenant. 

Azure Function App configuration options 

As we set up the authentication method, the Function App will now perform authorization based on an access token (JWT): 

The following claims will be considered: 

  • aud (audience): The App ID URI of the App Registration we requested access to (e.g., api://my-function-app), 
  • appid (App ID) or azp (Authorized Party): The App ID of the App Registration that requested the access, 
  • oid (Object ID): Object ID of the Enterprise Application (in case of client credentials) or the user (in case of the authorization code flow), 
  • iss (issuer): The URL address of the tenant (https://sts.windows.net/{tenantId}). 

With the above being a simplification, you can find more information about access token claims in the Microsoft documentation

When configuring the authentication options, we basically tell our Function App what values it should accept if present in the above claims. 

Now, let’s take a look at the possible configuration options. 

Audience 

The expected audience of the access token can be configured as Allowed token audiences

The token audience corresponds to App ID URI that is set up in App Registration > Expose an API

You can allow multiple audiences. 

Authorized Party 

As opposed to custom scopes, as I described in my previous article, any App Registration from a specific tenant can ask for an access token for a given App ID with a .default scope. For that reason, we can add a Client application requirement—so we not only accept access tokens for a specific audience but also check the Authorized Party claim that identifies which App Registration instance asked for this access token. We can also choose to opt out of restrictions and allow requests from any application. 

Object ID (Subject) 

Usually, the entity requesting a token is described in the subject claim (sub). However, with Microsoft, we will focus on the Object ID (oid) claim. The difference is explained in the Microsoft documentation

Object ID (oid)“The immutable identifier for the requestor, which is the verified identity of the user or service principal. This ID uniquely identifies the requestor across applications. Two different applications signing in the same user receive the same value in the oid claim.” 
Subject (sub)“The principal associated with the token. For example, the user of an application.  The subject is a pairwise identifier that’s unique to a particular application ID. If a single user signs into two different applications using two different client IDs, those applications receive two different values for the subject claim.

To sum up, Object ID remains consistent across applications within a tenant, which is why it is utilized here, as we can get access tokens issued by different App Registrations. However, I will use Object ID and Subject interchangeably.  

We can configure the subjects that will be able to access the Function App: 

  • To create an allow list of applications, provide the Enterprise Application Object ID: 

  • To create an allow list of users, provide their Object ID: 

Issuer 

Configuring the Issuer (Tenant requirement) is one of the most important options: 

If we choose to use default restrictions based on issuer, we should configure the issuer here:  

Of course, it is also possible to leave the Issuer URL empty… 

Insecure Function App Single Sign-On authentication 

Now, knowing all the possible options, let’s go back to the vulnerability I found. To remind you, I was able to call a vulnerable Function App using an App Registration with the same App ID URI created in my Azure tenant. The JWT looked as follows: 

To replicate the insecure configuration of the victim’s Function App, I used the following options: 

Issuer URL (empty) 
Allowed token audiences api://insecure-function-app 
Client application requirement Allow requests from any application (Not recommended) 
Identity requirement Allow requests from any identity 
Tenant requirement Use default restrictions based on issuer 

As we can see, the Function App utilized a configuration option that is marked as “not recommended” and accepted any issuer due to the empty Issuer URL field, as well as tenant restrictions based on issuer.  

However, the Function App still accepts only access tokens issued for api://insecure-function-app, which is said to be globally unique: 

Here comes a plot twist. 

It turns out that App ID URI is indeed globally unique as long as you follow the recommended naming schemes. With an App ID URI unique across different tenants, the only outcome of the above insecure configuration would be that all App Registrations in that specific tenant would be able to request access tokens for this Function App. This is because App Registrations from other tenants would not be able to request the scope api://insecure-function-app/.default.  

How was it possible for me to create an App Registration with the same Application ID URI in my tenant? 

A globally (un)unique App ID URI 

As usually, let’s take a look at the Microsoft documentation

The Application ID URI property of the application specifies the globally unique URI used to identify the web API. It’s the prefix for scopes and in access tokens, it’s also the value of the audience claim and it must use a verified customer owned domain. For multi-tenant applications, the value must also be globally unique. If you add a GUID value, it must match either the app ID or the tenant ID. The application ID URI value must be unique for your tenant. 

The following Application ID URIs are supported: 

What I discovered during multiple penetration testing assessments is that it is common to set the App ID URI to one of the following: 

  • <string> (e.g., productsapi
  • api://<string> (e.g., api://productsapi

If you use tenant ID or app ID in your App ID URI, the value is guaranteed to be globally unique across different tenants, as it is not possible to use a GUID that is not associated with a specific App Registration. In the case of a protocol scheme (starting with https://, ftp://, etc.), the domain must be verified—it means that it is linked to your tenant and cannot be used by anybody else. 

However, if you deviate from the documentation, you actually introduce a weakness into your infrastructure.  

In the case of the first vulnerable scheme (<string>), an alert will be shown: 

However, it is still possible to use this App ID URI, but it will only work with OAuth V1 endpoints.  

The second vulnerable scheme (api://<string>), on the other hand, does not result in any alerts and can be used with both V1 and V2 OAuth endpoints: 

Using an App ID URI that is not globally unique is not a vulnerability by itself—in SecuRing, we usually report it as a recommendation. However, an insecure App ID URI along with misconfigured Function App authentication exposed our client’s API to unauthorized access.  

Rethink your Azure Single Sign-On architecture

In this article I discussed how several small misconfigurations lead to a high-risk vulnerability. This is why, as a penetration tester, I always try to chain small vulnerabilities into a bigger one—it is a fast track to getting them all fixed! 

Product documentation typically focuses on what we should do to create a working setup, rather than explaining why certain configurations should be avoided. This approach leaves a gap in understanding unless one either questions “why not?” or encounters a vulnerable configuration firsthand, as I did. 

The guidelines will often show us tried-and-true methods that have been thoroughly tested and verified. It does not mean that these are the only possible approaches. However, let’s stick to the beaten track with our corporate infrastructure instead of sailing into uncharted waters. 

Natalia Trojanowska-Korepta
Natalia Trojanowska-Korepta Senior IT Security Consultant