Azure AD Cross-tenant synchronization is a feature currently in preview, that allows a tenant to synchronize its users as guests into other tenants, maintaining attributes and eventually deleting the guests when no longer needed (such as when an employee leaves the company). In this blogpost we will take a look under the hood of the feature, trying to understand how this feature really works.
The way we will do this is to configure synchronization of a set of users from Tenant A to Tenant B, while looking at what type of resources are created in Azure AD, and different API calls going from the Azure Portal to the backend APIs. Let’s go!
I will be working with these two demo tenants, where Tenant B will be the target tenant (meaning where the B2B guests will be created) and Tenant A will be the source tenant:
Tenant display name | Tenant ID | Tenant initial domain |
Tenant A | 140f6c96-5df3-4992-8dcc-2d8f2c6e7eee | M365x15302292.onmicrosoft.com |
Tenant B | 1cd52b79-e2d1-4e0c-bbe5-3c6d45b8e1ce | M365x19742525.onmicrosoft.com |
Target tenant configuration
We start in Tenant B, configuring the cross tenant access settings, to allow inbound configuration from Tenant A. We find Cross-tenant access settings under External identities and click +Add organization.

While inputting the domain name of Tenant A, we can see that the Azure Portal uses the findTenantInformationByDomainName endpoint to determine the Tenant ID of Tenant A, before it uses the create crossTenantAccessPolicyConfigurationPartner endpoint to add the tenant as an organization – everything done using documented APIs:


After going into the organization we just created, and enabling the Allow users to sync into this tenant checkbox, we see the portal calling an endpoint identitySynchronization under the added organization, with isSyncAllowed as the only parameter (tried GETing the same object, but it only conatins isSyncAllowed):

We have now configured everything needed in Tenant B. Nothing too interesting yet, so let’s continue to Tenant A.
Source tenant configuration
Now, in order to configure the source tenant Tenant A, we go to Cross-tenant synchronization and click +New configuration.

Let’s name it Guest sync to Tenant B and see what happens behind the scenes:

The first request that is sent, goes out to the instantiate applicationTemplates endpoint, with the displayName that we just provided as the name of our configuration:

The application template has id 518e5f48-1fc8-4c48-9387-9fdf28b0dfe7, and we can actually look into it as follows:
GET https://graph.microsoft.com/v1.0/applicationTemplates/518e5f48-1fc8-4c48-9387-9fdf28b0dfe7
{
"@odata.context": "https://graph.microsoft.com/v1.0/$metadata#applicationTemplates/$entity",
"id": "518e5f48-1fc8-4c48-9387-9fdf28b0dfe7",
"displayName": "Cross-Tenant Synchronization Preview",
"homePageUrl": "https://www.microsoft.com/",
"supportedSingleSignOnModes": [
"external"
],
"supportedProvisioningTypes": [
"sync"
],
"logoUrl": "https://az495088.vo.msecnd.net/app-logo/aad2aadsync_215.png",
"categories": [
"humanResources"
],
"publisher": "Microsoft",
"description": "Automate creating, updating, and deleting B2B users across Azure AD tenants in your organization. Learn more: aka.ms/CrossTenantSynchronizationGallery"
}
Not too interesting perhaps, but it is interesting to see that Microsoft is building services like cross tenant synchronization on top of their own ecosystem. Now, when instantiating an applicationTemplate, both an app registration is created, and a service principal is created from the app registration. These are both returned from the instantiate endpoint:
POST https://graph.microsoft.com/beta/applicationTemplates/518e5f48-1fc8-4c48-9387-9fdf28b0dfe7/instantiate
{
"displayName": "Guest sync to Tenant B"
}
{
"@odata.context": "https://graph.microsoft.com/beta/$metadata#microsoft.graph.applicationServicePrincipal",
"application": {
"objectId": "bb1baed0-68f6-462d-9b9f-7a60429bf8c2",
"appId": "93476ce2-266c-4218-bc36-1bab51636cba",
"applicationTemplateId": "518e5f48-1fc8-4c48-9387-9fdf28b0dfe7",
"displayName": "Guest sync to Tenant B",
"homepage": "https://account.activedirectory.windowsazure.com:444/applications/default.aspx?metadata=aad2aadsync|ISV9.1|primary|z",
"identifierUris": [],
"publicClient": null,
"replyUrls": [],
"logoutUrl": null,
"samlMetadataUrl": null,
"errorUrl": null,
"groupMembershipClaims": null,
"availableToOtherTenants": false,
"requiredResourceAccess": []
},
"servicePrincipal": {
"objectId": "f21f5ca1-6a00-4b5a-a38d-1a6aaa1e020c",
"deletionTimestamp": null,
"accountEnabled": true,
"appId": "93476ce2-266c-4218-bc36-1bab51636cba",
"appDisplayName": "Guest sync to Tenant B",
"applicationTemplateId": "518e5f48-1fc8-4c48-9387-9fdf28b0dfe7",
"appOwnerTenantId": "140f6c96-5df3-4992-8dcc-2d8f2c6e7eee",
"appRoleAssignmentRequired": true,
"displayName": "Guest sync to Tenant B",
"errorUrl": null,
"loginUrl": null,
"logoutUrl": null,
"homepage": "https://account.activedirectory.windowsazure.com:444/applications/default.aspx?metadata=aad2aadsync|ISV9.1|primary|z",
"samlMetadataUrl": null,
"microsoftFirstParty": null,
"publisherName": "Tenant A",
"preferredSingleSignOnMode": null,
"preferredTokenSigningKeyThumbprint": null,
"preferredTokenSigningKeyEndDateTime": null,
"replyUrls": [],
"servicePrincipalNames": [
"93476ce2-266c-4218-bc36-1bab51636cba"
],
"tags": [
"WindowsAzureActiveDirectoryIntegratedApp"
],
"notificationEmailAddresses": [],
"samlSingleSignOnSettings": null,
"keyCredentials": [],
"passwordCredentials": []
}
}
After the instantiate endpoint is called, the portal awaits for the service principal to become properly available (because of replication, etc., this might take a minute).
One of the requests the portal sends, is for the synchronization templates of the service principal:
GET https://graph.microsoft.com/beta/servicePrincipals/f21f5ca1-6a00-4b5a-a38d-1a6aaa1e020c/synchronization/templates
The returned body is way too long to paste here, but I have put the whole body it in pastebin, but will highlight some interesting things below. There is no way to PATCH these templates it seems, as that would be interesting.
The name of the template is Azure2Azure:

