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 π