Using Azure CLI to create Azure App Registrations

This blog shows how to setup Azure App Registrations using Azure CLI and Powershell. The scripts setup the configuration for the applications created in the previous posts in this serious. The aim was to achieve the same as configured in the Azure Portal. This is not possible with the current version of Azure CLI. A few steps still need to be completed manually.

Code: https://github.com/damienbod/AzureAD-Auth-MyUI-with-MyAPI

Posts in this Series

The scripts are organised so that you can create the Azure infrastructure using the create_app_registrations with one or two parameters. Before you call the script, you need to login with the az login, and install Azure CLI if not already installed. The script requires the tenantId as parameter. This is where the App registrations will be created.

.\create_app_registrations.ps1 "your tenantId"

The create_app_registrations calls three separate scripts to create the different apps. An App registration is created for the API, a Web UI App registration is created, and the group and the Enterprise APP is updated.

Param( [string]$tenantId = "", [string]$secretForWebApp = "" )

function testParams() {

	if (!$tenantId) 
	{ 
		Write-Host "tenantId is null"
		exit 1
	}

	if (!$secretForWebApp) 
	{ 
		Write-Host "secretForWebApp is not defined, creating one"
		$secretForWebApp = New-Guid
	}
	return $secretForWebApp
}

function testSubscription {
    $account = az account show | ConvertFrom-Json
	$accountTenantId = $account.tenantId
    if ($accountTenantId -ne $tenantId) 
	{ 
		Write-Host "$accountTenantId not possible, change account"
		exit 1
	}
	$accountName = $account.name
    Write-Host "tenant: $accountName can update"
}

$secretForWebApp = testParams
testSubscription

Write-Host "tenantId $tenantId"
Write-Host "secretForWebApp $secretForWebApp"
Write-Host "-----------"
Write-Host (az version)
Write-Host "-----------"

# Create API App Registration
$createdAppRegAppIdApi = &".\api_create_azure_app_registration.ps1" $tenantId | select -Last 1
Write-Host "Created Api App registraion: $createdAppRegAppIdApi"

# Create Server Rendered App Registration
$createdAppRegAppIdServerRenderedUI = &".\server_rendered_create_azure_app_registration.ps1" $tenantId $createdAppRegAppIdApi $secretForWebApp | select -Last 1
Write-Host "Created Server Rendered App registraion: $createdAppRegAppIdServerRenderedUI"

# Create Group and Update Azure AD Enterprise APP for the API App Registration 
$groupName = &".\app_group_enterprise.ps1" $tenantId $createdAppRegAppIdApi  | select -Last 1
Write-Host "Created Group and updated Azure AD Enterprise APP for the API App Registration, groupName: $groupName"

Write-Host "Add the $groupName group to the App Registration and add users"

Creating the Azure Registration for the API

The API script creates the infrastructure in different steps. The App Registration is created in the first step and the optional claims are added after that. The oauth2Permissions are used to add the scopes. This is a bit complicated because you need to disable the default scope which was created in the create command, and then delete this. When creating an App registration using Azure CLI, it adds a default permission, which needs to be disabled before you can remove this or update. A service principle is created for the App registration.

Param( [string]$tenantId = "" )
$displayNameApi = "mi-api"
$bodyApi = '{
	"signInAudience" : "AzureADandPersonalMicrosoftAccount", 
	"groupMembershipClaims": "None"
}' | ConvertTo-Json | ConvertFrom-Json
$userAccessScopeApi = '{
		"lang": null,
		"origin": "Application",		
		"adminConsentDescription": "Allow access to the API",
		"adminConsentDisplayName": "mi-api-access",
		"id": "--- replaced in scripts ---",
		"isEnabled": true,
		"type": "User",
		"userConsentDescription": "Allow access to mi-api access_as_user",
		"userConsentDisplayName": "Allow access to mi-api",
		"value": "access_as_user"
}' | ConvertTo-Json | ConvertFrom-Json

##################################
### testParams
##################################

function testParams {

	if (!$tenantId) 
	{ 
		Write-Host "tenantId is null"
		exit 1
	}
}

testParams

Write-Host "Begin API Azure App Registration"

##################################
### Create Azure App Registration
##################################

