
I just put my Azure AD Group Writeback Script on Github, and figured it was time to do something I know many have requested from Microsoft to deliver, but that is still missing; Using Azure AD Privileged Identity Management to control access to Active Directory built-in groups such as Domain Admin, Schema Admin and Enterprise Admin.
To keep this blog post as short as possible, I will not be covering things such as setting up Active Directory, configuring Azure AD Connect and so on. Also, this section on github covers what you need to know for authenticating the Azure AD Group Writeback Script to Azure AD for none-Azure environments, so I will only cover that briefly.
What we will do in this blog post is:
- Create a few privileged groups
- Set up the AAD Group Writeback Script to write back all privileged groups to an AD OU
- Add some of our privileged groups to the AD built-in groups
- Use Azure AD PIM to manage the privileged groups (Preview)
- Show the experience π
Let us start by creating a few privileged groups in the Azure Portal – “AD – Domain Admins” and “AD – Enterprise Admins”.

Next, for both groups, we open the group properties, find “Privileged access” and click “Enable privileged access”:

Now, I add two active assignments, just to have a few members to actually sync when setting up the AAD Group Writeback Script. These will be converted to eligible later.

Now that our two groups have been created, let us set up the AAD Group Writeback Script. Start by either downloading the Writeback Script repo or installing git and cloning the repo like this:
git clone "https://github.com/goodworkaround/AAD-Group-Writeback-Script.git" "c:\git\AAD-Group-Writeback-Script"
cd c:\git\AAD-Group-Writeback-Script
Next, make sure that the Active Directory PowerShell Module and the Azure AD PowerShell Module is installed using PowerShell:
Install-WindowsFeature RSAT-AD-PowerShell
Install-Module AzureAD
Start PowerShell and launch the script provided here (which is the same as the one below this line):
function New-WriteBackScriptInstallation {
[CmdletBinding()]
Param
(
[Parameter(Mandatory=$false,
Position=0)]
$ConfigFile = ".\Run.config"
)
$appName = "AzureAD to AD group writeback script"
Install-Module AzureAD | Out-Null
Connect-AzureAD | Out-Null
$requiredGrants = [Microsoft.Open.AzureAD.Model.RequiredResourceAccess]::new(
"00000003-0000-0000-c000-000000000000", # Microsoft Graph
@(
[Microsoft.Open.AzureAD.Model.ResourceAccess]::new("5b567255-7703-4780-807c-7be8301ae99b","Role")
[Microsoft.Open.AzureAD.Model.ResourceAccess]::new("df021288-bdef-4463-88db-98f22de89214","Role")
)
)
Write-Verbose "[Change] Creating the app registration '$appName'" -Verbose
$app = New-AzureADApplication -DisplayName $appName -RequiredResourceAccess $requiredGrants
$sp = New-AzureADServicePrincipal -AppId $app.appid
$key = New-AzureADApplicationPasswordCredential -ObjectId $app.ObjectId -EndDate (get-date).AddYears(100)
$url = "https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/CallAnAPI/appId/$($app.AppId)/isMSAApp/"
Write-Verbose "Go to the url that already is put on the clipboard and click 'Grant admin consent':`n`n$url" -Verbose
$url | Set-Clipboard
Read-host -Prompt "Click enter when done"
if(Test-Path $ConfigFile) {
$ConfigFile2 = ".\{0}.config" -f [guid]::NewGuid()
Write-Verbose "File $ConfigFile already exists, writing to $ConfigFile2"
$ConfigFile = $ConfigFile2
}
$_config = [ordered] @{
AuthenticationMethod = "ClientCredentials"
ClientID = $app.appid
EncryptedSecret = "$($key.Value | ConvertTo-SecureString -AsPlainText -Force | ConvertFrom-SecureString)"
TenantID = "$((Get-AzureADCurrentSessionInfo).TenantId.ToString())"
DestinationOU = "OU=AAD Group writeback,DC=contoso,DC=com"
ADGroupObjectIDAttribute = "info"
AADGroupScopingMethod = "PrivilegedGroups"
GroupDeprovisioningMethod = "PrintWarning"
} | ConvertTo-Json
set-content $ConfigFile -Value $_config
Write-Host "Created file $ConfigFile"
}
New-WriteBackScriptInstallation
Authenticate to Azure AD with an account with Global Admin, Cloud Application Admin or Applicaiton Admin role (need to be able to create an app registration, and consent to it):

