Configure cross-tenant synchronization using PowerShell or Microsoft Graph API

This article describes the key steps to configure cross-tenant synchronization using Microsoft Graph PowerShell or Microsoft Graph API. When configured, Microsoft Entra ID automatically provisions and de-provisions B2B users in your target tenant. For detailed steps using the Microsoft Entra admin center, see Configure cross-tenant synchronization.

Diagram that shows cross-tenant synchronization between source tenant and target tenant.

Prerequisites

Icon for the source tenant.
Source tenant

Icon for the target tenant.
Target tenant

Step 1: Sign in to the target tenant

Icon for the target tenant.
Target tenant

  1. Start PowerShell.

  2. If necessary, install the Microsoft Graph PowerShell SDK.

  3. Get the tenant ID of the source and target tenants and initialize variables.

    $SourceTenantId = "<SourceTenantId>"
    $TargetTenantId = "<TargetTenantId>"
    
  4. Use the Connect-MgGraph command to sign in to the target tenant and consent to the following required permissions.

    • Policy.Read.All
    • Policy.ReadWrite.CrossTenantAccess
    Connect-MgGraph -TenantId $TargetTenantId -Scopes "Policy.Read.All","Policy.ReadWrite.CrossTenantAccess"
    

Step 2: Enable user synchronization in the target tenant