$identifierApi = New-Guid
$identifierUrlApi = "api://" + $identifierApi
$myApiAppRegistration = az ad app create `
	--display-name $displayNameApi `
	--available-to-other-tenants true `
	--oauth2-allow-implicit-flow  false `
	--identifier-uris $identifierUrlApi `
	--required-resource-accesses `@api_required_resources.json 

$myApiAppRegistrationResult = ($myApiAppRegistration | ConvertFrom-Json)
$myApiAppRegistrationResultAppId = $myApiAppRegistrationResult.appId
Write-Host " - Created API $displayNameApi with myApiAppRegistrationResultAppId: $myApiAppRegistrationResultAppId"

##################################
### Add optional claims to App Registration 
##################################

az ad app update --id $myApiAppRegistrationResultAppId --optional-claims `@api_optional_claims.json
Write-Host " - Optional claims added to App Registration: $myApiAppRegistrationResultAppId"

##################################
###  Add scopes (oauth2Permissions)
##################################

# 1. read oauth2Permissions
$oauth2PermissionsApi = $myApiAppRegistrationResult.oauth2Permissions

# 2. set to enabled to false from the defualt scope, because we want to remove this
$oauth2PermissionsApi[0].isEnabled = 'false'
$oauth2PermissionsApi = ConvertTo-Json -InputObject @($oauth2PermissionsApi) 
# Write-Host "$oauth2PermissionsApi" 
# disable oauth2Permission in Azure App Registration
$oauth2PermissionsApi | Out-File -FilePath .\oauth2Permissionsold.json
az ad app update --id $myApiAppRegistrationResultAppId --set oauth2Permissions=`@oauth2Permissionsold.json

# 3. delete the default oauth2Permission
az ad app update --id $myApiAppRegistrationResultAppId --set oauth2Permissions='[]'

# 4. add the new scope required add the new oauth2Permissions values
$oauth2PermissionsApiNew += (ConvertFrom-Json -InputObject $userAccessScopeApi)
$oauth2PermissionsApiNew[0].id = $identifierApi
$oauth2PermissionsApiNew = ConvertTo-Json -InputObject @($oauth2PermissionsApiNew) 
# Write-Host "$oauth2PermissionsApiNew" 
$oauth2PermissionsApiNew | Out-File -FilePath .\oauth2Permissionsnew.json
az ad app update --id $myApiAppRegistrationResultAppId --set oauth2Permissions=`@oauth2Permissionsnew.json
Write-Host " - Updated scopes (oauth2Permissions) for App Registration: $myApiAppRegistrationResultAppId"

##################################
###  Create a ServicePrincipal for the API App Registration
##################################

az ad sp create --id $myApiAppRegistrationResultAppId | Out-String | ConvertFrom-Json
Write-Host " - Created Service Principal for API App registration"

return $myApiAppRegistrationResultAppId

Creating the Azure Registration for the Server Rendered UI

The App registration for the server rendered UI is like the API one. The reply URLs need to be added and the logout parameter. The id for the scope which was created in the API App registration needs to be added to the scopes of the WEB App registration. A client secret is also required as this is a trusted application.

Param( [string]$tenantId = "", [string]$inputAppIdApi = "", [string]$secretForWebApp = "" )

$replyUrlsSrUI = "https://localhost:44344/signin-oidc"
$logoutUrl = "https://localhost:44344/signout-callback-oidc"
$displayNameSrUI = "mi-server-rendered-portal"
$bodySrUI = '{
	"signInAudience" : "AzureADandPersonalMicrosoftAccount", 
	"groupMembershipClaims": "None"
}' | ConvertTo-Json | ConvertFrom-Json

$requiredResourceAccessesSrUI = '[{
	"resourceAppId": "00000003-0000-0000-c000-000000000000",
	"resourceAccess": [
		{
			"id": "64a6cdd6-aab1-4aaf-94b8-3cc8405e90d0",
			"type": "Scope"
		},
		{
			"id": "7427e0e9-2fba-42fe-b0c0-848c9e6a8182",
			"type": "Scope"
		},
		{
			"id": "37f7f235-527c-4136-accd-4a02d197296e",
			"type": "Scope"
		},
		{
			"id": "14dad69e-099b-42c9-810b-d002981feec1",
			"type": "Scope"
		},
		{
			"id": "e1fe6dd8-ba31-4d61-89e7-88639da4683d",
			"type": "Scope"
		}
	]
},
{
	"resourceAppId": "-- replace --",
	"resourceAccess": [
		{
			"id": "-- replace --",
			"type": "Scope"
		}
	]
}]' | ConvertTo-Json | ConvertFrom-Json

##################################
### testParams
##################################

function testParams {

	if (!$tenantId) 
	{ 
		Write-Host "tenantId is null"
		exit 1
	}

	if (!$inputAppIdApi) 
	{ 
		Write-Host "inputAppIdApi is null"
		exit 1
	}
	
	if (!$secretForWebApp) 
	{ 
		Write-Host "secretForWebApp is null"
		exit 1
	}
}

testParams

Write-Host "Begin ServerRendered Azure App Registration"

$appDataFromInputAppIdApi = az ad app show --id $inputAppIdApi | Out-String | ConvertFrom-Json
$appDataFromInputAppIdApiOauth2Permissions = $appDataFromInputAppIdApi.oauth2Permissions[0]

$requiredResourceAccessesSrUIData = ConvertFrom-Json $requiredResourceAccessesSrUI
# add the API values to the requiredResourceAccessesSrUIData
$requiredResourceAccessesSrUIData[1].resourceAppId = $inputAppIdApi
$requiredResourceAccessesSrUIData[1].resourceAccess[0].id = $appDataFromInputAppIdApiOauth2Permissions.id
$requiredResourceAccessesSrUINew =  $requiredResourceAccessesSrUIData | ConvertTo-Json -Depth 5
# Write-Host "$requiredResourceAccessesNew" 
 
$requiredResourceAccessesSrUINew | Out-File -FilePath .\server_rendered_required_resources.json
Write-Host " - Updated required-resource-accesses for new App Registration"

##################################
### Create Azure App Registration
##################################

$myServerRenderedAppRegistration = az ad app create `
	--display-name $displayNameSrUI `
	--available-to-other-tenants true `
	--oauth2-allow-implicit-flow  false `
	--reply-urls $replyUrlsSrUI `
	--password $secretForWebApp `
	--required-resource-accesses `@server_rendered_required_resources.json

