When deploying function apps in Azure, it’s common to configure a trigger endpoint to invoke the function on demand. Whether this endpoint is reachable via the Internet, or only available to internal networks, it’s sensible to authenticate requests to prevent nuisance triggering of your function app. In order to keep your app as lean as possible, you might want to lean on native integration to authenticate potential callers and Azure Function Apps do indeed offer this capability.
The Authentication feature of Function Apps supports multiple identity providers including Entra ID and GitHub, as well as third parties like Google or Facebook, and even a generic OIDC identity provider. When setting up integration between your internal services, you’ll typically stick with Entra ID but it’s not obvious how to set this up effectively particularly when facing a multi-tenant scenario where your function app is hosted in a subscription in one Azure tenant, but the calling service lives in a remote subscription in another tenant.
This scenario is entirely possible by means of multi-tenant Application Registrations, and I’ll explain how to set this up.
Prerequisites
You are going to need administrator access to 2 Azure tenants, and contributor access to a subscription in one of these tenants. We’ll set up a new function app with a demo deployment, as well as the necessary directory primitives to facilitate authentication across the two tenants. We’ll test by authenticating remotely from our workstation, but if you have access to a second subscription in the remote tenant, you can deploy a virtual machine, app service, or any other compute resource to run your test workload.
Assuming that you want to authenticate requests to the trigger endpoint for a Function App, you will also need a deployed Function App in a subscription belonging to the first tenant (later referred to as the “Corporate” tenant). If you don’t yet have this set up, I recommend following the Develop Azure Functions locally using Core Tools tutorial on Microsoft Learn. It’s fairly trivial to deploy a hello-world equivalent Python application. The function app should have at least one HTTP trigger endpoint configured so that we have something to authenticate against later on.
Getting Started
To start with, we’ll create 2 Application Registrations – one in each tenant – as well as 3 Service Principals, then publish, assign and consent to an app role in order to authorize access to the trigger endpoint we will add to a Function App later on.
I’ll refer to the tenants as Corporate
and Platform
, where the Corporate tenant houses the Primary
subscription where the function app is hosted. The tenants do not need to be in the same Azure billing account, or even belong to the same organization, and can be any two Azure tenants anywhere in the world provided they both exist in the Azure Public Cloud.
Create an app registration for the Function App in the Corporate tenant
The quickest way to work with Entra ID is using the Microsoft Graph API, but you can also work in the Azure Portal if you are not confident working directly with the API. The easiest way to work with the API is using the Microsoft Graph Explorer. You’ll need to authenticate as a user that is homed in the tenant you want to work with, as guest/B2B accounts are not supported by this tool.
First, we’ll create the app registration:
POST https://graph.microsoft.com/v1.0/applications
{
"displayName": "MyFunctionApp",
"signInAudience": "AzureADMultipleOrgs",
"identifierUris": ["api://myfunctionapp.azurewebsites.net"],
"appRoles": [
{
"allowedMemberTypes": ["Application"],
"displayName": "Admin Invoke",
"description": "Invoke the function app",
"id": "445eaa55-c365-416c-b499-854b0d9580cb",
"isEnabled": true,
"value": "Admin.Invoke"
}
]
}
Let’s break down the important fields:
displayName
is just a user-friendly name for your app registration and is what will be displayed in the Azure Portal.signInAudience
indicates this will be a multi-tenant application and allows you to create a service principal in other tenants to facilitate consent.identifierUris
is a list of URI strings to uniquely identify your application, and whilst they can be pretty much anything, it’s common convention to use your application hostname. Commonly called App ID URIs in the Azure Portal.appRoles
specifies a list of app roles that will be published by your application.allowedMemberTypes
informs the type of identity that will be able to have the role assigned.displayName
anddescription
are again user-friendly strings that will be displayed through the Entra ID user experience.id
should be a globally-unique identifier (GUID) which you should generate yourself (tryuuidgen
on MacOS/Linux orNew-Guid
on PowerShell).value
is the machine-friendly name for the role and follows the same convention as OAuth2 scopes, i.e. a dot-separated hierarchical string.
After submitting this POST request, the response should be HTTP 200/201 and contain the application manifest in the body. Take a note of the appId
field in the response, this is the newly created Client ID for our application.
After creating the app registration, we’ll want to create a client secret so the function app we will deploy can authenticate itself (d7002ed4-8f72-4973-9375-e667b667eec9
is the Client ID of the app registration):
POST https://graph.microsoft.com/v1.0/applications(appId='d7002ed4-8f72-4973-9375-e667b667eec9')/addPassword
{
"passwordCredential": {
"displayName": "Function App",
"endDateTime": "2026-05-31T01:00:00Z"
}
}
The path for this request contains the Client ID that we noted from the previous API call. In the request body, choose a future date for this client secret to expire, this can typically be up to 2 years in the future. You’ll want to include this secret in your regular secret rotation procedures to prevent a future outage when it inevitably expires!
Make a note of the client secret returned in the response, as this is auto-generated by Azure and will never be shown to you again. If you lose it, you’ll need to delete it and create a new one.
Now we’ll create a service principal in the same tenant as the app registration, since Microsoft Graph does not automatically create this for us unlike Azure Portal (this is a good thing), noting again to use the Client ID in the path:
POST https://graph.microsoft.com/v1.0/servicePrincipals
{
"appId": "d7002ed4-8f72-4973-9375-e667b667eec9"
}
Make a note of the id
property in the response which indicates the Object ID of the newly created service principal. We will need this later when authorizing the calling application. That’s it for the initial configuration in the Corporate tenant. Now we’ll set up a remote application which we can later authenticate.
Create an app registration for the caller in the Platform tenant
Next up, we’ll create an app registration for our caller, i.e. the remote application that will be authorized to invoke our function app via its trigger endpoint.
POST https://graph.microsoft.com/v1.0/applications
{
"displayName": "ExampleCaller",
"signInAudience": "AzureADMultipleOrgs",
"requiredResourceAccess": [
{
"resourceAppId": "d7002ed4-8f72-4973-9375-e667b667eec9",
"resourceAccess": {
"id": "445eaa55-c365-416c-b499-854b0d9580cb",
"type": "Role"
}
}
]
}
In this request, we are specifying requiredResourceAccess
which will assign the app role from the application in the Corporate tenant to our caller’s application registration. Substitute the Client ID and App Role ID from the first application here.
Make sure to copy the appId
from the response as this is the Client ID we will need to reference this app registration (here this is 0860c97a-e1b3-4fe8-a771-83975bfb2c6e
). As with the first app registration, we’ll create a client secret for the caller to authenticate with:
POST https://graph.microsoft.com/v1.0/applications(appId='0860c97a-e1b3-4fe8-a771-83975bfb2c6e')/addPassword
{
"passwordCredential": {
"displayName": "CallerApplication",
"endDateTime": "2026-05-31T01:00:00Z"
}
}
As before, make a note of the client secret returned in the response as this is the only time you will ever see it.
Finally, we’ll create a service principal in the same (Platform) tenant. Whilst this isn’t strictly necessary for this setup, it is convention to create a service principal along with a new app registration and the Azure Portal and other tooling will automatically do this, so we’ll do the same. You can omit this step, but you may find that you’ll later need it if you want to authorize the application for any resources in the same tenant, and some same-tenant operations will not work without an accompanying service principal.
POST https://graph.microsoft.com/v1.0/servicePrincipals
{ "appId": "0860c97a-e1b3-4fe8-a771-83975bfb2c6e" }
Set up cross-tenant authorization for the caller
Now that we’ve created application registrations for both the function app and the calling application, we can proceed to authorize the caller. We already assigned the app role we published from the function app to the calling application, however this only amounts to a request to assign the role. In order for the role to take effect, we’ll need to ‘grant admin consent’. You may have encountered this in the Azure Portal when assigning roles and/or scopes to applications, and the mechanism that effects this consent for app roles is called an App Role Assignment.
For delegated scopes, the equivalent mechanism is an OAuth2 Permission Grant – though we will not be using this as our scenario only requires application-to-application authorization and does not involve users.
In the Corporate tenant (where the app registration for our function app resides), we’ll first create a service principal linked to the calling app registration in the Platform tenant, and then we’ll create an App Role Assignment to grant consent for the app role.
If you’re using the Microsoft Graph Explorer to send this API request, ensure that you are authenticated to the correct (Corporate) tenant, remembering that your user account must belong in the tenant you want to interact with since the Graph Explorer does not support B2B/guest accounts at this time.
POST https://graph.microsoft.com/v1.0/servicePrincipals
{ "appId": "0860c97a-e1b3-4fe8-a771-83975bfb2c6e"}
You should get an HTTP 200/201 response indicating the service principal was successfully created for the app registration from the remote tenant. Make a note of the id
field in the response – this is the object ID of the service principal and will be needed to create the app role assignment:
POST https://graph.microsoft.com/v1.0/servicePrincipals/9adba14b-7b51-4ba3-b36e-61355db07614/appRoleAssignments
{
"principalId": "9adba14b-7b51-4ba3-b36e-61355db07614",
"resourceId": "d96a8714-75fb-476d-b100-29516dc8cffe",
"appRoleId": "445eaa55-c365-416c-b499-854b0d9580cb"
}
Let’s break down this request which entirely comprises GUIDs, to make sense of it:
principalId
[9adba14b-7b51-4ba3-b36e-61355db07614
] is the Object ID of the service principal we created (in the Corporate tenant) for the calling application (in the Platform tenant) and also forms part of the path in the API URL.resourceId
[d96a8714-75fb-476d-b100-29516dc8cffe
] is the Object ID of the service principal for the Function App application registration, in the Corporate tenant.appRoleId
[445eaa55-c365-416c-b499-854b0d9580cb
] is our self-generated app role ID when publishing theAdmin.Invoke
app role in the Function App application registration.
This forms the final piece needed to authorize the remote calling application to trigger the Function App we will create. Creating App Role Assignments requires elevated permissions in Microsoft Entra, equivalent to Application Administrator or Global Administrator when performed by users, or the AppRoleAssignment.ReadWrite.All
app role for MIcrosoft Graph when performed by an application.
Testing our authentication setup
Before continuing further, it makes sense to test our setup so far, so that we can be sure we’ve done everything correctly before attempting to integrate a Function App. We’ll send an authentication request as the calling application to the Microsoft Identity login service, in order to request an access token for the Function App:
POST https://login.microsoftonline.com/acmecorp.onmicrosoft.com/oauth2/v2.0/token HTTP/1.1
Content-Type: application/x-www-form-urlencoded
client_id=0860c97a-e1b3-4fe8-a771-83975bfb2c6e&client_secret=MySup3rS3cr3t&tenant=acmecorp.onmicrosoft.com&grant_type=client_credentials&scope=api://myfunctionapp.azurewebsites.net
It’s trivial to make this request using cURL:
curl -i -X POST -H "Content-Type: application/x-www-form-urlencoded" -d 'client_id=0860c97a-e1b3-4fe8-a771-83975bfb2c6e' -d 'client_secret=MySup3rS3cr3t' -d 'tenant=acmecorp.onmicrosoft.com' -d 'grant_type=client_credentials' -d 'scope=api://myfunctionapp.azurewebsites.net/.default' https://login.microsoftonline.com/acmecorp.onmicrosoft.com/oauth2/v2.0/token
Looking at the parameters you’ll need to use in this authentication request:
client_id
[0860c97a-e1b3-4fe8-a771-83975bfb2c6e
] is the Client ID of the calling application registration in the Platform tenant.client_secret
[MySup3rS3cr3t
] is the client secret you created for that application registration.tenant
[acmecorp.onmicrosoft.com
] is either the domain name or Tenant ID for the Corporate tenant.scope
[api://myfunctionapp.azurewebsites.net/.default
] is the fully qualified default scope for the Function App application registration. It’s comprised of anidentifierUri
(App ID URI) which should be globally unique, and the special scope.default
.- Don’t forget to specify the correct domain name or Tenant ID for the Corporate tenant in the path of the URL.
If all went well, you should receive an HTTP 200 response in JSON format that looks something like this:
{
"token_type": "Bearer",
"expires_in": 3599,
"ext_expires_in": 3599,
"access_token": "eyJ0eXAiOiJKV1QiLCJhbTQ5MDc........F6Bw"
}
You’ll see the access_token
is a base64 encoded JWT in the familiar Header.Payload.Signature
format. You can visit JWT.ms and paste this into the textarea at the top of the page to see a breakdown of the token with helpful explanations for each claim:

