Overview
The recent work that I have been doing with Function Apps and linking them as backends to Azure API Management has relied on the use of the Function Apps Function SAS key for security. This is a valid authentication approach, but there are risks that you need to be aware of as well as best practices that you need to be abiding by. Such as:
-
Some Potential Risks:
- If a SAS is leaked, it can be used by anyone who obtains it to call your Function.
- If a SAS expires and the API Management API has not been updated to make use of the updated SAS, then the integration functionality will be hindered.
-
Some SAS Best Practices to mitigate risks:
- Always use HTTPS - If a SAS is passed over HTTP and intercepted, an attacker can perform a man-in-the-middle attack and read the SAS.
- Have a revocation plan - Make sure that you are prepared to respond if a SAS is compromised.
- Have a rotation plan - Look to replace the SAS with new ones at regular intervals and include shorter time intervals for the expiration period.
But what if you require further governance whereby the Function App requires a valid Entra ID bearer token in order to be invoked. To implement this we are going to make use of Easy Auth.
Easy Auth
Easy Auth is a built-in authentication and authorisation capability provided by Azure App Services, Azure Functions, and Standard Logic Apps. Easy Auth makes use of federated identity whereby a third-party identity provider manages the user identities and authentication flow for you.
Easy Auth is a platform feature running on the same virtual machine as your application. Once enabled, any incoming HTTP requests will pass through this feature prior to being handled by your application. Easy Auth runs separately from your application code and can be configured using ARM settings or using a configuration file.
Using Easy Auth and Linking to API Management
As mentioned above, I would like to use Easy Auth to protect my HTTP triggered functions, but more importantly, I would like Azure API Management to be the only identity that can be used to make requests to my functions as shown in the diagram below:
Note:
As we will be invoking the Function App HTTP triggered Functions with Easy Auth, we no longer need to specify the following parameters or details:
- Parameter code: Shared-Access-Signature
- AuthorizationLevel: When Easy Auth is enabled, the HTTP triggered Functions no longer need to have the
AuthorizationLevel
set to Function but rather set to Anonymous. This is because Easy Auth will conduct the authorisation prior to reaching your code and no longer requires a SAS key to be provided of which the Function AuthorisationLevel requires.
For setup, we will need to conduct the following steps:
I have opted for Infrastructure as Code (IaC) as my method of implementation, specifically Bicep. I have also opted for Microsoft Entra as my Identity Provider.
-
Configure the Function App to Use Microsoft Entra sign-in.
Working through the Microsoft instructions in the link above, you will require as minimum the following when setting up the Function App Application Registration:
-
Select the supported account type. (I’m using Current tenant - single tenant)
-
Setup a Redirect URI for your Function:
Select Web for platform and set the URI to
<app-url>/.auth/login/aad/callback
. For example, https://contoso.azurewebsites.net/.auth/login/aad/callback. -
Make not of the Application (client) ID.
-
Create a Client Secret and store this securely (I’m using Azure DevOps Secure Library Variables as part of a deployment).
This is a secret value that the application uses to prove its identity when requesting a token. This value is saved in your app’s configuration as a slot-sticky application setting named
MICROSOFT_PROVIDER_AUTHENTICATION_SECRET
. If the client secret isn’t set, sign-in operations from the service use the OAuth 2.0 implicit grant flow, which isn’t recommended. -
Expose an API > Add > Save. This value uniquely identifies the application when it’s used as a resource, allowing tokens to be requested that grant access. It’s used as a prefix for scopes you create.
I am using a single-tenant app and therefore using the default value. Appears as such
api://<application-client-id>
-
Add the
user_impersonation
scope to your App Registration allowingAdmins and users
to consent.
-
-
Make sure that API Management has been setup with Managed Identity. I am using System Assigned.
@description('Deployment of the APIM instance') resource apimInstanceDeploy 'Microsoft.ApiManagement/service@2024-05-01' = { ... identity: { type: 'SystemAssigned' } ... }
-
Store the App Registration Secret in KeyVault and Reference in your Function App Settings to allow the Function App to prove its identity.
... @secure() @description('The client secret for the Easy Auth App Registration') param applicationEasyAuthClientSecret string ... @description('Role Definition Id for the Key Vault Secrets User role') var keyVaultSecretsUserRoleDefId = '4633458b-17de-408a-b874-0445c86b69e6' ... @description('Deploy the Application Easy Auth App Registration Secret to Keyvault') resource vaultFunctionAppRegSecret 'Microsoft.KeyVault/vaults/secrets@2023-07-01' = { name: '${applicationFunctionAppName}-EasyAuth-Secret' parent: applicationKeyVaultDeploy properties: { contentType: 'string' value: applicationEasyAuthClientSecret } } ... @description('Deploy the Application Function App') resource applicationFunctionAppDeploy 'Microsoft.Web/sites@2024-04-01' = { name: applicationFunctionAppName location: location identity: { type: 'SystemAssigned' } ... resource config 'config@2024-04-01' = { name: 'appsettings' properties: { ... MICROSOFT_PROVIDER_AUTHENTICATION_SECRET: '@Microsoft.KeyVault(VaultName=${applicationKeyVaultDeploy.name};SecretName=${vaultFunctionAppRegSecret.name})' ... } } } ... @description('Create the RBAC for the Function App to Read the Secret from Key Vault') resource applicationFunctionAppRBACWithKV 'Microsoft.Authorization/roleAssignments@2022-04-01' = { name: guid(applicationKeyVaultDeploy.id, applicationFunctionAppDeploy.id, keyVaultSecretsUserRoleDefId) scope: vaultFunctionAppRegSecret properties: { principalId: applicationFunctionAppDeploy.identity.principalId roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', keyVaultSecretsUserRoleDefId) principalType: 'ServicePrincipal' } }
-
Enable Easy Auth on the Function App using ARM/Bicep Template AuthSettingsV2.
@description('Setup the Easy Auth config settings for the Function App') resource applicationAuthSettings 'Microsoft.Web/sites/config@2024-04-01' = { name: 'authsettingsV2' parent: applicationFunctionAppDeploy properties: { globalValidation: { requireAuthentication: true unauthenticatedClientAction: 'Return401' } httpSettings: { requireHttps: true routes: { apiPrefix: '/.auth' } forwardProxy: { convention: 'NoProxy' } } identityProviders: { azureActiveDirectory: { enabled: true registration: { openIdIssuer: uri('https://sts.windows.net/', tenant().tenantId) clientId: functionAppEasyAuthClientId clientSecretSettingName: 'MICROSOFT_PROVIDER_AUTHENTICATION_SECRET' } validation: { allowedAudiences: environment().authentication.audiences defaultAuthorizationPolicy: { allowedPrincipals: { identities: [ apimInstance.identity.principalId ] } } } } } platform: { enabled: true runtimeVersion: '~1' } } }
Note:
EasyAuth is managed by the AppService, and for an incoming request, it is a hop that comes before FA Runtime. When EasyAuth is enabled for a Function App, all incoming requests are validated against the policies in your V2 Auth settings.
-
Configure API Management to obtain a valid Bearer token and add it to the request Authorization Header. Implemented through APIM Policy.
<!-- API ALL OPERATIONS SCOPE --> <policies> <inbound> <base /> <!-- Uses System Assigned Managed Identity of the APIM Instance --> <authentication-managed-identity resource="https://management.azure.com/" output-token-variable-name="msi-access-token" ignore-error="false" /> <set-header name="Authorization" exists-action="override"> <value>@("Bearer " + (string)context.Variables["msi-access-token"])</value> </set-header> </inbound> <backend> <base /> </backend> <outbound> <base /> </outbound> <on-error> <base /> </on-error> </policies>
Summary
In short, we have applied further governance on our Function App and API Management through the use of Easy Auth. To see my worked example, have a look at my GitHub repository along with a README that explains how to get started.
Hope this helps and have fun.