Grant the app registration the required access, which is to read all users and groups: Click enter on the script window, and it should return that it has created a file for you. This is a config file that should work – almost, just by changing a few settings π The authentication stuff is now correct, and you only need to update the DestinationOU in your script. Here is my final configuration, where only line 6 and 9 has changed (DestinationOU and GroupDeprovisioningMethod):
{
"AuthenticationMethod": "ClientCredentials",
"ClientID": "e0736d2a-d385-42b0-80f6-60a11d817996",
"EncryptedSecret": "01000000d08c9ddf0115d1118c7a00c04fc297eb01000000be3e7b5924a98f4cbf5ebc0e1d25f1130000000002000000000003660000c00000001000000091f23fe77a40a5c4ac2ebe88e97e9ccc0000000004800000a0000000100000009d6fb3fd22c6ae7a74768b9d8d440767600000003580e05d35304ee9fed6b2e3ec5473e09109664fe14420959eb0ad7f37581c9466ab443509764ab3f6c160e7140a6ccdbb5b1ce5b5d80c6b6ac290bb8ead530085d91ef6b7f54e91a936d08a37dbf0e37526f69ccff49842e7c106d382e4ae5f14000000aa145ebe1fcffd99e6eca8d669b67a9858b6bbd8",
"TenantID": "937a3b05-1264-406f-ade1-3f4a42d4e26f",
"DestinationOU": "OU=AAD Privileged Groups,DC=contoso,DC=com",
"ADGroupObjectIDAttribute": "info",
"AADGroupScopingMethod": "PrivilegedGroups",
"GroupDeprovisioningMethod": "Delete"
}
Running Run.ps1 with -WhatIf and -Verbose now shows what is going to happen when:
.\Run.ps1 -WhatIf -Verbose
VERBOSE: Successfully received access token
VERBOSE: - oid: e67bebd1-7f97-4837-9379-bd3869c7d814
VERBOSE: - aud: e67bebd1-7f97-4837-9379-bd3869c7d814
VERBOSE: - iss: https://sts.windows.net/937a3b05-1264-406f-ade1-3f4a42d4e26f/
VERBOSE: - appid: e0736d2a-d385-42b0-80f6-60a11d817996
VERBOSE: - app_displayname: AzureAD to AD group writeback script
VERBOSE: - roles: Group.Read.All User.Read.All
VERBOSE: Getting all scoped groups
VERBOSE: Found 2 groups in scope
VERBOSE: Starting Save-ADGroup
VERBOSE: - Processing AADGroup 'AD - Domain Admins' (22fe80d8-674f-48ef-83b9-6539a7df9f03)
VERBOSE: - Creating group 'AD - Domain Admins' in AD
What if: Performing the operation "New" on target "CN=AD - Domain Admins (22fe80d8-674f-48ef-83b9-6539a7df9f03),OU=AAD Privileged Groups,DC=contoso,DC=com".
VERBOSE: - Processing AADGroup 'AD - Enterprise Admins' (8b264bee-f405-46f7-b1be-b7f1bf4ef72d)
VERBOSE: - Creating group 'AD - Enterprise Admins' in AD
What if: Performing the operation "New" on target "CN=AD - Enterprise Admins (8b264bee-f405-46f7-b1be-b7f1bf4ef72d),OU=AAD Privileged Groups,DC=contoso,DC=com".
VERBOSE: Save-ADGroup finished
VERBOSE: Processing all memberships
VERBOSE: - Processing group 'AD - Domain Admins' (22fe80d8-674f-48ef-83b9-6539a7df9f03)
WARNING: Unable to find AD group for AAD group 'AD - Domain Admins' (22fe80d8-674f-48ef-83b9-6539a7df9f03)
VERBOSE: - Processing group 'AD - Enterprise Admins' (8b264bee-f405-46f7-b1be-b7f1bf4ef72d)
WARNING: Unable to find AD group for AAD group 'AD - Enterprise Admins' (8b264bee-f405-46f7-b1be-b7f1bf4ef72d)
VERBOSE: Determining whether there are AD groups to deprovision
VERBOSE: No groups that should be deprovisioned
This looks good! Let’s just run it without WhatIf, before starting to invoke it as a scheduled task every 5 minutes (or you know, if you want to be a bit more sure that the script is working, run it manually and see what it does, like I will do in this blog post)
.\Run.ps1 -Verbose
VERBOSE: Successfully received access token
VERBOSE: - oid: e67bebd1-7f97-4837-9379-bd3869c7d814
VERBOSE: - aud: e67bebd1-7f97-4837-9379-bd3869c7d814
VERBOSE: - iss: https://sts.windows.net/937a3b05-1264-406f-ade1-3f4a42d4e26f/
VERBOSE: - appid: e0736d2a-d385-42b0-80f6-60a11d817996
VERBOSE: - app_displayname: AzureAD to AD group writeback script
VERBOSE: - roles: Group.Read.All User.Read.All
VERBOSE: Getting all scoped groups
VERBOSE: Found 2 groups in scope
VERBOSE: Starting Save-ADGroup
VERBOSE: - Processing AADGroup 'AD - Domain Admins' (22fe80d8-674f-48ef-83b9-6539a7df9f03)
VERBOSE: - Creating group 'AD - Domain Admins' in AD
VERBOSE: - Processing AADGroup 'AD - Enterprise Admins' (8b264bee-f405-46f7-b1be-b7f1bf4ef72d)
VERBOSE: - Creating group 'AD - Enterprise Admins' in AD
VERBOSE: Save-ADGroup finished
VERBOSE: Processing all memberships
VERBOSE: - Processing group 'AD - Domain Admins' (22fe80d8-674f-48ef-83b9-6539a7df9f03)
VERBOSE: - Adding member to AD group 'AD - Domain Admins (22fe80d8-674f-48ef-83b9-6539a7df9f03)': CN=Donald Duck,OU=User accounts,DC=contoso,DC=com
VERBOSE: - Adding member to AD group 'AD - Domain Admins (22fe80d8-674f-48ef-83b9-6539a7df9f03)': CN=Dolly Duck,OU=User accounts,DC=contoso,DC=com
VERBOSE: - Processing group 'AD - Enterprise Admins' (8b264bee-f405-46f7-b1be-b7f1bf4ef72d)
VERBOSE: Determining whether there are AD groups to deprovision
VERBOSE: No groups that should be deprovisioned
That’s it. Our AD now looks like this! We can not add the “AD – Domain Admins” group to our Domain Admins group to achieve global admin permissions.
Note 1: The adminSDHolder process will mess with the permissions of the group when you add it to Domain Admins, so make sure that you are running the Run.ps1 script as a user that has access to those kinds of groups
Note 2: Large ADs with many sites will experience that replication is an issue. This is where you could consider using this script to populate groups in a bastion forest. I am looking at adding support for msDs-ShadowPrincipal objects, just like MIM PAM does π

