Jorge's Quest For Knowledge!

All About Identity And Security On-Premises And In The Cloud – It's Just Like An Addiction, The More You Have, The More You Want To Have!

(2021-10-18) Azure AD Administrative Units – Dynamically Managing AU Assignments – Part 4

Posted by Jorge on 2021-10-18


Find PART 3 of this series HERE

[AD.4 – A registered application with the correct permissions to manage the AU assignments]

To be able to assign objects (users and groups) in an automatic manner to Administrative Units, a registered application is needed with the correct application permissions (not delegated permissions) to assign the objects that match the filter to the corresponding Administrative Unit.

The following permissions are required:

  • Group.Read.All
  • User.Read.All
  • AdministrativeUnit.ReadWrite.All

After configuring the permissions, those permissions also need to be consented before you can actually use them.

To control the application in a secure authenticated manner, either a client secret can be used or a certificate. I those to use a certificate as that requires possession (private key) and knowledge (password for private key) instead of just knowledge (secret).

The code below will create a self-signed certificate on your local computer, export that to a CER and PFX (protected with a password) and then delete the certificate and private key from the local computer store. It will also present the password on screen for you to copy it to a secure location! Make sure to store the PFX file and its corresponding password in a secure place.

Invoke-Command -ScriptBlock {
	Clear-Host
	
	# The Subject Name
	$subjectName = "<SPECFIFY THE SUBJECT NAME FOR THE CERTIFICATE>" # e.g. "mgmt-Admin-Units-MSFT-Graph"

	# Tenant Details
	$tenantFQDN = "<SPECFIFY YOUR TENANT FQDN>" # e.g. "<TENANT NAME>.ONMICROSOFT.COM"

	# Certificate Store Location
	$certStoreLocation = "Cert:\CurrentUser\My"

	# Where To Export The Certificate And The Private Key
	$exportFolderPath = "<Folder Path To Export The Certificate Data To>" # e.g. "C:\TEMP"
	$cerOutputPath = Join-Path $exportFolderPath "$subjectName.cer"
	$pfxOutputPath = Join-Path $exportFolderPath "$subjectName.pfx"

	# Splat For Readability
	$createCertificateSplat = @{
		Type				= "Custom"
		Subject				= $subjectName
		KeyFriendlyName		= $subjectName
		KeyDescription		= $subjectName
		FriendlyName		= $subjectName
		DnsName				= $tenantName
		CertStoreLocation	= $certStoreLocation
		Provider			= "Microsoft Enhanced RSA and AES Cryptographic Provider"
		KeySpec				= "KeyExchange"
		KeyUsage			= @("None")
		HashAlgorithm		= "SHA256"
		KeyAlgorithm		= "RSA"
		KeyLength			= 2048
		NotBefore			= $([datetime]::now.AddHours(-1))
		NotAfter			= $([datetime]::now.AddDays(1185)) # 3 Years and 3 months | This is to make sure the process always start in the same period as it otherwise will crawl back!
		KeyExportPolicy		= "Exportable"
	}

	# Create Certificate
	$certificate = New-SelfSignedCertificate @createCertificateSplat

	# Get Certificate Path
	$certificatePath = Join-Path -Path $certStoreLocation -ChildPath $certificate.Thumbprint

	# Generate A Password
	# This only contains numeric and alphanumeroc characters and not any special characters to prevent issues when uploading the private keys and certs.
	# The max length of the generated password depends on the number of available characters, hence repeating the list of characters to allow very long passwords if needed
	$pfxPassword = $(-join (48..57+65..90+97..122+48..57+65..90+97..122+48..57+65..90+97..122+48..57+65..90+97..122 | ForEach-Object {[char]$_} | Get-Random -Count 64))
	$pfxPasswordSecure = $(ConvertTo-SecureString $pfxPassword -AsPlainText -Force)

	# Export Certificate Without Private Key
	Export-Certificate -Cert $certificatePath -FilePath $cerOutputPath | Out-Null
	Export-PfxCertificate -Cert $certificatePath -FilePath $pfxOutputPath -Password $pfxPasswordSecure | Out-Null

	# Deleting The Private Key And Certificate
	Set-Location $certStoreLocation
	Get-ChildItem $($certificate.Thumbprint) | Remove-Item -DeleteKey -ErrorAction SilentlyContinue | Out-Null

	# Displaying The File Paths Of The Exported CER/PFX File
	Write-Host ""
	Write-Host " > File Path For Exported CER File...: '$cerOutputPath'" -ForegroundColor Yellow
	Write-Host " > File Path For Exported PFX File...: '$pfxOutputPath'" -ForegroundColor Yellow

	# Displaying The Password For 60 Seconds. After That The Screen And The Variables Are Cleared
	Write-Host ""
	Write-Host "! ! ! Store The Password Of The Private Key In A Safe Location ! ! !" -ForegroundColor Red
	Write-Host "WARNING: In 60 Seconds The Screen And Variables Will Be Cleared. Copy The Password Value A.S.A.P.!!!" -ForegroundColor White
	Write-Host " > 'PFX Password'.................: '$pfxPassword'" -ForegroundColor Yellow
	Write-Host "! ! ! Store The Password Of The Private Key In A Safe Location ! ! !" -ForegroundColor Red
	Write-Host ""
	Start-Sleep -s 60
	$certificate = $null
	$pfxPassword = $null
	$pfxPasswordSecure = $null
	Set-Location C:\
	Clear-Host
}
Figure 1: Creating A Self-Signed Certificate And Exporting It
– 

