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
}