We are now ready to convert Dolly and Donald to Eligible Assignments. Go back to the Azure portal, on the AD – Domain Admins group and click “Update”, and change the assignments to eligible:



When running Run.ps1 now, we should see the users being removed from the group in AD:
VERBOSE: Successfully received access token
VERBOSE: - oid: e67bebd1-7f97-4837-9379-bd3869c7d814
VERBOSE: - aud: e67bebd1-7f97-4837-9379-bd3869c7d814
VERBOSE: - iss: https://sts.windows.net/937a3b05-1264-406f-ade1-3f4a42d4e26f/
VERBOSE: - appid: e0736d2a-d385-42b0-80f6-60a11d817996
VERBOSE: - app_displayname: AzureAD to AD group writeback script
VERBOSE: - roles: Group.Read.All User.Read.All
VERBOSE: Getting all scoped groups
VERBOSE: Found 2 groups in scope
VERBOSE: Starting Save-ADGroup
VERBOSE: - Processing AADGroup 'AD - Domain Admins' (22fe80d8-674f-48ef-83b9-6539a7df9f03)
VERBOSE: - Processing AADGroup 'AD - Enterprise Admins' (8b264bee-f405-46f7-b1be-b7f1bf4ef72d)
VERBOSE: Save-ADGroup finished
VERBOSE: Processing all memberships
VERBOSE: - Processing group 'AD - Domain Admins' (22fe80d8-674f-48ef-83b9-6539a7df9f03)
VERBOSE: - Removing member from AD group 'AD - Domain Admins (22fe80d8-674f-48ef-83b9-6539a7df9f03)': CN=Dolly Duck,OU=User accounts,DC=contoso,DC=com
VERBOSE: - Removing member from AD group 'AD - Domain Admins (22fe80d8-674f-48ef-83b9-6539a7df9f03)': CN=Donald Duck,OU=User accounts,DC=contoso,DC=com
VERBOSE: - Processing group 'AD - Enterprise Admins' (8b264bee-f405-46f7-b1be-b7f1bf4ef72d)
VERBOSE: Determining whether there are AD groups to deprovision
VERBOSE: No groups that should be deprovisioned
Sweet, now let’s look at Donald’s experience in all of this:
First he signs into the Azure Portal, and searches for “PIM” (which can be a favorite of course, directly linking to PIM)

He then goes to “Active just in time”

He goes to Privileged Access Groups, and acitivates his “AD – Domain Admins” group