$myServerRenderedAppRegistrationData = ($myServerRenderedAppRegistration | ConvertFrom-Json)
$myServerRenderedAppRegistrationDataAppId = $myServerRenderedAppRegistrationData.appId
Write-Host " - Created ServerRendered $displayNameSrUI with appId: $myServerRenderedAppRegistrationDataAppId"

##################################
###  add logoutUrl
##################################

az ad app update --id $myServerRenderedAppRegistrationDataAppId --set logoutUrl=$logoutUrl
Write-Host " - Updated logoutUrl"

##################################
### Add optional claims to App Registration 
##################################

az ad app update --id $myServerRenderedAppRegistrationDataAppId --optional-claims `@server_rendered_optional_claims.json
Write-Host " - Optional claims added to App Registration: $myServerRenderedAppRegistrationDataAppId"

##################################
###  Remove scopes (oauth2Permissions)
##################################

# 1. read oauth2Permissions
$oauth2PermissionsSrUI = $myServerRenderedAppRegistrationData.oauth2Permissions

# 2. set to enabled to false from the defualt scope, because we want to remove this
$oauth2PermissionsSrUI[0].isEnabled = 'false'
$oauth2PermissionsSrUI = ConvertTo-Json -InputObject @($oauth2PermissionsSrUI) 
# Write-Host "$oauth2PermissionsSrUI" 
# disable oauth2Permission in Azure App Registration
$oauth2PermissionsSrUI | Out-File -FilePath .\oauth2Permissionsold.json
az ad app update --id $myServerRenderedAppRegistrationDataAppId --set oauth2Permissions=`@oauth2Permissionsold.json

# 3. delete the default oauth2Permission
az ad app update --id $myServerRenderedAppRegistrationDataAppId --set oauth2Permissions='[]'
Write-Host " - Updated scopes (oauth2Permissions) for App Registration: $myServerRenderedAppRegistrationDataAppId"

##################################
###  Create a ServicePrincipal for the ServerRendered App Registration
##################################

az ad sp create --id $myServerRenderedAppRegistrationDataAppId
Write-Host " - Created Service Principal for ServerRendered App registration"

return $myServerRenderedAppRegistrationDataAppId

Creating the Azure AD Group and update the Enterprise APP

The last script creates an Azure AD group in the Azure AD tenant. This group is used to add users which can access or use the API. The Enterprise APP for the WEB is setup to only allow defined users. You need to add the users yourself either using dynamic queries, or manually.

Param( [string]$tenantId = "", [string]$apiAppId = "" )
$groupName = "miapigroup"

function testParams {

	if (!$tenantId) 
	{ 
		Write-Host "tenantId is null"
		exit 1
	}

	if (!$apiAppId) 
	{ 
		Write-Host "apiAppId is null"
		exit 1
	}
}

testParams

Write-Host "Creating group and updating App Registration: $apiAppId"

##################################
### Create group
##################################

Write-Host " - Create new group"
$group = az ad group create `
	--display-name $groupName `
	--mail-nickname $groupName `

#Write-Host "$group" 
$groupObjectId = ($group | ConvertFrom-Json).objectId
Write-Host " - Created new group objectId: $groupObjectId"
	
##################################
### Only allowed defined users, groups use the app
##################################

az ad sp update --id $apiAppId --set appRoleAssignmentRequired=true
Write-Host " - Set user assigned true to APP registration"
#az ad sp show --id $apiAppId

##################################
### Assign group to App Registration
##################################

# NOT possible, you need to do this per Portal UI ...

#az ad group list --display-name $groupName
#az role assignment create --assignee $groupObjectId --resourceId $apiAppId --role "00000000-0000-0000-0000-000000000000"

return $groupName

Problems With Azure CLI and Azure App Registrations

Azure CLI comes short in a lot of ways when creating infrastructure for Azure App Registrations. You need to use the Azure portal to add the Azure AD group to the Azure AD Enterprise application. The signInAudience, groupMembershipClaims might need to be updated as well, depending on your requirements. For example, it is not possible to set the value AzureADandPersonalMicrosoftAccount for the signInAudience. You can use the graph API for this, but this does not work well together with Azure CLI.

  1. Not possible to set all 4 values of the signInAudience property
  2. Not possible to assign the Application to the group (Or not documentated)
  3. Cannot update the groupMembershipClaims to one of the 3 values
  4. It is not possible to create return URLs of type SPA
  5. Using the Graph API seems to have problems when working together with Azure CLI. Items created with graph are not found by CLI or the other way around. The Graph API is also missing properties
  6. The Azure CLI docs for creating Azure App Registrations are not good

Workarounds for the missing CLI features

The az rest command can be used to use the graph API to update the signInAudience and the groupMembershipClaims values. This works well the first time, but has problems running more than once in the same script.

az rest 
   --method PATCH 
   --uri 'https://graph.microsoft.com/v1.0/applications/7a7d2df0-0fce-4fd9-8bcd-87d62b967dae' 
   --headers 'Content-Type=application/json' 
   --body '{\"signInAudience\" : \"AzureADandPersonalMicrosoftAccount\", \"groupMembershipClaims\": \"None\"}'

This can also be executed with the Invoke-RestMethod powershell request, but with the same problem as the az rest.

# https://docs.microsoft.com/en-us/graph/api/application-update
$myServerRenderedAppRegistrationDataObjectId = $myServerRenderedAppRegistrationData.objectId
Write-Host " - id = srApp.objectId: $myServerRenderedAppRegistrationDataObjectId"
$tokenResponseSrUI = az account get-access-token --resource https://graph.microsoft.com
$tokenSrUI = ($tokenResponseSrUI | ConvertFrom-Json).accessToken
#Write-Host "$tokenSrUI"
$uriSrUI = 'https://graph.microsoft.com/v1.0/applications/' + $myServerRenderedAppRegistrationDataObjectId
Write-Host " - $uriSrUI"
$headersSrUI = @{
    "Authorization" = "Bearer $tokenSrUI"
}

Invoke-RestMethod `
	-ContentType application/json `
	-Uri $uriSrUI `
	-Method Patch `
	-Headers $headersSrUI `
	-Body $bodySrUI

Now just use the App registrations in the ASP.NET Core applications and everything should work. Add users to the Azure AD group to login.

Links