Most notably you should be able to correlate the appid
claim with the Client ID of your calling application, the oid
claim with the Object ID of the calling application’s service principal in the Corporate tenant, the aud
claim with the App ID URI of your Function App application, and finally the roles
claim which should include the Admin.Invoke
application role that you published, assigned, and then granted consent.
Configuring the Function App trigger endpoint
The hard work is done, and we’re ready to configure our function app to authenticate requests to its HTTP trigger endpoint(s)!
We’re going to do this using the Azure Portal, primarily because the App Service APIs are not fun to work with. It is left as an exercise to the reader (you, or perhaps your long-suffering Ops specialist) to work out how to express this configuration in your Infrastructure-As-Code tool of choice.
Sign in to the Azure Portal, and navigate to your Function App. Before proceeding to configure authentication for our Function App, let’s first test its HTTP trigger endpoint to ensure it is working as expected. In the Overview blade, click on the trigger you wish to use and click on the “Get Function URL” button. You should see the URL to use in order to hit the trigger endpoint – construct a cURL request and ensure that you get an HTTP 200 response before proceeding:
curl -i https://myfunctionapp.azurewebsites.net/api/MyHttpTrigger
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Date: Tue, 03 Jun 2025 10:44:59 GMT
Server: Kestrel
Transfer-Encoding: chunked
Request-Context: appId=cid-v1:fa19d010-565e-4aaa-8eb7-1c14ecaf6675
This HTTP triggered function executed successfully.
Now we’ll go ahead and enable authentication, and configure authorization as needed to ensure that only our caller application will be able to hit this endpoint going forward. Navigate back to the Function App, then open the Authentication blade from the service menu and you’ll see this:

Click the “Add identity provider” button and choose “Microsoft” from the list of available providers. The form will expand; complete it as follows.
- App Registration Type: Provide the details of an existing app registration.
- Application (client) ID: Paste in the Client ID of the app registration we created for the function app (the first one).
- Client secret: Paste in the client secret we created for that app registration.
- Issuer URL: should be prepopulated but ensure this is set to
https://sts.windows.net/12345678-1234-1234-1234-123456789012/v2.0
(where the GUID is the Tenant ID of the Corporate tenant) - Allowed Token Audiences: Paste the App ID URI for the Function App application registration, i.e.
api://myfunctionapp.azurewebsites.net
. - Client application requirement: Allow requests from specific client applications, and in the box that appears, paste the Client ID for the calling application registration.
- Identity requirement: Allow requests from specific identities, and in the box that appears, paste the Object ID of the service principal in the Corporate tenant created for the calling application.
- Tenant requirement: Allow requests only from the issuer tenant
There should be an additional section at the end of this form which allows you to conveniently configure the authentication settings for your Function App. Use the following settings:
- Restrict Access: Require authentication
- Unauthenticated requests: Return HTTP 401 Unauthorized
This should look something like this (noting that the specific values in this screenshot are for a different application but using the same scenario):

The Big Reveal
Now that we’ve enabled authentication, we should no longer be able to trigger our Function App with unauthenticated requests to its trigger endpoint:
curl -i https://myfunctionapp.azurewebsites.net/api/MyHttpTrigger
HTTP/1.1 401 Unauthorized
Content-Length: 0
Date: Wed, 04 Jun 2025 00:50:19 GMT
Server: Kestrel
WWW-Authenticate: Bearer realm="myfunctionapp.azurewebsites.net"
Strict-Transport-Security: max-age=31536000; includeSubDomains
Our trigger endpoint is now protected, but can we authorize our request? Let’s obtain another access token, since the one we obtained earlier may have expired already:
curl -i -X POST -H "Content-Type: application/x-www-form-urlencoded" -d 'client_id=0860c97a-e1b3-4fe8-a771-83975bfb2c6e' -d 'client_secret=MySup3rS3cr3t' -d 'tenant=acmecorp.onmicrosoft.com' -d 'grant_type=client_credentials' -d 'scope=api://myfunctionapp.azurewebsites.net/.default' https://login.microsoftonline.com/acmecorp.onmicrosoft.com/oauth2/v2.0/token
Copy the value of access_token
from the response and build an authenticate request using cURL:
curl -i -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGci........XA2Tw' https://myfunctionapp.azurewebsites.net/api/MyHttpTrigger
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Date: Wed, 04 Jun 2025 00:54:43 GMT
Server: Kestrel
Transfer-Encoding: chunked
Strict-Transport-Security: max-age=31536000; includeSubDomains
x-ms-middleware-request-id: bcdfd6d1-050c-4b30-a5a6-7b423265930c
This HTTP triggered function executed successfully.
If you get an HTTP 200 response, then you have successfully configured cross-tenant authentication to enable a remote application to invoke your Function App via its HTTP trigger endpoint!
If there’s a mismatch somewhere in your configuration, you’ll likely receive an HTTP 401 response with a JSON body explaining the reason for the authorization failure, for example:
{
"code":401,
"message":"IDX10214: Audience validation failed. Audiences: 'api:\/\/myfunctionapp.azurewebsites.net'. Did not match: validationParameters.ValidAudience: 'ed337979-f9cd-4d0d-a909-5c9b0a6ffc9b' or validationParameters.ValidAudiences: 'null'."
}
In this example error, the aud
claim in the access token did not match the configuration in the Function App. Perhaps this field was missed or the wrong value was used.
Further Improvements
We’ve successfully implemented a simple authorization mechanism for a remote application from another Entra tenant to invoke our Function App. This works fine for internal scenarios where only one caller, or even a handful of potential callers, might be involved. But if we want to expand this to hundreds of remote identities or more, this isn’t going to scale well.
Astute readers may have noticed that we assigned and consented an app role, but didn’t seem to make use of it? And that is entirely correct – we haven’t. Unfortunately, the integration offered by App Service is not robust enough to be able to make use of the app role. So why did we configure it?
The short answer is that it’s good practise. The longer answer is that it’s extremely useful to grant consent in this way, because it helps the casual observer (which might include your future self) to see what a given application might be accessing. And it lays the groundwork for you to improve on this authorization setup and actually make use of the app role in the access token to authorize requests to your Function App. This improvement is unfortunately left to you by Azure, and you will need to consume the Authorization header within your app and inspect the roles
claim yourself.
References
- Microsoft Learn: App Service Authentication using Microsoft Entra
- Microsoft Learn: Develop Azure Functions locally using Core Tools
- Microsoft Learn: Microsoft identity platform and the OAuth 2.0 client credentials flow
- Microsoft Learn: Access token claims reference
- Microsoft Graph: Applications
- Microsoft Graph: App Role Assignments