The schema does contain information about sync of other objects than users, though this might be “always present” in these:

I am definitely awaiting group synchronization as a feature here in the future.
There is only a single synchronization rule available, called USER_INBOUND_USER.

Anyway, let’s continue configuring the synchronization:

FYI: I have added the disable consent prompt option on inbound for Tenant B and outbound for Tenant A – just skipping the screenshots and stuff to shorten the blogpost.
Adding our Tenant B tenant id and clicking Test Connection causes a request to the validateCredentials endpoint, with the tenant id and authentication type set to SyncPolicy:

POST https://graph.microsoft.com/beta/servicePrincipals/f21f5ca1-6a00-4b5a-a38d-1a6aaa1e020c/synchronization/jobs
{
"templateId":"Azure2Azure"
}
After the POST is successful, the GUI gets some more stuff, such as the attribute map:

This is the same mapping I found in the JSON returned when getting the synchronization template, after instantiating the application template:

Ok we will circle back here, we just need to get the user synchronization to work properly first.
Adding the All Employees group is actually simply an app role assignment, on the service principal we have created – named Guest sync to Tenant B:



Well well, let’s start the sync:


Ok, cool, so things are working. We can of course not really access the requests going between the Azure AD synchronization jobs and Azure AD, but we can have a look at the logs to get some information, such as the provisioning logs:

Here we can actually see that the destination guest account have an attribute source populated with the source object id, source tenant id and a new type of creation type. We can actually get this using the Graph:
GET https://graph.microsoft.com/beta/users/22c79c45-a395-4086-ae8f-28aa33f6ed8c/source
{
"@odata.context": "https://graph.microsoft.com/beta/$metadata#users('22c79c45-a395-4086-ae8f-28aa33f6ed8c')/source",
"@odata.type": "#microsoft.graph.user",
"sourceTenantId": "140f6c96-5df3-4992-8dcc-2d8f2c6e7eee",
"sourceId": "a45ef751-7c76-4c9f-87bd-fa6e6ac85d29",
"synchronizationInfo": {
"creationType": "TenantToTenantSync"
}
}
I tried modifying the source attribute using PATCH and PUT, without success. But this does actually mean that you in the destination tenant can find the object id of the user object in the source tenant.
I looked into the app registration manifest, but found nothing of interest. This is mainly because everything that has to do with synchronization jobs are defined on the service principal level.
What I really wanted to find a way to do is to modify the synchronization rule by adding support for synchronizing groups, but I guess we’ll ave to wait for Microsoft to solve that 🙂
Guess I’ll leave it there. Did not find too much saucy stuff this time, but it is always interesting to see how Microsoft are actually building their services.