Janne Mattila

From programmer to programmer -- Programming just for the fun of it

Managed identities, Application Permissions and Entra ID automations

Posted on: November 4, 2024

I have previously blogged about various Azure and Entra ID automation topics e.g., Automating maintenance tasks with Azure Functions and PowerShell, Entra ID Group automation with PowerShell, and Automations with Azure Pipelines or GitHub Actions.

Typically, when you start working on these automations, you need to create a new app registration in Entra ID and assign it the necessary permissions based on your scenario. So, you end up into this Entra ID view:

Since we would be looking to do some kind of background processing, we would need to select the following permissions:

Application permissions
Your application runs as a background service or daemon without a signed-in user.

Let’s try to cover the following implementation scenario:

I would start by adding the necessary permissions to the app registration in Entra ID. First, I would need to add the following permission to be able to create a new group:

Second, I would need to add the following permission to be able to read the basic information of the users:

Here are the permissions that I’ve added:

Since the above permissions are application permissions, they require admin consent:

Now, I can test my automation script to create a new group in Entra ID. I’ll Login with a service principal, I’ve created using the above configuration.

$clientId = "<put your client id here>"
$clientSecret = "<put your client secret here>"
$tenantId = "<put your tenant id here>"

$clientPassword = ConvertTo-SecureString $clientSecret -AsPlainText -Force
$credentials = New-Object System.Management.Automation.PSCredential($clientID, $clientPassword)
Connect-AzAccount -ServicePrincipal -Credential $credentials -TenantId $tenantId

After the login, I can execute my script to create a new group in Entra ID. Here is an abbreviated and simplified version of the script but you can find the link to the full script at the end of the post:

$groupJson = @{
  displayName         = $GroupName
  description         = $GroupDescription
  mailEnabled         = $false
  mailNickname        = $GroupMailNickName
  securityEnabled     = $true
  groupTypes          = @()
  "owners@odata.bind" = [string[]]"https://graph.microsoft.com/v1.0/servicePrincipals(appId='$($ClientId)')"
} | ConvertTo-Json

$groupResponse = Invoke-AzRestMethod `
  -Uri "https://graph.microsoft.com/v1.0/groups" `
  -Method Post -Payload $groupJson
$group = $groupResponse.Content | ConvertFrom-Json

$memberIds | ForEach-Object {
  $id = $_
  $bodyJson = @{
    "@odata.id" = "https://graph.microsoft.com/v1.0/directoryObjects/$id"
  } | ConvertTo-Json

  Invoke-AzRestMethod `
    -Uri "https://graph.microsoft.com/v1.0/groups/$($group.id)/members/`$ref" `
    -Method POST -Payload $bodyJson
}

Notice that we’re using members@odata.bind to add owner to the group during the create operation:

Creating a group using the Group.Create application permission without specifying owners creates the group anonymously and the group isn’t modifiable. Add owners to the group while creating it so the owners can manage the group.

The above approach works fine, and a new group gets created as expected:

However, using service principal requires you to use client secrets, certificates or federated credentials for the authentication. You must maintain and store these secrets securely.

Therefore, we’ll investigate achieving the same using managed identity. Let’s start by creating a new user assigned-identity:

$identityName = "id-entra-id-automation-identity"
$resourceGroupName = "rg-entra-id-automation"
$location = "swedencentral"

# Create a new resource group
New-AzResourceGroup `
  -Name $resourceGroupName `
  -Location $location

# Create a new managed identity
$identity = New-AzUserAssignedIdentity `
  -ResourceGroupName $resourceGroupName `
  -Location $location `
  -Name $identityName

Unfortunately, we don’t have API Permissions in the managed identity view. Therefore, we need to assign the necessary permissions using the APIs.

If we quickly still get back to the previous configuration of our service principal, we can see the permission names:

Permissions to our managed identity can be granted using
New-AzADServicePrincipalAppRoleAssignment cmdlet:

$microsoftGraphApp = Get-AzADServicePrincipal -ApplicationId "00000003-0000-0000-c000-000000000000"
$graphPermissions = $microsoftGraphApp | Select-Object -ExpandProperty AppRole

New-AzADServicePrincipalAppRoleAssignment `
  -ServicePrincipalId $identity.PrincipalId `
  -ResourceId $microsoftGraphApp.Id `
  -AppRoleId ($graphPermissions `
  | Where-Object { $_.Value -eq "Group.Create" } `
  | Select-Object -ExpandProperty Id)