Now, when Run.ps1 is again triggered, we see that Donald is added to the “AD – Domain Admins” AD group:
VERBOSE: Successfully received access token
VERBOSE: - oid: e67bebd1-7f97-4837-9379-bd3869c7d814
VERBOSE: - aud: e67bebd1-7f97-4837-9379-bd3869c7d814
VERBOSE: - iss: https://sts.windows.net/937a3b05-1264-406f-ade1-3f4a42d4e26f/
VERBOSE: - appid: e0736d2a-d385-42b0-80f6-60a11d817996
VERBOSE: - app_displayname: AzureAD to AD group writeback script
VERBOSE: - roles: Group.Read.All User.Read.All
VERBOSE: Getting all scoped groups
VERBOSE: Found 2 groups in scope
VERBOSE: Starting Save-ADGroup
VERBOSE: - Processing AADGroup 'AD - Domain Admins' (22fe80d8-674f-48ef-83b9-6539a7df9f03)
VERBOSE: - Processing AADGroup 'AD - Enterprise Admins' (8b264bee-f405-46f7-b1be-b7f1bf4ef72d)
VERBOSE: Save-ADGroup finished
VERBOSE: Processing all memberships
VERBOSE: - Processing group 'AD - Domain Admins' (22fe80d8-674f-48ef-83b9-6539a7df9f03)
VERBOSE: - Adding member to AD group 'AD - Domain Admins (22fe80d8-674f-48ef-83b9-6539a7df9f03)': CN=Donald Duck,OU=User accounts,DC=contoso,DC=com
VERBOSE: - Processing group 'AD - Enterprise Admins' (8b264bee-f405-46f7-b1be-b7f1bf4ef72d)
VERBOSE: Determining whether there are AD groups to deprovision
VERBOSE: No groups that should be deprovisioned
There are a few configuration options available for the script that I have not mentioned here. Please visit github for checking those out.
Good luck!
Really nice script! Got i working with filter. Is it possible to choose if the guid should be in the AD groupname or not? Seems that it will remove it if the groupname is long.
Hi,
Yes, have a look at “ADGroupNamePattern” in the documentation. You’ll find an example in Example1.config, and here: https://github.com/goodworkaround/AAD-Group-Writeback-Script#configuration
So essentially, if you know that your displaynames will always be unique, you can configure ADGroupNamePattern to “{0}”, which will cause the group to be called the same thing in AD as in Azure AD (but it will also cause collisions if there are groups with same display name, due to having same DistinguishedName)
Stepping through this after errors I’m finding lines 25-33 in the Run.ps1 don’t seem to be taking the AccessToken.
When the AuthenticationMethod is set to “ClientCredentials” the line…
$AccessToken = Get-ClientCredentialsMSGraphAccessToken -ClientID $Config.ClientID -EncryptedSecret $Config.EncryptedSecret -TenantID $Config.TenantID
Doesn’t seem to assign the AccessToken to the variable.
Any help would be much appreciated.
thanks
Hi Jeff,
This issue should now have been resolved once Marius commit latest changes to on Github.
Best regards,
Peter Selch Dahl
Done, thanks! π
Hi Marius,
When I set the AADGroupScopingMethod to “Filter” and AADGroupScopingConfig to “id eq “, the Save-ADGroup function wants to re-create all groups previously synced from AD to Azure AD. I tried changing AADGroupScopingMethod to “PrivilegedGroups” as well, which produces the same result. Any idea why that’s happening? Thanks
There appears to be a limitation in the graph requests I’m able to run. I can run Get-GraphRequestRecursive -Url “https://graph.microsoft.com/v1.0/groups//members?`$select=id,displayName,userPrincipalName,onPremisesDistinguishedName,onPremisesImmutableId” -AccessToken $AccessToken, and it will return the members of the group, but I can’t filter on /groups directly or even run a get of the group itself: https://graph.microsoft.com/v1.0/groups/. I read an article about ‘group search limitations for guest users in organizations’, but I wouldn’t think it’d apply here. Any thoughts?
That is strange. Are you changing the anchor attribute in AD at the same time? This could cause such as issue.
Hi Marius, thanks for the response. I haven’t modified the anchor attribute. I ended up having to set the $ScopeGroups variable to the specific Graph URL for the group. However, then I was getting a ‘Cannot bind argument to parameter ‘ScopedGroups’ because it is null.’ error. I added a $Result reference on the line after $Result = Invoke-RestMethod $Url -Headers @{Authorization = “Bearer $AccessToken” } -Verbose:$false in the HelperFunctions script, and then it worked. I’m not quite sure why. One other question. Should I be filtering these groups from being synced from AD back to Azure AD via the Connect utility? Otherwise, it looks like the PIM group gets created in on-premise AD, but then when synced to Azure, it creates a duplicate group. Thanks again!
Hi, since ADconnect now allows to bring cloud groups present only on cloud on prem, I wanted to use this to create cloud-only groups where users could request to become domain admin of local domain; everything works, however I would like to bypass the sync imposed by ad connect (30 minutes) otherwise the requestor is activated, but until the sync is performed he will not be seen as domain admin on onprem servers.
Would you have any advice?
Hi, currently Microsoft does not support lower time intervals than 30 minutes between each sync, so the only method is to utilize the my github project for those groups, which can run every minute or so: https://github.com/goodworkaround/AAD-Group-Writeback-Script
Also, built-in groups like “Domain admins” should be of scope “Domain local”, which groups that AAD Connect writes back cannot be a member of (because they are Global)