Now we can create the application, configure the required permissions, consent those and upload the certificate to the registered application. Consenting will be done in a semi-automated manner using the device code flow. Run this code in the previous PowerShell window where you already connected to Azure AD.

Invoke-Command -ScriptBlock {
	Function retrieveTenantIDFromTenantFDQN () {
		Param (
			[string]$tenantFQDN
		)

		# Specify The Tenant Specific Discovery Endpoint URL
		$oidcConfigDiscoveryURL = $null
		$oidcConfigDiscoveryURL = "https://login.microsoftonline.com/$tenantFQDN/v2.0/.well-known/openid-configuration"
		$oidcConfigDiscoveryResult = $null

		# Retrieve The Information From The Discovery Endpoint URL
		$tenantID = $null
		$oidcConfigDiscoveryResult = $null
		Try {
			$oidcConfigDiscoveryResult = Invoke-RestMethod -Uri $oidcConfigDiscoveryURL -ErrorAction Stop
		}
		Catch {
			# Placeholder
		}

		# If There Is A Result Determine The Tenant ID
		If ($null -ne $oidcConfigDiscoveryResult) {
			$tenantID = $oidcConfigDiscoveryResult.authorization_endpoint.Split("/")[3]
		}

		Return $tenantID
	}
	
	Clear-Host

	# Load Assembly To Use The URLEncode Function
	Add-Type -AssemblyName System.Web

	# Load Assembly To Use MessageBox
	Add-Type -assemblyName PresentationFramework

	# Generic Details
	$msftGraphFQDN = "graph.microsoft.com" # FQDN For Microsoft Graph

	# Tenant Details
	$tenantFQDN = "<SPECFIFY YOUR TENANT FQDN>" # e.g. "<TENANT NAME>.ONMICROSOFT.COM"
	$tenantID = retrieveTenantIDFromTenantFDQN -tenantFQDN $tenantFQDN
	$tenantName = "<SPECFIFY YOUR TENANT NAME>"

	# Where To Import The Certificate From
	$subjectName = "<SPECFIFY THE SUBJECT NAME FOR THE CERTIFICATE>" # e.g. "mgmt-Admin-Units-MSFT-Graph"
	$exportFolderPath = "<Folder Path To Export The Certificate Data To>" # e.g. "C:\TEMP"
	$cerOutputPath = Join-Path $exportFolderPath "$subjectName.cer"
	$pfxOutputPath = Join-Path $exportFolderPath "$subjectName.pfx"

	# Azure AD Device Code Request Endpoint URL
	$aadDeviceCodeRequestEndpointURL = "https://login.microsoftonline.com/$tenantID/oauth2/devicecode"

	# Device Code Approval Endpoint URL
	$deviceCodeApprovalEndpointURL = "https://www.microsoft.com/devicelogin"

	# Application Details
	$msftGraphMgmtAppDisplayName = "<SPECIFY THE APPLICATION DISPLAY NAME OF THE REGISTERED APPLICATION>" # Example: "<TENANT NAME>: Mgmt App - Managing Automatic AU Assignments"
	$msftGraphMgmtAppIdentifierURI = "https://$tenantName.onmicrosoft.com/$($msftGraphMgmtAppDisplayName.Replace(" ","-").Replace("---","-").Replace(":-","-"))"
	$msftGraphMgmtAppReplyURL = "https://$($msftGraphMgmtAppDisplayName.Replace(" ","-").Replace("---","-").Replace(":-","-"))"

	# Required Resource Access, In Other Words The Required Permissions
	$requiredResourceAccessPSObjectListMSFTGraphMgmtApp = @(
		[PSCustomObject]@{
			resourceAppId = "00000003-0000-0000-c000-000000000000"	# MSFT Graph => (Get-AzureADServicePrincipal -filter "DisplayName eq 'Microsoft Graph'")
			resourceAccess = @(
				@{
					id = "5eb59dd3-1da2-4329-8733-9dabdc435916"		# AdministrativeUnit.ReadWrite.All => (Get-AzureADServicePrincipal -filter "DisplayName eq 'Microsoft Graph'").AppRoles | ?{$_.Value -eq "AdministrativeUnit.ReadWrite.All"}
					type = "Role"
				},
				@{
					id = "5b567255-7703-4780-807c-7be8301ae99b"		# Group.Read.All => (Get-AzureADServicePrincipal -filter "DisplayName eq 'Microsoft Graph'").AppRoles | ?{$_.Value -eq "Group.Read.All"}
					type = "Role"
				},
				@{
					id = "df021288-bdef-4463-88db-98f22de89214"		# User.Read.All => (Get-AzureADServicePrincipal -filter "DisplayName eq 'Microsoft Graph'").AppRoles | ?{$_.Value -eq "User.Read.All"}
					type = "Role"
				}
			)
		}
	)
	$requiredResourceAccessListMSFTGraphMgmtApp = @()
	ForEach($resourceApp in $requiredResourceAccessPSObjectListMSFTGraphMgmtApp) {
		$requiredResourceAccess = New-Object -TypeName "Microsoft.Open.AzureAD.Model.RequiredResourceAccess"
		$requiredResourceAccess.ResourceAppId = $resourceApp.resourceAppId
		ForEach($resourceAccess in $resourceApp.resourceAccess) {
			$requiredResourceAccess.resourceAccess += New-Object -TypeName "Microsoft.Open.AzureAD.Model.ResourceAccess" -ArgumentList $resourceAccess.Id,$resourceAccess.type
		}
		$requiredResourceAccessListMSFTGraphMgmtApp += $requiredResourceAccess
	}

	# Creating The App Registration
	$msftGraphMgmtApp = New-AzureADApplication -DisplayName $msftGraphMgmtAppDisplayName -IdentifierUris $msftGraphMgmtAppIdentifierURI -ReplyUrls @($msftGraphMgmtAppReplyURL) -RequiredResourceAccess $requiredResourceAccessListMSFTGraphMgmtApp
	$msftGraphMgmtAppObjectID = $msftGraphMgmtApp.ObjectID
	$msftGraphMgmtAppApplicationID = $msftGraphMgmtApp.AppId
	Start-Sleep -s 10

	# Creating The Service Principal
	$msftGraphMgmtSvcPrinc = New-AzureADServicePrincipal -DisplayName $msftGraphMgmtAppDisplayName -AppId $msftGraphMgmtAppApplicationID -AccountEnabled $true -AppRoleAssignmentRequired $false
	$msftGraphMgmtSvcPrincObjectID = $msftGraphMgmtSvcPrinc.ObjectID
	$msftGraphMgmtSvcPrincApplicationID = $msftGraphMgmtSvcPrinc.AppId
	Start-Sleep -s 10

	# Uploading The Certificate To The Application Registration
	$msftGraphMgmtCert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2
	$msftGraphMgmtCert.Import($cerOutputPath)
	$msftGraphMgmtCertRawData = $msftGraphMgmtCert.GetRawCertData()
	$msftGraphMgmtCertCERBase64 = [System.Convert]::ToBase64String($msftGraphMgmtCertRawData) # Base64 Encode The Public Key
	$msftGraphMgmtCertHash = $msftGraphMgmtCert.GetCertHash() # Get The Custom Key Identifier
	$msftGraphMgmtCertCustomKeyIdentifier = [System.Convert]::ToBase64String($msftGraphMgmtCertHash)
	$msftGraphMgmtCertNotBeforeISO8601Format = (Get-Date $($msftGraphMgmtCert.NotBefore)).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ") # Date In ISO8601Format
	$msftGraphMgmtCertNotAfterISO8601Format = (Get-Date $($msftGraphMgmtCert.NotAfter)).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ") # Date In ISO8601Format
	New-AzureADApplicationKeyCredential -ObjectId $msftGraphMgmtAppObjectID -Type AsymmetricX509Cert -Usage Verify -CustomKeyIdentifier $msftGraphMgmtCertCustomKeyIdentifier  -Value $msftGraphMgmtCertCERBase64 -StartDate $msftGraphMgmtCertNotBeforeISO8601Format -EndDate $msftGraphMgmtCertNotAfterISO8601Format | Out-Null

	# Grant Consent To The Required Permissions - Build The Request Body To Request A Device/User Code From MSFT Graph
	$msftGraphDeviceCodeRequestBody = @()
	$msftGraphDeviceCodeRequestBody += "resource=$([System.Web.HttpUtility]::UrlEncode($('https://' + $msftGraphFQDN + '/')))"
	$msftGraphDeviceCodeRequestBody += "&client_id=$msftGraphMgmtAppApplicationID"
	$deviceTokenRequestResponse = Invoke-RestMethod -uri $aadDeviceCodeRequestEndpointURL -ContentType "application/x-www-form-urlencoded" -Method POST -Body $msftGraphDeviceCodeRequestBody -ErrorAction Stop
	$msftGraphDeviceCodeResponseUserCode = $deviceTokenRequestResponse.user_code

	# Grant Consent To The Required Permissions - Put The User Code In The Clipboard To Paste It Later As Needed
	Set-Clipboard -Value $msftGraphDeviceCodeResponseUserCode

	# Grant Consent To The Required Permissions - Present A Notification On What To Do Next
	[System.Windows.MessageBox]::Show("A device code has been requested from Azure AD. For this device code the corresponding user code ($msftGraphDeviceCodeResponseUserCode) has been copied to the clipboard. After clicking [OK] an authentication screen will be opened. On that new screen just paste the user code by pressing [CTRL]+[V] and then click [NEXT] to authenticate with your Global Admin Credentials for the AAD Tenant:`n`nTenant FQDN....: '$tenantFQDN'`nTenant ID..........: '$tenantID'`nUser Code.........: '$msftGraphDeviceCodeResponseUserCode'", "Device Code Authentication - Please Read Carefully", 0, 64) | Out-Null

	# Grant Consent To The Required Permissions - Navigate To The Device Approcal URL For The Actual Consent
	[System.Diagnostics.Process]::Start($deviceCodeApprovalEndpointURL)
}
Figure 2: After Requesting The Device Code And User Code To Consent The Application Permissions
– 
Figure 3: Specifying The User Code To Continue Authentication Followed By Consent Of Application Permissions
– 
Figure 4: Signing-In Before Consenting The Required Permissions For The Registered App
– 
Figure 5: Reviewing The Required Permissions For The Registered App
– 
Figure 6: Reviewing The Required Permissions For The Registered App
Figure 7: The Configured Certificate For The Registered App Managing The Automatic AU Assignments
Figure 8: The Configured And Consented Permissions For The Registered App Managing The Automatic AU Assignments

To be continued in PART 5 of this series.

Cheers,

Jorge

————————————————————————————————————————————————————-
This posting is provided “AS IS” with no warranties and confers no rights!
Always evaluate/test everything yourself first before using/implementing this in production!
This is today’s opinion/technology, it might be different tomorrow and will definitely be different in 10 years!
DISCLAIMER: https://jorgequestforknowledge.wordpress.com/disclaimer/

————————————————————————————————————————————————————-
########################### IAMTEC | Jorge’s Quest For Knowledge ##########################
#################### http://JorgeQuestForKnowledge.wordpress.com/ ###################

————————————————————————————————————————————————————

IAMTEC

Identity | Security | Recovery

————————————————————————————————————————————————————-

2 Responses to “(2021-10-18) Azure AD Administrative Units – Dynamically Managing AU Assignments – Part 4”

  1. […] « (2021-10-18) Azure AD Administrative Units – Dynamically Managing AU Assignments – Part&… […]

    Liked by 1 person

  2. […] Azure AD Administrative Units – Dynamically Managing AU Assignments – Part 2 (2021-10-18) Azure AD Administrative Units – Dynamically Managing AU Assignments – Part&… […]

    Liked by 1 person

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

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

 
%d bloggers like this: