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
- Login and use an ASP.NET Core API with Azure AD Auth and user access tokens
- Angular SPA with an ASP.NET Core API using Azure AD Auth and user access tokens
- Restricting access to an Azure AD protected API using Azure AD Groups
- Using Azure CLI to create Azure App Registrations
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.
- Not possible to set all 4 values of the signInAudience property
- Not possible to assign the Application to the group (Or not documentated)
- Cannot update the groupMembershipClaims to one of the 3 values
- It is not possible to create return URLs of type SPA
- 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
- 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/
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/graph/api/resources/application?view=graph-rest-1.0
https://winsmarts.com/update-azure-ad-applications-signinaudience-using-microsoft-graph-79b5af3ec901
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.
Hi Alexandre thanks for the link and the blog. I still find CLI a bit messy for App registrations.
Greetings Damien
[…] Using Azure CLI to create Azure App Registrations (Damien Bowden) […]
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
Hi Timothy
This does work, the char is due to powershell, I use this. Do you call it from a powershell console?
Greetings Damien
[…] Using Azure CLI to create Azure App Registrations — Software Engineering […]
[…] Using Azure CLI to create Azure App Registrations […]
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]”
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.
thanks, I could add this to my CI Infra build then, greetings Damien
Also thinking about implementing this in the Azure SDK.
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.
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
good idea, thanks for the tip, greetings Damien