Azure Single Sign-On Case Study #3: Secure authentication for Function Apps

General guidance on Azure Function App authentication setup for different client types.

Natalia Trojanowska-Korepta 2024.09.04   –   8 MIN read

While researching broken access control in certain Azure Function App configurations for my last case study, I gathered some insights into best practices regarding secure authentication, which I would like to share with you.  

My goal was to create a secure setup featuring a Function App. Now I would like to guide you through my configuration step by step, showing which options I picked and why. 

I will also: 

  • Review App Keys authorization levels, 
  • Guide you on securely setting up Function App authentication, 
  • Discuss how to set up an Azure Function App with multiple clients, 
  • Explain how to enhance the security of a Function App’s default App Registration. 

Connect with the author on LinkedIn!

Azure Function App authentication with keys 

When you first create a Function App with an HTTP Trigger, you can choose one of the authorization levels: Function, Anonymous, or Admin. Depending on your choice, you will be able to use different App Keys to access your function: 

  • Function Key (Function App > HTTP Trigger > Function Keys
  • Host Key (Function App > App keys
  • Master Host Key (Function App > App keys

You can find a quick breakdown of possibilities in the table below: 

Function authorization level Anonymous authorization level Admin authorization level Runtime APIs
Anonymous No access Has access No accessNo access
Function Key Has access Has access No accessNo access
Host Key Has access Has access No accessNo access
Master Host Key Has access Has access Has access Has access 

The Master Host Key also grants access to Azure Functions Runtime APIs, which is why you should never use it in client applications to access a function. When I was writing this article, I got curious what would I be able to do if I found a master key. It turns out you can access and even overwrite the Function App’s source code. It is possible to list the contents of the web root directory using the following URL: 

https://{appName}.azurewebsites.net/admin/vfs/site/wwwroot/?code={masterHostKey} 

Azure allows you to create multiple Host Keys and Function Keys. However, it is important to note that when you are creating a new key, its value is not automatically generated—you must provide the random value yourself. Always supply random keys with sufficient complexity, so nobody will be able to guess them! 

According to Microsoft documentation, app keys are intended for use only during development. A production instance, on the other hand, should use Single Sign-On—which I think is beyond doubt, as the differences between properties of API keys and OAuth tokens have been widely discussed. 

Target architecture for an Azure Function App 

Our goal is to create a secure and functional architecture featuring an Azure Function App with an HTTP-triggered function that will serve as an API. The API will be called by Entra ID users via a Single Page Application and by a separate backend service.  

Now we will move on to the configuration process in Azure. In our case, a Function App operates like an API, which is why we will give it its own App Registration. We will also use separate App Registration instances for each client. In theory, it is also possible to use just one App Registration for both the clients and the Function App, but it comes at a cost of separation and accountability. 

As a result, we have three App Registration instances: one representing the Function App and two for the clients.  

Secure Azure configuration for Function Apps authentication 

Azure Function Apps allow you to configure Single Sign-On with multiple providers: 

In my previous article, I explained the different settings for a Microsoft SSO integration, as it is the most comprehensive. This time, however, we will focus on an architecture featuring two clients—an SPA and a backend service—and their differences. 

When we start configuring Microsoft authentication, we are asked to set up an App Registration that will link the Identity Provider with the application.  

My approach is to always create (or select) an App Registration that will be dedicated to representing a specific API (in this case, the Function App). For the subsequent options, we will choose: 

  • Allow requests from specific client applications 
  • Allow requests from any identity 
  • Allow requests only from the issuer tenant 
  • Require authentication 
  • HTTP 401 Unauthorized: recommended for APIs 

The allowed audience is automatically set in the Function App SSO configuration when the default App Registration is created. While you can modify it to be more user-friendly, you should stick to the Application ID URI patterns mentioned in the configuration.  

Next, we need to create an allow list of the clients: 

With this architecture, the JWT payload will be meaningful and your security logs will be perfectly readable. A valid JWT that will be accepted by the API will include the following claims: 

Claim Description Contents 
aud Audience App ID URI of the App Registration that represents the Function App. 
appid App ID App ID of the App Registration that represents the client (e.g., SPA or the backend service). 
iss Issuer URL address of the Security Token Service that issued the token, which includes the ID of the issuer tenant. 
oid Object ID Object ID of the Service Principal (in case of the backend service) or Object ID of the user (in case of the SPA). 

Using different OAuth grant types to call APIs 

Let’s assume that the Application ID URI of the App Registration associated with the Function App is https://api.securing.local. It is also the audience expected by our Function App. I will now guide you through the configuration of App Registrations for different clients.  

Backend service 

Create a separate App Registration instance for the backend service.

To call the API, we will use the client credentials grant type. It requires several parameters: 

  • OAuth 2.0 token endpoint (v2): Available at App Registration > Endpoints 
  • Client ID: Available at App Registration > Overview 
  • Client secret: Can be created at App Registration > Certificates & secrets 
  • Scope: https://api.securing.local/.default – a default scope provided by the App Registration linked to the Function App 

You can now request a JWT using the following HTTP request: 

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=https://api.securing.local/.default 

Single Page Application 

Create a separate App Registration instance for the SPA. 

To call the API, we will use the authorization code grant type with PKCE extension. To do so, we first need to allow the SPA’s App Registration to request the Function App’s user_impersonation scope. We can do it in two different ways: 

  • In the SPA’s App Registration (API permissions): 

  • In the Function App’s App Registration (Expose an API): 

We will also need the OAuth 2.0 token endpoint (v2) and the client ID. A client secret is not required, as the SPA is a public client and doesn’t have a secure place to store it. 

It is possible to start the authentication flow using the following URL: 

https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/authorize?client_id={clientId}&response_type=code&scope=https://api.securing.local/.default&nonce={nonce} 

App Roles 

If you want to set different permissions for users that will be included in the JWT and used in the Function App API, you can use App Roles. App Roles can also be used for backend services, where they serve as application permissions (as opposed to delegated permissions). 

Hardening Function App’s default App Registration 

If you chose to create a new App Registration instance during setup, you could additionally harden it by following these steps: 

  • Disable implicit grant type 

The implicit grant type will be deprecated in OAuth 2.1, and is also discouraged by Microsoft. It is less secure than other OAuth grant types since the access token is exposed in the URL. Additionally, the App Registration for the Function App shouldn’t handle any user sign-ins directly. 

  • Remove the existing redirect URI 

Since the App Registration instance, we created for our Function App represents an API, it doesn’t need a redirect URI. Sign-ins are handled by different App Registration instances that represent the clients (in our case, the SPA or the backend service). Without a redirect URI, users will not be able to log in with the authorization code flow using this App Registration directly. 

An alternative to creating a new App Registration during SSO setup is linking the Function App authentication to an existing App Registration—the only configuration it needs is to add a client secret, Application ID URI (you can use the default value), and the user_impersonation scope if you plan to use it with an SPA. 

Connect with the author on LinkedIn!

Rethink your Azure Single Sign-On architecture 

In my attempt to construct a seemingly straightforward Single Sign-On architecture that incorporates a Function App, I have realized that the process is not as simple as it initially appears. Azure, by default, generates what I perceive to be a sample configuration, a sort of blueprint that helps us quickly test Function App authentication, but is not suitable for production environments. 

Further analysis reveals that an efficient architecture, which allows us to pinpoint the origin and destination of API traffic, requires multiple App Registrations. While shortcuts are possible, they often lead to a loss of accountability, a lack of clarity in logs, and in many instances, granting excessive permissions. 

These issues typically surface as our architecture evolves and expands. What was once a small setup of an API and a client, gradually transforms into a large network of actors, each with its own set of dependencies. 

Consider a scenario where the backend service now needs access to another API. How would the architecture adapt to this new requirement? Would it compromise security, or is it just a matter of a few CLI commands? Design with growth in mind. 

References 

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