Or alternatively, you can use Graph Rest API directly:

$appRoleJson = [ordered]@{
  principalId = $identity.PrincipalId
  resourceId  = $microsoftGraphApp.Id
  appRoleId   = ($graphPermissions `
    | Where-Object { $_.Value -eq "Group.Create" } `
    | Select-Object -ExpandProperty Id)
} | ConvertTo-Json

$response = Invoke-AzRestMethod `
  -Method Post `
  -Uri "https://graph.microsoft.com/v1.0/servicePrincipals/$($microsoftGraphApp.Id)/appRoleAssignedTo" `
  -Payload $appRoleJson

After the Group.Create and User.ReadBasic.All permissions are granted, you can see these permissions under Enterprise apps:

Now, we’re ready to use the configured managed identity in e.g., Virtual Machine (or any other service that supports it):

In Azure PowerShell, you can replace the above service principal login with the managed identity and rest of the script remains the same:

Connect-AzAccount -Identity

If you just want to use bash script, you can use the following approach based on this tutorial:

# Get the identity information including token
curl -s \
 'http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://graph.microsoft.com/' \
 -H 'Metadata: true' > identity.json

# Create a new group
curl -s -X POST 'https://graph.microsoft.com/v1.0/groups' \
  -H "Authorization: Bearer $(cat identity.json | jq -r .access_token)" \
  -H "Content-Type: application/json" \
  -d '{
  "displayName": "Example Group",
  "description": "Example Group Description",
  "mailEnabled": false,
  "mailNickname": "examplegroup",
  "securityEnabled": true,
  "groupTypes": [],
  "owners@odata.bind": [
    "https://graph.microsoft.com/v1.0/servicePrincipals(appId='\'$(cat identity.json | jq -r .client_id)\'')"
  ] 
}' | jq -r .id > id.txt

# Add member to the group
curl -X POST "https://graph.microsoft.com/v1.0/groups/$(cat id.txt)/members/\$ref" \
  -H "Authorization: Bearer $(cat identity.json | jq -r .access_token)" \
  -H "Content-Type: application/json" -d '{
    "@odata.id": "https://graph.microsoft.com/v1.0/directoryObjects/cdbc71fd-0e73-48f0-b14c-f5216c4fb440"
}'

If you grab the access token from the above, you can see the roles claims with the help of jwt.ms:

It would be nice to use Azure PowerShell cmdlets in this limited permissions scenario, but unfortunately, the New-AzADGroup doesn’t support adding owners during the creation.


Another very similar scenario is automating group memberships which is typically combined with the requirement to scope this to only specific groups.

Typical requirements are:

Here’s how you can achieve this:

1) Create a new app registration in Entra ID or use managed identity as shown above

2) Add this identity as owner to the groups that you want to manage:

Now you can add the members directly to those groups even if you don’t have any permissions:

# Add member
$bodyJson = @{
    "@odata.id" = "https://graph.microsoft.com/v1.0/directoryObjects/cdbc71fd-0e73-48f0-b14c-f5216c4fb440"
} | ConvertTo-Json

Invoke-AzRestMethod `
    -Uri "https://graph.microsoft.com/v1.0/groups/ae22a67d-ce3e-4626-8b0c-94b003525a09/members/`$ref" `
    -Method POST -Payload $bodyJson

As in the above first example, you will have to add additional permissions if you need to look up e.g., users. Typical permissions for this case is the same as the one used above: User.ReadBasic.All. In this scenario, our identity is limited managing only these 25 groups and it cannot manage any other groups.

Conclusion

In this post, we’ve looked into how to use managed identity and application permissions to automate Entra ID operations. We’ve also saw how to limit the permissions to only specific groups.

You can find the full scripts from the following links:

I hope you find this useful!