Just migrated in total 4 million accounts to Azure AD B2C, and that required a few tricks in order to manage in time. The batch endpoint helped out nicely, and here is a quick a easy module you can use.
The first is the module, which is very straight forward. It uses client credential grant, that you can of course replace. The important thing is that it provides a function called “Invoke-GraphBatchRequest” that takes requests as pipeline input, and creates batches out of them. If you feed millions of users, this method also automatically refreshes the access token for you. Loads of room for improvement here, such as using refresh token etc. π
This is GraphBatch.psm1:
New-Variable -Scope Script -Name TenantID -Value $null -Force New-Variable -Scope Script -Name Credential -Value $null -Force New-Variable -Scope Script -Name AccessToken -Value $null -Force New-Variable -Scope Script -Name AccessTokenExpires -Value $null -Force function Set-GraphBatchAuthInfo { [CmdletBinding()] Param ( [Parameter(Mandatory=$true,Position=0)] [String] $TenantID, [Parameter(Mandatory=$true,Position=1)] [String] $ClientID, # The clientid [Parameter(Mandatory=$true,Position=2)] [SecureString] $Secret ) Process { $Script:TenantID = $TenantID $Script:Credential = [PSCredential]::new($ClientID, $Secret) } } function Get-GraphBatchAccessToken { [CmdletBinding()] Param () Process { if($Script:AccessTokenExpires -and $Script:AccessTokenExpires -gt (get-date)) { return $Script:AccessToken } $token = Invoke-RestMethod -Uri "https://login.microsoftonline.com/$($Script:TenantID)/oauth2/v2.0/token" -Method Post -Body "client_id=$($Script:Credential.Username)&grant_type=client_credentials&scope=https%3A%2F%2Fgraph.microsoft.com%2F.default&client_secret=$($Script:Credential.GetNetworkCredential().Password)" -ErrorAction Stop -ContentType "application/x-www-form-urlencoded" $Script:AccessToken = $token.access_token $Script:accessTokenExpires = (get-date).AddSeconds($token.expires_in - 120) return $token.access_token } } function Process-GraphBatch { [CmdletBinding()] Param ( [Parameter(Mandatory=$true,ValueFromPipeline=$true,Position=0)] [System.Collections.ArrayList] $Batch ) Process { $accessToken = Get-GraphBatchAccessToken Write-Verbose "Sending batch of $($Batch.count) requests" $batchBody = @{requests = $Batch} | ConvertTo-Json -Depth 7 Write-Debug $batchBody $r = Invoke-RestMethod "https://graph.microsoft.com/v1.0/`$batch" -Method POST -ContentType "application/json" -Body $batchBody -Headers @{Authorization = "Bearer $accessToken"} $r.responses } } function Invoke-GraphBatchRequest { [CmdletBinding()] Param ( [Parameter(Mandatory=$true,ValueFromPipeline=$true,Position=0)] [System.Collections.Hashtable] $Request ) Begin { $accessToken = Get-GraphBatchAccessToken $Batch = New-Object System.Collections.ArrayList } Process { if($Request.Method -in "Get","Delete") { $Batch.Add( @{ id = "{0}" -f ($Batch.Count + 1) method = $Request.Method.ToUpper() url = $Request.Url } ) | Out-Null } else { $Batch.Add( @{ id = "{0}" -f ($Batch.Count + 1) method = $Request.Method.ToUpper() url = $Request.Url body = $Request.Body "headers" = @{ "Content-Type" = "application/json" } } ) | Out-Null } # If there are 20 elements in the list, process the batch as this is the max number of requests per batch if($Batch.Count -eq 20) { Process-GraphBatch $Batch $Batch.Clear() } } End { if($Batch.Count -gt 0) { # Process the last page of requests Process-GraphBatch $Batch } } } Export-ModuleMember "Set-GraphBatchAuthInfo","Get-GraphBatchAccessToken", "Invoke-GraphBatchRequest"
And this is run.ps1:
Import-Module .\GraphBatch.psm1 -Force # Set auth info. This will be stored in the module and used when refershing access token Set-GraphBatchAuthInfo -TenantID "caa6370e-34fa-4a95-a8d8-895f1b39ed59" -ClientID "3ad4efc7-c0b9-49fa-833b-d918ea225e52" -Secret ("topSecret!" | ConvertTo-SecureString -AsPlainText -Force) # Example bear minimum GET request $request = @{ Method = "GET" Url = "/users" } # Send ONE request $demo1 = $request | Invoke-GraphBatchRequest -Verbose # Send TWO requests $demo2 = $request, $request | Invoke-GraphBatchRequest -Verbose # Feed 10 000 users into Azure AD B2C: $Result = 1..10000 | Foreach { @{ Method = "POST" Url = "/users" Body = @{ displayName = "Disp $($_)" identities = [System.Collections.ArrayList]::new(@( @{ signInType = "emailAddress" issuer = "gwb2c.onmicrosoft.com" issuerAssignedId = "$($_)@example.com" } )) passwordProfile = @{ password = "Rnd123!$([guid]::newguid())" forceChangePasswordNextSignIn = $false } } } } | Invoke-GraphBatchRequest -Verbose
Based on this, you should be able to modify the Body of the requests to suit your needs – such as adding additional attributes (or you know, not random strings are email addresses).
Hope this helps as a reference to someone π