Recently I did a prototype application for the team and wanted to share access to it but configuring easy auth wasnt quite as easy as I had hoped and I felt the guidance online was a bit painful to decompose for what I was trying to do hence this blog post.

Requirements

  • Build a .net web app
  • Host it on Azure App Service
  • Restrict access to the web app to only people within the tenant
  • Restrict access to the web app to only people within a specific group in the tenant
  • As this is an internal helper app it doesnt need to be overly complicated or pretty but it needs to be functional and secure

Video

Also here is a video if you find that easier.

Code Snippets

There are also additional code snippets for this video/blog in this github folder.

https://github.com/michaelstephensonuk/CodeSnippets/tree/main/WebApp-Easy-Auth

Steps

1. Setup App Reg & Enterprise App

At the bottom of the page there is a script which you can use to setup the app reg and enterprise app. First login to Azure CLI with the scope on graph api.

az login --tenant 'TBC' --use-device-code --scope https://graph.microsoft.com//.default

Run the below script to setup the app

.\Create-AppRegistration.ps1 `
  -AppName "[The main bit of the enterprise app and app reg name]" `
  -EnvironmentName "[The environment suffix on your app reg and enterprise app]" `
  -RedirectUrl "https://[Your web app url here].azurewebsites.net/.auth/login/aad/callback" `
  -GroupObjectIds @(
    "[Add the object id for any groups you want to assign here]"
  )

You will now have both an enterprise app created and an app reg. The enterprise app will have a group associated with it that you can then add users into who you want to give access to your app.

2. BiCep to Create Web App


// Web App
resource webApp 'Microsoft.Web/sites@2023-01-01' = {
  name: webAppName
  location: location
  properties: {
    serverFarmId: appServicePlan.id
    httpsOnly: true
    siteConfig: {
      netFrameworkVersion: 'v9.0'
      alwaysOn: true
      ftpsState: 'Disabled'
      minTlsVersion: '1.2'
      use32BitWorkerProcess: false
      appSettings: [

        {
          name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
          value: appInsights.properties.ConnectionString
        }
        {
          name: 'ApplicationInsightsAgent_EXTENSION_VERSION'
          value: '~3'
        }                      
        {
          name: 'MICROSOFT_PROVIDER_AUTHENTICATION_SECRET'          
          value: authClientSecret
        }
        {
          name: 'WEBSITE_AUTH_AAD_ALLOWED_TENANTS'
          value: allowedTenantIds != [] ? join(allowedTenantIds, ',') : ''
        }        
        {
          name: 'XDT_MicrosoftApplicationInsights_Mode'
          value: 'default'
        }
        
      ]
      connectionStrings: [
        
      ]
    }
  }
}

There are 2 key points in the bicep for the Web App Setup:

  • MICROSOFT_PROVIDER_AUTHENTICATION_SECRET

When you run the bicep script, you want to supply a client secret for the Easy Auth setup and this gets stored in this App Setting so we can set this in advance.

  • WEBSITE_AUTH_AAD_ALLOWED_TENANTS

When you setup Easy Auth this setting holds the allowed Entra tenant ids which are allowed to access the application based on the way we will configure it.

3. authsettingsV2 for the Web App

Next we will apply the Easy Auth configuration using the below Bicep.

resource webAppAuth 'Microsoft.Web/sites/config@2023-01-01' = {
  parent: webApp
  name: 'authsettingsV2'
  properties: {
    globalValidation: {
      requireAuthentication: true
      unauthenticatedClientAction: 'RedirectToLoginPage'
    }
    identityProviders: {
      azureActiveDirectory: {
        enabled: true
        registration: {
          // Use specific tenant instead of /common/ to restrict to your tenant only
          openIdIssuer: 'https://login.microsoftonline.com/${allowedTenantIds[0]}/v2.0'
          clientId: authClientId
          clientSecretSettingName: 'MICROSOFT_PROVIDER_AUTHENTICATION_SECRET'
        }
        validation: {          
          allowedAudiences: [
            'api://${authClientId}'
            authClientId  // Also allow the raw client ID as audience
          ]          
          defaultAuthorizationPolicy: { 
            allowedPrincipals: {
              groups: allowedGroups      
            }
            // Restrict to only this application's client ID
            allowedApplications: [
              authClientId
            ]                               
          }
        }
        isAutoProvisioned: false
      }
    }
    login: {
      tokenStore: {
        enabled: true
      }
    }
  }
}

The key points here are:

  • We restricted the app to take authentication from a single tenant
  • We added a restriction to only allow auth via our App Reg, this is the allowedApplications
  • We then restricted the app to only allow auth for users in specific groups under the allowedPrincipals section

4. Testing The App

Once you have deployed your Azure infrastructure you should be able to add a user to the group and then browse to the url for the web app. You will be redirected to Entra from where you will authenticate and be returned successfully to the app and you should see the default App Service home page. This is a success.

For a failed test, if you login with a user who is not a member of the group, depending on how you configure things you may get one of the following errors:

  • The App Service will display that you do not have access to the app
  • Entra will indicate you do not have access to this app

5. Deploy your code

You can now do a standard deployment to the Azure Web App of your code. You didnt need any special authentication locally in your code on your dev machine to build the app.

Once your code is deployed the authentication/authorization is handled at the Azure Resource and Entra level and before your code is accessed.

Now its Easy Auth now I know how to do this repeatably.

Scripts / Larger Code Snippets

Entra App Reg / Enterprise App Script

The script below is used to setup an app reg and enterprise app, it is called in the example earlier in this doc.

Create-AppRegistration.ps1

param(
  [Parameter(Mandatory=$true)]
  [string]$AppName,
  
  [Parameter(Mandatory=$true)]
  [string]$EnvironmentName,
  
  [Parameter(Mandatory=$true)]
  [string]$RedirectUrl,
  
  [Parameter(Mandatory=$false)]
  [string[]]$GroupObjectIds,
  
  [Parameter(Mandatory=$false)]
  [int]$SecretExpiryYears = 2
)

$displayName = "$AppName-$EnvironmentName"

Write-Host ""
Write-Host "Creating App Registration: $displayName" -ForegroundColor Cyan

# 1. Create App Registration
az ad app create --display-name $displayName --sign-in-audience "AzureADMyOrg" --web-redirect-uris $RedirectUrl --enable-id-token-issuance true --enable-access-token-issuance true --output none

# 2. Get the App ID and Object ID
$appId = az ad app list --display-name $displayName --query "[0].appId" -o tsv
$appObjectId = az ad app list --display-name $displayName --query "[0].id" -o tsv

if (-not $appId) {
  Write-Host "Error: Failed to create App Registration" -ForegroundColor Red
  exit 1
}

Write-Host "App Registration created with ID: $appId" -ForegroundColor Cyan

# 3. Add Group Claims to Token Configuration
Write-Host "Adding Group Claims (Security Groups)..." -ForegroundColor Cyan
az ad app update --id $appId --set groupMembershipClaims=SecurityGroup

# 4. Add Optional Claims for groups in ID and Access tokens
Write-Host "Adding Optional Claims for groups..." -ForegroundColor Cyan

$optionalClaimsBody = @{
  optionalClaims = @{
    idToken = @(
      @{
        name = "groups"
        source = $null
        essential = $false
        additionalProperties = @()
      }
    )
    accessToken = @(
      @{
        name = "groups"
        source = $null
        essential = $false
        additionalProperties = @()
      }
    )
    saml2Token = @()
  }
} | ConvertTo-Json -Depth 10

$tempFile = [System.IO.Path]::GetTempFileName()
Set-Content -Path $tempFile -Value $optionalClaimsBody -Encoding UTF8

$uri = "https://graph.microsoft.com/v1.0/applications/$appObjectId"
az rest --method PATCH --uri $uri --headers "Content-Type=application/json" --body "@$tempFile" --output none

Remove-Item -Path $tempFile -Force

Write-Host "Optional Claims added successfully" -ForegroundColor Green

# 5. Create Enterprise App (Service Principal)
Write-Host "Creating Enterprise App (Service Principal)..." -ForegroundColor Cyan
az ad sp create --id $appId --output none

# 6. Get Service Principal ID
$spId = az ad sp list --filter "appId eq '$appId'" --query "[0].id" -o tsv