https://docs.microsoft.com/en-us/cli/azure/install-azure-cli-windows?view=azure-cli-latest

https://docs.microsoft.com/en-us/cli/azure/ad/app?view=azure-cli-latest#az-ad-app-create

https://www.vrdmn.com/2018/08/create-azure-ad-app-registration-with.html

https://www.frodehus.dev/add-scopes-to-azure-ad-via-azure-cli/

https://anmock.blog/2020/01/10/azure-cli-create-an-azure-ad-application-for-an-api-that-exposes-oauth2-permissions/

https://joonasw.net/view/defining-permissions-and-roles-in-aad

https://docs.microsoft.com/en-us/cli/azure/ad/group?view=azure-cli-latest#az-ad-group-create

https://docs.microsoft.com/en-us/azure/active-directory/manage-apps/assign-user-or-group-access-portal

https://docs.microsoft.com/en-us/graph/api/resources/application?view=graph-rest-1.0

https://winsmarts.com/update-azure-ad-applications-signinaudience-using-microsoft-graph-79b5af3ec901

https://developer.microsoft.com/graph/graph-explorer

14 comments

  1. Interesting post, I wrote an article on a similar topic but focus on PowerShell instead of CLI (https://www.techwatching.dev/posts/teams-sso-powershell). As you said, it seems things are missing in az cli, but it’s nice to see there are workarounds, I was thinking Azure PowerShell was the only way around.

    1. Hi Alexandre thanks for the link and the blog. I still find CLI a bit messy for App registrations.

      Greetings Damien

  2. […] Using Azure CLI to create Azure App Registrations (Damien Bowden) […]

  3. Timothy Hennessy · · Reply

    This key line of code : az ad app update –id $appId –set oauth2Permissions=`@oauth2Permissionsold.json does not work for me. In fact, the `@ characters look suspect to me. To be fair, I can’t find a working line of code doing exactly what this is intended to do which is disable the default scope. Pls reply if you know what is wrong. the basic message is “Unable to build a model: Cannot deserialize as [OAuth2Permission] an object of type ….” Thanks

    1. Hi Timothy

      This does work, the char is due to powershell, I use this. Do you call it from a powershell console?

      Greetings Damien

  4. […] Using Azure CLI to create Azure App Registrations — Software Engineering […]

  5. I am getting deserialization issue while configuring new scope
    az ad app update –id $apiAppRegistrationResultAppId –set oauth2Permissions=`@oauth2Permissionsnew.json

    When I print
    $oauth = Get-Content -Path .\oauth2Permissionsnew.json
    Write-Host “New scope” $oauth

    My scope does contains all the updated value….
    “[\n {\n \”adminConsentDescription\”: \”Allow access to the API\”,\n \”adminConsentDisplayName\”: \”mi-api-access\”,\n \”id\”: \”242345w-478c-441a-a057-a34b2bcf096e\”,\n \”isEnabled\”: true,\n \”type\”: \”User\”,\n \”userConsentDescription\”: \”Allow access to mi-api access_as_user\”,\n \”userConsentDisplayName\”: \”Allow access to mi-api\”,\n \”value\”: \”access_as_user\”\n }\n]”

  6. Josie · · Reply

    You can add users/groups/service principles to Enterprise apps programmatically either via Azure AD Powershell Cmdlets

    https://docs.microsoft.com/en-us/powershell/module/azuread/new-azureadgroupapproleassignment?view=azureadps-2.0

    Or using the az rest to hit the graph api
    https://docs.microsoft.com/en-us/graph/api/resources/approleassignment?view=graph-rest-1.0

    You’re right though it can’t be done directly in the CLI itself.

    1. thanks, I could add this to my CI Infra build then, greetings Damien

    2. Also thinking about implementing this in the Azure SDK.

  7. Rekisteri · · Reply

    Example command how to add users (and groups) to service principals using CLI: https://stackoverflow.com/a/60091021/275183
    Thank you! This really helped a lot.

  8. Bram Fokke · · Reply

    Thanks, this article was very helpful.

    One thing: Please don’t use New-Guid to generate application secrets:
    https://stackoverflow.com/questions/4517497/how-secure-are-guids-in-terms-of-predictability

    1. good idea, thanks for the tip, greetings Damien

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.