Importing users to Azure AD B2C rapidly using the batch endpoint

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 πŸ™‚

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 )

Google photo

You are commenting using your Google 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