# 7. Enable User Assignment Required
Write-Host "Enabling User Assignment Required..." -ForegroundColor Cyan
az ad sp update --id $spId --set appRoleAssignmentRequired=true

# 8. Assign Groups to Enterprise App
if ($GroupObjectIds -and $GroupObjectIds.Count -gt 0) {
  Write-Host "Assigning Groups to Enterprise App..." -ForegroundColor Cyan
  
  $defaultAppRoleId = "00000000-0000-0000-0000-000000000000"
  
  foreach ($groupId in $GroupObjectIds) {
    Write-Host "  Assigning group: $groupId" -ForegroundColor Cyan
    
    $tempFile = [System.IO.Path]::GetTempFileName()
    $jsonBody = @{
      principalId = $groupId
      resourceId = $spId
      appRoleId = $defaultAppRoleId
    } | ConvertTo-Json
    
    Set-Content -Path $tempFile -Value $jsonBody -Encoding UTF8
    
    $uri = "https://graph.microsoft.com/v1.0/groups/$groupId/appRoleAssignments"
    
    az rest --method POST --uri $uri --headers "Content-Type=application/json" --body "@$tempFile" --output none 2>$null
    
    $success = $LASTEXITCODE -eq 0
    
    Remove-Item -Path $tempFile -Force
    
    if ($success) {
      Write-Host "  Group $groupId assigned successfully" -ForegroundColor Green
    }
    else {
      Write-Host "  Warning: Failed to assign group $groupId" -ForegroundColor Yellow
    }
  }
}

# 9. Create Client Secret
Write-Host "Creating Client Secret..." -ForegroundColor Cyan
$secretJson = az ad app credential reset --id $appId --append --display-name "BicepDeployment-$EnvironmentName" --years $SecretExpiryYears --output json
$secretOutput = $secretJson | ConvertFrom-Json
$clientSecret = $secretOutput.password

# Output
Write-Host ""
Write-Host "============================================" -ForegroundColor Green
Write-Host "App Registration Created Successfully!" -ForegroundColor Green
Write-Host "============================================" -ForegroundColor Green
Write-Host ""
Write-Host "Display Name:        $displayName" -ForegroundColor Cyan
Write-Host "App Object ID:       $appObjectId" -ForegroundColor Cyan
Write-Host "Service Principal:   $spId" -ForegroundColor Cyan
Write-Host ""
Write-Host "Group Claims Config:" -ForegroundColor Cyan
Write-Host "  - groupMembershipClaims: SecurityGroup" -ForegroundColor Cyan
Write-Host "  - Optional Claims: groups (ID Token + Access Token)" -ForegroundColor Cyan
Write-Host ""
Write-Host "--------------------------------------------" -ForegroundColor Yellow
Write-Host "CLIENT ID:           $appId" -ForegroundColor Yellow
Write-Host "CLIENT SECRET:       $clientSecret" -ForegroundColor Yellow
Write-Host "--------------------------------------------" -ForegroundColor Yellow
Write-Host ""

if ($GroupObjectIds -and $GroupObjectIds.Count -gt 0) {
  Write-Host "Assigned Groups:" -ForegroundColor Cyan
  foreach ($groupId in $GroupObjectIds) {
    $groupName = az ad group show --group $groupId --query displayName -o tsv 2>$null
    if ($groupName) {
      Write-Host "  - $groupName ($groupId)" -ForegroundColor Cyan
    }
    else {
      Write-Host "  - $groupId" -ForegroundColor Cyan
    }
  }
  Write-Host ""
}

Write-Host "Bicep Parameters:" -ForegroundColor Magenta
Write-Host ""
Write-Host "  authClientId = `"$appId`""
Write-Host "  authClientSecret = `"$clientSecret`""
Write-Host ""
Write-Host "============================================" -ForegroundColor Green

return [PSCustomObject]@{
  DisplayName        = $displayName
  ClientId           = $appId
  ClientSecret       = $clientSecret
  AppObjectId        = $appObjectId
  ServicePrincipalId = $spId
  AssignedGroups     = $GroupObjectIds
}

 

Buy Me A Coffee