Icon for the target tenant.
Target tenant

  1. In the target tenant, use the New-MgPolicyCrossTenantAccessPolicyPartner command to create a new partner configuration in a cross-tenant access policy between the target tenant and the source tenant. Use the source tenant ID in the request.

    If you get the error New-MgPolicyCrossTenantAccessPolicyPartner_Create: Another object with the same value for property tenantId already exists, you might already have an existing configuration. For more information, see Symptom - New-MgPolicyCrossTenantAccessPolicyPartner_Create error.

    $Params = @{
        TenantId = $SourceTenantId
    }
    New-MgPolicyCrossTenantAccessPolicyPartner -BodyParameter $Params | Format-List
    
    AutomaticUserConsentSettings : Microsoft.Graph.PowerShell.Models.MicrosoftGraphInboundOutboundPolicyConfiguration
    B2BCollaborationInbound      : Microsoft.Graph.PowerShell.Models.MicrosoftGraphCrossTenantAccessPolicyB2BSetting
    B2BCollaborationOutbound     : Microsoft.Graph.PowerShell.Models.MicrosoftGraphCrossTenantAccessPolicyB2BSetting
    B2BDirectConnectInbound      : Microsoft.Graph.PowerShell.Models.MicrosoftGraphCrossTenantAccessPolicyB2BSetting
    B2BDirectConnectOutbound     : Microsoft.Graph.PowerShell.Models.MicrosoftGraphCrossTenantAccessPolicyB2BSetting
    IdentitySynchronization      : Microsoft.Graph.PowerShell.Models.MicrosoftGraphCrossTenantIdentitySyncPolicyPartner
    InboundTrust                 : Microsoft.Graph.PowerShell.Models.MicrosoftGraphCrossTenantAccessPolicyInboundTrust
    IsServiceProvider            :
    TenantId                     : <SourceTenantId>
    TenantRestrictions           : Microsoft.Graph.PowerShell.Models.MicrosoftGraphCrossTenantAccessPolicyTenantRestrictions
    AdditionalProperties         : {[@odata.context, https://graph.microsoft.com/v1.0/$metadata#policies/crossTenantAccessPolicy/partners/$entity],
                                   [crossCloudMeetingConfiguration,
                                   System.Collections.Generic.Dictionary`2[System.String,System.Object]], [protectedContentSharing,
                                   System.Collections.Generic.Dictionary`2[System.String,System.Object]]}
    
  2. Use the Invoke-MgGraphRequest command to enable user synchronization in the target tenant.

    If you get an Request_MultipleObjectsWithSameKeyValue error, you might already have an existing policy. For more information, see Symptom - Request_MultipleObjectsWithSameKeyValue error.

    $Params = @{
        userSyncInbound = @{
            isSyncAllowed = $true
        }
    }
    Invoke-MgGraphRequest -Method PUT -Uri "https://graph.microsoft.com/v1.0/policies/crossTenantAccessPolicy/partners/$SourceTenantId/identitySynchronization" -Body $Params
    
  3. Use the Get-MgPolicyCrossTenantAccessPolicyPartnerIdentitySynchronization command to verify IsSyncAllowed is set to True.

    (Get-MgPolicyCrossTenantAccessPolicyPartnerIdentitySynchronization -CrossTenantAccessPolicyConfigurationPartnerTenantId $SourceTenantId).UserSyncInbound
    
    IsSyncAllowed
    -------------
    True
    

Step 3: Automatically redeem invitations in the target tenant

Icon for the target tenant.
Target tenant

  1. In the target tenant, use the Update-MgPolicyCrossTenantAccessPolicyPartner command to automatically redeem invitations and suppress consent prompts for inbound access.

    $AutomaticUserConsentSettings = @{
        "InboundAllowed"="True"
    }
    Update-MgPolicyCrossTenantAccessPolicyPartner -CrossTenantAccessPolicyConfigurationPartnerTenantId $SourceTenantId -AutomaticUserConsentSettings $AutomaticUserConsentSettings
    

Step 4: Sign in to the source tenant

Icon for the source tenant.
Source tenant

  1. Start an instance of PowerShell.

  2. Get the tenant ID of the source and target tenants and initialize variables.

    $SourceTenantId = "<SourceTenantId>"
    $TargetTenantId = "<TargetTenantId>"
    
  3. Use the Connect-MgGraph command to sign in to the source tenant and consent to the following required permissions.

    • Policy.Read.All
    • Policy.ReadWrite.CrossTenantAccess
    • Application.ReadWrite.All
    • Directory.ReadWrite.All
    • AuditLog.Read.All
    Connect-MgGraph -TenantId $SourceTenantId -Scopes "Policy.Read.All","Policy.ReadWrite.CrossTenantAccess","Application.ReadWrite.All","Directory.ReadWrite.All","AuditLog.Read.All"
    

Step 5: Automatically redeem invitations in the source tenant

Icon for the source tenant.
Source tenant

  1. In the source tenant, use the New-MgPolicyCrossTenantAccessPolicyPartner command to create a new partner configuration in a cross-tenant access policy between the source tenant and the target tenant. Use the target tenant ID in the request.

    If you get the error New-MgPolicyCrossTenantAccessPolicyPartner_Create: Another object with the same value for property tenantId already exists, you might already have an existing configuration. For more information, see Symptom - New-MgPolicyCrossTenantAccessPolicyPartner_Create error.

    $Params = @{
        TenantId = $TargetTenantId
    }
    New-MgPolicyCrossTenantAccessPolicyPartner -BodyParameter $Params | Format-List
    
    AutomaticUserConsentSettings : Microsoft.Graph.PowerShell.Models.MicrosoftGraphInboundOutboundPolicyConfiguration
    B2BCollaborationInbound      : Microsoft.Graph.PowerShell.Models.MicrosoftGraphCrossTenantAccessPolicyB2BSetting
    B2BCollaborationOutbound     : Microsoft.Graph.PowerShell.Models.MicrosoftGraphCrossTenantAccessPolicyB2BSetting
    B2BDirectConnectInbound      : Microsoft.Graph.PowerShell.Models.MicrosoftGraphCrossTenantAccessPolicyB2BSetting
    B2BDirectConnectOutbound     : Microsoft.Graph.PowerShell.Models.MicrosoftGraphCrossTenantAccessPolicyB2BSetting
    IdentitySynchronization      : Microsoft.Graph.PowerShell.Models.MicrosoftGraphCrossTenantIdentitySyncPolicyPartner
    InboundTrust                 : Microsoft.Graph.PowerShell.Models.MicrosoftGraphCrossTenantAccessPolicyInboundTrust
    IsServiceProvider            :
    TenantId                     : <TargetTenantId>
    TenantRestrictions           : Microsoft.Graph.PowerShell.Models.MicrosoftGraphCrossTenantAccessPolicyTenantRestrictions
    AdditionalProperties         : {[@odata.context, https://graph.microsoft.com/v1.0/$metadata#policies/crossTenantAccessPolicy/partners/$entity],
                                   [crossCloudMeetingConfiguration,
                                   System.Collections.Generic.Dictionary`2[System.String,System.Object]], [protectedContentSharing,
                                   System.Collections.Generic.Dictionary`2[System.String,System.Object]]}
    
    
  2. Use the Update-MgPolicyCrossTenantAccessPolicyPartner command to automatically redeem invitations and suppress consent prompts for outbound access.

    $AutomaticUserConsentSettings = @{
        "OutboundAllowed"="True"
    }
    Update-MgPolicyCrossTenantAccessPolicyPartner -CrossTenantAccessPolicyConfigurationPartnerTenantId $TargetTenantId -AutomaticUserConsentSettings $AutomaticUserConsentSettings
    

Step 6: Create a configuration application in the source tenant

Icon for the source tenant.
Source tenant

  1. In the source tenant, use the Invoke-MgInstantiateApplicationTemplate command to add an instance of a configuration application from the Microsoft Entra application gallery into your tenant.

    Invoke-MgInstantiateApplicationTemplate -ApplicationTemplateId "518e5f48-1fc8-4c48-9387-9fdf28b0dfe7" -DisplayName "Fabrikam"
    
  2. Use the Get-MgServicePrincipal command to get the service principal ID and app role ID.

    Get-MgServicePrincipal -Filter "DisplayName eq 'Fabrikam'" | Format-List
    
    AccountEnabled                      : True
    AddIns                              : {}
    AlternativeNames                    : {}
    AppDescription                      :
    AppDisplayName                      : Fabrikam
    AppId                               : <AppId>
    AppManagementPolicies               :
    AppOwnerOrganizationId              : <AppOwnerOrganizationId>
    AppRoleAssignedTo                   :
    AppRoleAssignmentRequired           : True
    AppRoleAssignments                  :
    AppRoles                            : {<AppRoleId>}
    ApplicationTemplateId               : 518e5f48-1fc8-4c48-9387-9fdf28b0dfe7
    ClaimsMappingPolicies               :
    CreatedObjects                      :
    CustomSecurityAttributes            : Microsoft.Graph.PowerShell.Models.MicrosoftGraphCustomSecurityAttributeValue
    DelegatedPermissionClassifications  :
    DeletedDateTime                     :
    Description                         :
    DisabledByMicrosoftStatus           :
    DisplayName                         : Fabrikam
    Endpoints                           :
    ErrorUrl                            :
    FederatedIdentityCredentials        :
    HomeRealmDiscoveryPolicies          :
    Homepage                            : https://account.activedirectory.windowsazure.com:444/applications/default.aspx?metadata=aad2aadsync|ISV9.1|primary|z
    Id                                  : <ServicePrincipalId>
    Info                                : Microsoft.Graph.PowerShell.Models.MicrosoftGraphInformationalUrl
    KeyCredentials                      : {}
    LicenseDetails                      :
    
    ...
    
  3. Initialize a variable for the service principal ID.

    Be sure to use the service principal ID instead of the application ID.

    $ServicePrincipalId = "<ServicePrincipalId>"
    
  4. Initialize a variable for the app role ID.

    $AppRoleId= "<AppRoleId>"
    

Step 7: Test the connection to the target tenant

Icon for the source tenant.
Source tenant

  1. In the source tenant, use the Invoke-MgGraphRequest command to test the connection to the target tenant and validate the credentials.

    $Params = @{
        "useSavedCredentials" = $false
        "templateId" = "Azure2Azure"
        "credentials" = @(
            @{
                "key" = "CompanyId"
                "value" = $TargetTenantId
            }
            @{
                "key" = "AuthenticationType"
                "value" = "SyncPolicy"
            }
        )
    }
    Invoke-MgGraphRequest -Method POST -Uri "https://graph.microsoft.com/v1.0/servicePrincipals/$ServicePrincipalId/synchronization/jobs/validateCredentials" -Body $Params
    

Step 8: Create a provisioning job in the source tenant

Icon for the source tenant.
Source tenant

In the source tenant, to enable provisioning, create a provisioning job.

  1. Determine the synchronization template to use, such as Azure2Azure.

    A template has pre-configured synchronization settings.

  2. In the source tenant, use the New-MgServicePrincipalSynchronizationJob command to create a provisioning job based on a template.

    New-MgServicePrincipalSynchronizationJob -ServicePrincipalId $ServicePrincipalId -TemplateId "Azure2Azure" | Format-List
    
    Id                         : <JobId>
    Schedule                   : Microsoft.Graph.PowerShell.Models.MicrosoftGraphSynchronizationSchedule
    Schema                     : Microsoft.Graph.PowerShell.Models.MicrosoftGraphSynchronizationSchema
    Status                     : Microsoft.Graph.PowerShell.Models.MicrosoftGraphSynchronizationStatus
    SynchronizationJobSettings : {AzureIngestionAttributeOptimization, LookaheadQueryEnabled}
    TemplateId                 : Azure2Azure
    AdditionalProperties       : {[@odata.context, https://graph.microsoft.com/v1.0/$metadata#servicePrincipals('<ServicePrincipalId>')/synchro
                                 nization/jobs/$entity]}
    
  3. Initialize a variable for the job ID.

    $JobId = "<JobId>"
    

Step 9: Save your credentials

Icon for the source tenant.
Source tenant

  1. In the source tenant, use the Invoke-MgGraphRequest command to save your credentials.

    $Params = @{
        "value" = @(
            @{
                "key" = "AuthenticationType"
                "value" = "SyncPolicy"
            }
            @{
                "key" = "CompanyId"
                "value" = $TargetTenantId
            }
        )
    }
    Invoke-MgGraphRequest -Method PUT -Uri "https://graph.microsoft.com/v1.0/servicePrincipals/$ServicePrincipalId/synchronization/secrets" -Body $Params
    

Step 10: Assign a user to the configuration

Icon for the source tenant.
Source tenant

For cross-tenant synchronization to work, at least one internal user must be assigned to the configuration.

  1. In the source tenant, use the New-MgServicePrincipalAppRoleAssignedTo command to assign an internal user to the configuration.

    $Params = @{
        PrincipalId = "<PrincipalId>"
        ResourceId = $ServicePrincipalId
        AppRoleId = $AppRoleId
    }
    New-MgServicePrincipalAppRoleAssignedTo -ServicePrincipalId $ServicePrincipalId -BodyParameter $Params | Format-List
    
    AppRoleId            : <AppRoleId>
    CreatedDateTime      : 7/31/2023 10:27:12 PM
    DeletedDateTime      :
    Id                   : <Id>
    PrincipalDisplayName : User1
    PrincipalId          : <PrincipalId>
    PrincipalType        : User
    ResourceDisplayName  : Fabrikam
    ResourceId           : <ServicePrincipalId>
    AdditionalProperties : {[@odata.context, https://graph.microsoft.com/v1.0/$metadata#appRoleAssignments/$entity]}
    

Step 11: Test provision on demand

Icon for the source tenant.
Source tenant

Now that you have a configuration, you can test on-demand provisioning with one of your users.

  1. In the source tenant, use the Get-MgServicePrincipalSynchronizationJobSchema command to get the schema rule ID.

    $SynchronizationSchema = Get-MgServicePrincipalSynchronizationJobSchema -ServicePrincipalId $ServicePrincipalId -SynchronizationJobId $JobId
    $SynchronizationSchema.SynchronizationRules | Format-List
    
    ContainerFilter      : Microsoft.Graph.PowerShell.Models.MicrosoftGraphContainerFilter
    Editable             : True
    GroupFilter          : Microsoft.Graph.PowerShell.Models.MicrosoftGraphGroupFilter
    Id                   : <RuleId>
    Metadata             : {defaultSourceObjectMappings, supportsProvisionOnDemand}
    Name                 : USER_INBOUND_USER
    ObjectMappings       : {Provision Azure Active Directory Users, , , …}
    Priority             : 1
    SourceDirectoryName  : Azure Active Directory
    TargetDirectoryName  : Azure Active Directory (target tenant)
    AdditionalProperties : {}
    
  2. Initialize a variable for the rule ID.

    $RuleId = "<RuleId>"
    
  3. Use the New-MgServicePrincipalSynchronizationJobOnDemand command to provision a test user on demand.

    $Params = @{
        Parameters = @(
            @{
                Subjects = @(
                    @{
                        ObjectId = "<UserObjectId>"
                        ObjectTypeName = "User"
                    }
                )
                RuleId = $RuleId
            }
        )
    }
    New-MgServicePrincipalSynchronizationJobOnDemand -ServicePrincipalId $ServicePrincipalId -SynchronizationJobId $JobId -BodyParameter $Params | Format-List
    
    Key                  : Microsoft.Identity.Health.CPP.Common.DataContracts.SyncFabric.StatusInfo
    Value                : [{"provisioningSteps":[{"name":"EntryImport","type":"Import","status":"Success","description":"Retrieved User
                           'user1@fabrikam.com' from Azure Active Directory","timestamp":"2023-07-31T22:31:15.9116590Z","details":{"objectId":
                           "<UserObjectId>","accountEnabled":"True","displayName":"User1","mailNickname":"user1","userPrincipalName":"use
                           ...
    AdditionalProperties : {[@odata.context, https://graph.microsoft.com/v1.0/$metadata#microsoft.graph.stringKeyStringValuePair]}
    

Step 12: Start the provisioning job

Icon for the source tenant.
Source tenant

  1. Now that the provisioning job is configured, in the source tenant, use the Start-MgServicePrincipalSynchronizationJob command to start the provisioning job.

    Start-MgServicePrincipalSynchronizationJob -ServicePrincipalId $ServicePrincipalId -SynchronizationJobId $JobId
    

Step 13: Monitor provisioning

Icon for the source tenant.
Source tenant

  1. Now that the provisioning job is running, in the source tenant, use the Get-MgServicePrincipalSynchronizationJob command to monitor the progress of the current provisioning cycle as well as statistics to date such as the number of users and groups that have been created in the target system.

    Get-MgServicePrincipalSynchronizationJob -ServicePrincipalId $ServicePrincipalId -SynchronizationJobId $JobId | Format-List
    
    Id                         : <JobId>
    Schedule                   : Microsoft.Graph.PowerShell.Models.MicrosoftGraphSynchronizationSchedule
    Schema                     : Microsoft.Graph.PowerShell.Models.MicrosoftGraphSynchronizationSchema
    Status                     : Microsoft.Graph.PowerShell.Models.MicrosoftGraphSynchronizationStatus
    SynchronizationJobSettings : {AzureIngestionAttributeOptimization, LookaheadQueryEnabled}
    TemplateId                 : Azure2Azure
    AdditionalProperties       : {[@odata.context, https://graph.microsoft.com/v1.0/$metadata#servicePrincipals('<ServicePrincipalId>')/synchro
                                 nization/jobs/$entity]}
    
  2. In addition to monitoring the status of the provisioning job, use the Get-MgAuditLogProvisioning command to retrieve the provisioning logs and get all the provisioning events that occur. For example, query for a particular user and determine if they were successfully provisioned.

    Get-MgAuditLogDirectoryAudit | Select -First 10 | Format-List
    
    ActivityDateTime     : 7/31/2023 12:08:17 AM
    ActivityDisplayName  : Export
    AdditionalDetails    : {Details, ErrorCode, EventName, ipaddr...}
    Category             : ProvisioningManagement
    CorrelationId        : cc519f3b-fb72-4ea2-9b7b-8f9dc271c5ec
    Id                   : Sync_cc519f3b-fb72-4ea2-9b7b-8f9dc271c5ec_L5BFV_161778479
    InitiatedBy          : Microsoft.Graph.PowerShell.Models.MicrosoftGraphAuditActivityInitiator1
    LoggedByService      : Account Provisioning
    OperationType        :
    Result               : success
    ResultReason         : User 'user2@fabrikam.com' was created in Azure Active Directory (target tenant)
    TargetResources      : {<ServicePrincipalId>, }
    AdditionalProperties : {}
    
    ActivityDateTime     : 7/31/2023 12:08:17 AM
    ActivityDisplayName  : Export
    AdditionalDetails    : {Details, ErrorCode, EventName, ipaddr...}
    Category             : ProvisioningManagement
    CorrelationId        : cc519f3b-fb72-4ea2-9b7b-8f9dc271c5ec
    Id                   : Sync_cc519f3b-fb72-4ea2-9b7b-8f9dc271c5ec_L5BFV_161778264
    InitiatedBy          : Microsoft.Graph.PowerShell.Models.MicrosoftGraphAuditActivityInitiator1
    LoggedByService      : Account Provisioning
    OperationType        :
    Result               : success
    ResultReason         : User 'user2@fabrikam.com' was updated in Azure Active Directory (target tenant)
    TargetResources      : {<ServicePrincipalId>, }
    AdditionalProperties : {}
    
    ActivityDateTime     : 7/31/2023 12:08:14 AM
    ActivityDisplayName  : Synchronization rule action
    AdditionalDetails    : {Details, ErrorCode, EventName, ipaddr...}
    Category             : ProvisioningManagement
    CorrelationId        : cc519f3b-fb72-4ea2-9b7b-8f9dc271c5ec
    Id                   : Sync_cc519f3b-fb72-4ea2-9b7b-8f9dc271c5ec_L5BFV_161778395
    InitiatedBy          : Microsoft.Graph.PowerShell.Models.MicrosoftGraphAuditActivityInitiator1
    LoggedByService      : Account Provisioning
    OperationType        :
    Result               : success
    ResultReason         : User 'user2@fabrikam.com' will be created in Azure Active Directory (target tenant) (User is active and assigned
                           in Azure Active Directory, but no matching User was found in Azure Active Directory (target tenant))
    TargetResources      : {<ServicePrincipalId>, }
    AdditionalProperties : {}
    

Troubleshooting tips

Symptom - Insufficient privileges error

When you try to perform an action, you receive an error message similar to the following:

code: Authorization_RequestDenied
message: Insufficient privileges to complete the operation.

Cause

Either the signed-in user doesn't have sufficient privileges, or you need to consent to one of the required permissions.

Solution

  1. Make sure you're assigned the required roles. See Prerequisites earlier in this article.

  2. When you sign in with Connect-MgGraph, make sure you specify the required scopes. See Step 1: Sign in to the target tenant and Step 4: Sign in to the source tenant earlier in this article.

Symptom - New-MgPolicyCrossTenantAccessPolicyPartner_Create error

When you try to create a new partner configuration, you receive an error message similar to the following:

New-MgPolicyCrossTenantAccessPolicyPartner_Create: Another object with the same value for property tenantId already exists.

Cause

You are likely trying to create a configuration or object that already exists, possibly from a previous configuration.

Solution

  1. Verify your syntax and that you are using the correct tenant ID.

  2. Use the Get-MgPolicyCrossTenantAccessPolicyPartner command to list the existing object.

  3. If you have an existing object, you might need to make an update using Update-MgPolicyCrossTenantAccessPolicyPartner

Symptom - Request_MultipleObjectsWithSameKeyValue error

When you try to enable user synchronization, you receive an error message similar to the following:

Invoke-MgGraphRequest: PUT https://graph.microsoft.com/v1.0/policies/crossTenantAccessPolicy/partners/<SourceTenantId>/identitySynchronization
HTTP/1.1 409 Conflict
...
{"error":{"code":"Request_MultipleObjectsWithSameKeyValue","message":"A conflicting object with one or more of the specified property values is present in the directory.","details":[{"code":"ConflictingObjects","message":"A conflicting object with one or more of the specified property values is present in the directory.", ... }}}

Cause

You are likely trying to create a policy that already exists, possibly from a previous configuration.

Solution

  1. Verify your syntax and that you are using the correct tenant ID.

  2. Use the Get-MgPolicyCrossTenantAccessPolicyPartnerIdentitySynchronization command to list the IsSyncAllowed setting.

    (Get-MgPolicyCrossTenantAccessPolicyPartnerIdentitySynchronization -CrossTenantAccessPolicyConfigurationPartnerTenantId $SourceTenantId).UserSyncInbound
    
  3. If you have an existing policy, you might need to make an update using Set-MgPolicyCrossTenantAccessPolicyPartnerIdentitySynchronization command to enable user synchronization.

    $Params = @{
        userSyncInbound = @{
            isSyncAllowed = $true
        }
    }
    Set-MgPolicyCrossTenantAccessPolicyPartnerIdentitySynchronization -CrossTenantAccessPolicyConfigurationPartnerTenantId $SourceTenantId -BodyParameter $Params
    

Next steps