Blog Post

Microsoft Developer Community Blog
7 MIN READ

Easily add login to your Azure app with Bicep

Pamela_Fox's avatar
Pamela_Fox
Icon for Microsoft rankMicrosoft
Mar 10, 2025

Did you know that you can now add user login to app deployed on Azure, with just Bicep code? No Portal, CLI, SDK, or app code needed!

For those new to Bicep, it's an "infrastructure-as-code" language that can describe all the Azure resources, their connections, and role-based permissions. It's similar to Terraform, but it's Azure-specific and compiles down to ARM JSON files. We encourage developers to use infrastructure-as-code (IaC), since you can then reliably setup the same resource configuration, store your setup in version control, and even programmatically audit your IaC for security issues.

Microsoft recently announced a Graph extension that can create Graph resources, like Entra application registrations and service principals. Along with that, it's now possible for Entra applications to be secured using a managed identity as a federated identity credential ("MI as FIC"), which are simpler to manage and create than client secrets and certificates. You never have to worry about an app breaking in production due to a secret or certificate suddenly expiring.

Both Azure Container Apps and App Service offer a built-in authentication feature, and they've now extended that feature so that it can be configured with an Entra application using MI as FIC, in either the Portal, CLI, or Bicep.

👀 The Graph extension, MI-as-FIC, and built-in auth support for MI-as-FIC are all currently in "public preview", which means they are subject to change based on community feedback.

When we put all those new features, we now have a 100% Bicep solution for configuring built-in authentication! I've put together minimal templates here, which you can deploy and test for yourself:

In the rest of this post, I'll walk through the steps of adding this Bicep configuration to an existing application, for the many developers that are not starting from scratch.

Enable the Graph extension

The Graph extension requires the "extensions" functionality of Bicep, which was introduced in Bicep version 0.30.3 in September 2024. 

Add a bicepconfig.json file to your infrastructure folder with these contents:

{ 
  "experimentalFeaturesEnabled": { "extensibility": true },
  "extensions": {
    "microsoftGraphV1": "br:mcr.microsoft.com/bicep/extensions/microsoftgraph/v1.0:0.1.8-preview"
  }
}

If you do get an error about extensions not being understood, you may need to upgrade your Bicep CLI (if using it directly) or the Azure Developer CLI (if you're using "azd" instead).

Prepare for Bicep changes

Normally, when we provision resources in Bicep, we try to configure everything at once. However, for built-in auth, we need a three-step process, due to the dependencies involved:

  1. Create the backend application (either Container Apps or App Service Webapp) with an associated user-assigned managed identity
  2. Create the app registration with a reference to the backend application's managed identity
  3. Configure the backend application to use built-in auth with that app registration

1) Create the backend app

Start with your usual Bicep for creating your backend app. 

Create a user-assigned managed identity for the backend:

resource identity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
  name: 'backend-app-identity'
  location: location
}

Associate that identity with the backend. For example, for Container Apps:

resource app 'Microsoft.App/containerApps@2022-03-01' = {
  identity: {
    type: 'UserAssigned'
    userAssignedIdentities: { '${identity.id}': {} }
  }

Store the client ID of the identity as a secret on the backend.

For App Service, store the client ID in an environment variable named OVERRIDE_USE_MI_FIC_ASSERTION_CLIENTID. It should look something like this:

appSettings: {
  OVERRIDE_USE_MI_FIC_ASSERTION_CLIENTID: 
    identity.properties.clientId
}

For Container Apps, store the client ID in a secret named override-use-mi-fic-assertion-client-id. The exact Bicep depends on whether you're using the Container Apps Bicep module directly, or using a wrapper module. It should look something like this:

secrets: [
  {
    name: 'override-use-mi-fic-assertion-client-id'
    value: acaIdentity.properties.clientId
  }
]

2) Create the app registration

The next step is to create an Entra application registration, along with a federated identity credential based on a managed identity ID, and a service principal representing the Entra app. Put all of this in a appregistration.bicep file that uses the microsoftGraphV1 extension:

extension microsoftGraphV1

param issuer string
param clientAppName string
param clientAppDisplayName string
param clientAppScopes array = ['User.Read', 'offline_access', 'openid', 'profile']
param webAppEndpoint string
param webAppIdentityId string
param serviceManagementReference string = ''

param cloudEnvironment string = environment().name
param audiences object = {
  AzureCloud: {
    uri: 'api://AzureADTokenExchange'
  }
  AzureUSGovernment: {
    uri: 'api://AzureADTokenExchangeUSGov'
  }
  AzureChinaCloud: {
    uri: 'api://AzureADTokenExchangeChina'
  }
}


// Get the MS Graph Service Principal based on its application ID:
var msGraphAppId = '00000003-0000-0000-c000-000000000000'
resource msGraphSP 'Microsoft.Graph/servicePrincipals@v1.0' existing = {
  appId: msGraphAppId
}

var graphScopes = msGraphSP.oauth2PermissionScopes
resource clientApp 'Microsoft.Graph/applications@v1.0' = {
  uniqueName: clientAppName
  displayName: clientAppDisplayName
  signInAudience: 'AzureADMyOrg'
  serviceManagementReference: empty(serviceManagementReference) ? null : serviceManagementReference
  web: {
    redirectUris: [
      '${webAppEndpoint}/.auth/login/aad/callback'
    ]
    implicitGrantSettings: { enableIdTokenIssuance: true }
  }
  requiredResourceAccess: [
    {
      resourceAppId: msGraphAppId
      resourceAccess: [
        for (scope, i) in clientAppScopes: {
          id: filter(graphScopes, graphScopes => graphScopes.value == scope)[0].id
          type: 'Scope'
        }
      ]
    }
  ]

  resource clientAppFic 'federatedIdentityCredentials@v1.0' = {
    name: '${clientApp.uniqueName}/miAsFic'
    audiences: [
      audiences[cloudEnvironment].uri
    ]
    issuer: issuer
    subject: webAppIdentityId
  }
}

resource clientSp 'Microsoft.Graph/servicePrincipals@v1.0' = {
  appId: clientApp.appId
}

output clientAppId string = clientApp.appId
output clientSpId string = clientSp.id

Let's look at a few interesting lines in that module:

  • signInAudience: 'AzureADMyOrg': This restricts the sign-in to your own organization. It's not currently possible to fully set up Entra External ID in Bicep. Check out this project for External ID setup with the Graph SDK. In addition, the MI+FIC approach can only be used for workforce tenants, not CIAM tenants.
  • redirectUris: This matches the redirect URI of the built-in auth feature, ".auth/login/aad/callback". There is no need to specify a localhost redirect URI, since built-in auth only works on the deployed app.
  • implicitGrantSettings: { enableIdTokenIssuance: true }: Along with the requiredResourceAccess, this grants the Entra application the permissions needed to do a user login flow, which uses the OpenID Connect protocol (OIDC) on top of OAuth2.

With that module saved, now you can reference it from main.bicep, passing in the required parameters:

var issuer = '${environment().authentication.loginEndpoint}${tenant().tenantId}/v2.0'
module registration 'appregistration.bicep' = {
  name: 'reg'
  scope: resourceGroup
  params: {
    clientAppName: '${prefix}-entra-client-app'
    clientAppDisplayName: 'MyWebsite Entra Client App'
    issuer: issuer
    webAppEndpoint: backend.outputs.uri
    webAppIdentityId: backend.outputs.identityPrincipalId
  }
}

The issuer URL is constructed based off your environment's login endpoint and tenant ID, so that should not require changing. However, you'll need to make sure the following parameters are set correctly:

  • webAppEndpoint: The full endpoint for the deployed application, including "https" protocol.
  • webAppIdentityId: The principal ID of the managed identity associated with the deployed application.

3) Configure built-in authentication

For the third and final step, you need to configure built-in authentication for your backend application, with a reference to that Entra application registration. The Bicep for configuration is slightly different across Container Apps and App Service, but they share properties in common:

  • redirectToProvider: The value of 'azureactivedirectory' tells built-in auth to use Entra ID to handle the user login
  • unauthenticatedClientAction: The value of 'RedirectToLoginPage' tells built-in auth to direct any unauthenticated users to the login page.
  • identityProviders/azureActiveDirectory: These settings contain the reference to the Entra application registration, issuer URL, and the name of the app setting storing the managed identity client ID. For App Service, that setting must be 'OVERRIDE_USE_MI_FIC_ASSERTION_CLIENTID'. For Container apps, that setting must be 'override-use-mi-fic-assertion-client-id'.
  • tokenStore: Whether the built-in auth feature should store tokens in a persistent storage. This is only needed if your app needs to access the access tokens itself, but not needed for the login flow itself. App Service comes with its own token store, but for a Container Apps token store, you must pass in a Blob storage account.

For App Service, save this module in a file named builtinauth.bicep:

param appServiceName string
param clientId string
param issuer string
param includeTokenStore bool = false

resource appService 'Microsoft.Web/sites@2022-03-01' existing = {
  name: appServiceName
}

resource configAuth 'Microsoft.Web/sites/config@2022-03-01' = {
  parent: appService
  name: 'authsettingsV2'
  properties: {
    globalValidation: {
      requireAuthentication: true
      unauthenticatedClientAction: 'RedirectToLoginPage'
      redirectToProvider: 'azureactivedirectory'
    }
    identityProviders: {
      azureActiveDirectory: {
        enabled: true
        registration: {
          clientId: clientId
          clientSecretSettingName: 'OVERRIDE_USE_MI_FIC_ASSERTION_CLIENTID'
          openIdIssuer: issuer
        }
        validation: {
          defaultAuthorizationPolicy: {
            allowedApplications: []
          }
        }
      }
    }
    login: {
      tokenStore: {
        enabled: includeTokenStore
      }
    }
  }
}

For Container Apps, save this module in a file named builtinauth.bicep:

param containerAppName string
param clientId string
param issuer string
// Only needed if using a token store:
param includeTokenStore bool = false
param blobContainerUri string = ''
param appIdentityResourceId string = ''

resource app 'Microsoft.App/containerApps@2023-05-01' existing = {
  name: containerAppName
}

resource auth 'Microsoft.App/containerApps/authConfigs@2024-10-02-preview' = {
  parent: app
  name: 'current'
  properties: {
    platform: {
      enabled: true
    }
    globalValidation: {
      redirectToProvider: 'azureactivedirectory'
      unauthenticatedClientAction: 'RedirectToLoginPage'
    }
    identityProviders: {
      azureActiveDirectory: {
        enabled: true
        registration: {
          clientId: clientId
          clientSecretSettingName: 'override-use-mi-fic-assertion-client-id'
          openIdIssuer: issuer
        }
        validation: {
          defaultAuthorizationPolicy: {
            allowedApplications: []
          }
        }
      }
    }
    login: {
      tokenStore: {
        enabled: includeTokenStore
        azureBlobStorage: includeTokenStore
          ? {
              blobContainerUri: blobContainerUri
              managedIdentityResourceId: appIdentityResourceId
            }
          : {}
      }
    }
  }
}

With that module saved, reference it from main.bicep, passing in the required parameters:

module builtinauth 'builtinauth.bicep' = {
  name: 'builtinauth'
  scope: resourceGroup
  params: {
    containerAppName: backend.outputs.name
    clientId: registration.outputs.clientAppId
    openIdIssuer: issuer
    includeTokenStore: false
  }
}

All together now

For an example of making those changes to a project, check out this pull request where I added built-in auth to an existing Azure Container app. Or you can check out my minimal templates for built-in auth, for Container Apps or App Service.

⚠️ Keep in mind the current limitations to this approach (as of February 2025):

  • When we run the app locally, it will not have a user login flow. That should be fine if you're only using user login to restrict access to the app, but will make development more difficult if you have features that rely on the details of logged in users, like their Entra ID. For local development, you would need to use the MSAL SDK in your language of choice, and you would need to secure the Entra application registration with either a secret or certificate, since your local server would not have a managed identity to use as the credential.
  • If you are trying to use Entra External ID, you cannot yet configure everything needed using the Graph Bicep extension. You would need to set up External ID with either the Graph SDK, as we do in this project, or in the Portal.
  • The Graph extension, MI-as-FIC, and built-in auth support for MI-as-FIC are all currently in "public preview", which means they are subject to change based on community feedback.

This is a great solution if you are deploying apps for your organization and want to ensure that that only your organization user's can see them! You should never rely on "security by obscurity" - assuming that a public endpoint won't get accessed by unauthorized users. Always protect your endpoints, either with user login, private networks, or both.

To a more secure future! 🔐

Updated Mar 06, 2025
Version 1.0
No CommentsBe the first to comment