Skip to main content

Notifications

Announcements

No record found.

Community site session details

Community site session details

Session Id :
Customer experience | Sales, Customer Insights,...
Answered

Dynamics CRM 2016 to Exchange Online PowerShell Script Question

(2) ShareShare
ReportReport
Posted on by 115
We currently use CRM 2016 on-premises and have been migrated to exchange online. They were not aware of the exchange integration with dynamics so the connection between CRM and exchange on premises was broken. I have read the documentation from the Microsoft Learning Portal (Here). There is a Powershell script that I am seeking advice on. 
 
Starting from the top of the documentation These are the questions that I currently have and I am looking for guidance concerning these. 
 
Question 1: 365 Hybrid Connector - We know how to purchase it but does it get installed anywhere? 
 
Question 2: On step 4 of the instructions it says
 
"In the PowerShell session from step 2, invoke the ConfigureCrmServerSideSync command.

Download the script and replace the existing script if the ConfigureCrmServerSideSync.ps1 script present in the current PowerShell session directory, from above, is different than the script in the download link:

$ConfigureCrmServerSideSyncWithCommand = 
".\ConfigureCrmServerSideSync.ps1 -privateKeyPassword (ConvertTo- 
SecureString 'personal_certfile_password' -AsPlainText -Force) -pfxFilePath 
c:\Personalcertfile.pfx -organizationName organization_name 
-microsoftEntraIdTenantIdOrDomainName microsoft_entraid_tenantid_or_domain_name 
-ClientID app_id_from_step3 -ClientSecret -client_secret" 

Invoke-Expression -command $ConfigureCrmServerSideSyncWithCommand
STEP 2 SCRIPT
$CertificateScriptWithCommand = ".\CertificateReconfiguration.ps1 -certificateFile c:\Personalcertfile.pfx -password personal_certfile_password -updateCrm -certificateType S2STokenIssuer -serviceAccount contoso\CRMAsyncService -storeFindType FindBySubjectDistinguishedName"
Invoke-Expression -command $CertificateScriptWithCommand
 
According to the directions, I should use this script, If I use this script in place do I use the entire script or do I need to replace some parameters?
# CRM on-premises with Exchange Online server-side synchronization setup  
# PowerShell script to upload private key to Azure.  
# This script has to be invoked from the server that has the deployment tool feature installed

Param(
    [Parameter(Mandatory=$True, HelpMessage="Enter password for Certificate private key.")]
    [SecureString]$privateKeyPassword,

    [Parameter(Mandatory=$True, HelpMessage="Enter path of Personal Information Exchange file.")]
    [string]$pfxFilePath,

    [Parameter(Mandatory=$true, HelpMessage="Enter organization name.")]
    [string]$organizationName,

    [Parameter(Mandatory=$true, HelpMessage="Enter Microsoft Entra ID TenantId Or Domain Name.")]
    [string]$microsoftEntraIdTenantIdOrDomainName,

    [Parameter(Mandatory=$true, HelpMessage="Enter Reverse Hybrid setup App Id.")]
    [string]$ClientID,

    [Parameter(Mandatory=$true, HelpMessage="Enter Reverse Hybrid setup App client secret")]
    [string]$ClientSecret
)

#region Functions
function Update-ServicePrincipal {
    param (
        [Parameter(Mandatory=$true)]
        [hashtable]$ServicePrincipal,

        [Parameter(Mandatory=$true)]
        [string]$operationName,

        [Parameter(Mandatory=$true)]
        [string]$uri,

        [Parameter(Mandatory=$true)]
        [hashtable]$headers
    )

    $jsonString = $ServicePrincipal | ConvertTo-Json

    try {
            $response = Invoke-WebRequest -Uri $uri -Method PATCH -Headers $headers -Body $jsonString -UseBasicParsing
        }
        catch {
            # An error occurred, access the error response
            if ($_.Exception -is [System.Net.WebException]) {
                $response = $_.Exception.Response
                $reader = New-Object System.IO.StreamReader($response.GetResponseStream())
                $responseContent = $reader.ReadToEnd()
                ExitWithError("HTTP error response content: " + $responseContent +" for operation: " + $operationName)
            } else {
                # Handle other types of exceptions if necessary
                ExitWithError("An error occurred:" + $_.Exception.Message + " for operation: " + $operationName)
            }
    }
}


#Function to Display Error message 
function ExitWithError([string] $errorMessage)
{
        Write-host $errorMessage -foreground "red"
        Write-Host "Process Failed." -foreground "red"
        Exit 1
}

#endregion Functions

try
{
    if(!(Get-Pssnapin | Where-Object {$_.name -like "Microsoft.Crm.PowerShell"} ))
    {
        Add-Pssnapin microsoft.crm.powershell
        Write-Host "Added CRM powershell Pssnapin." -foreground "Green"
    }

    if(!(Test-Path -Path $pfxFilePath -PathType Leaf))
    {
       ExitWithError("The specified value of the pfxFilePath parameter isn't valid. Please enter the correct path of the Personal Information Exchange file.")
    }
    else
    {

      $extentionPfx = (Get-Item $pfxFilePath ).Extension 
      if($extentionPfx -ne ".pfx")
      {
           ExitWithError("The specified value of the pfxFilePath parameter isn't valid. Please enter the path of the .pfx file.")
      }
    }

    $graphBaseUri = "https://graph.microsoft.com"

    #region AcquireMsGraphToken
    $Body =  @{
        Grant_Type    = "client_credentials"
        Scope         = "$graphBaseUri/.default"
        Client_Id     = $ClientID
        Client_Secret = $ClientSecret
    }

    $tokenResponse = Invoke-RestMethod `
        -Uri https://login.microsoftonline.com/$microsoftEntraIdTenantIdOrDomainName/oauth2/v2.0/token `
        -Method POST `
        -Body $body

        $headers = @{
                'Authorization'="Bearer $($tokenResponse.access_token)"
                "Content-Type" = "application/json"
             }

    $organizationResponse = Invoke-WebRequest `
        -Uri $graphBaseUri/v1.0/organization `
        -Method GET `
        -Headers $headers

    $TenantID = ($organizationResponse.Content | ConvertFrom-Json).Value.id
    #endregion AcquireMsGraphToken 

    #region SetCertificateInfo
    $securePassword = $privateKeyPassword
    $PfxData = Get-PfxData -FilePath $pfxFilePath -Password $securePassword
    $certificateInfo = $PfxData.EndEntityCertificates[0]
    $certificateBin = $certificateInfo.GetRawCertData() 
    $credentialValue = [System.Convert]::ToBase64String($certificateBin)

    $currentDateTime = Get-Date

    if ($certificateInfo.NotAfter -lt $currentDateTime)
    {
        throw "Certificate with thumbprint $($certificateInfo.Thumbprint) has already expired on $($certificateInfo.NotAfter)."
    }

    $keyCredentialsJsonPayload = @{
            endDateTime = $certEndDateTime
            startDateTime = $certStartDateTime
            type = "AsymmetricX509Cert"
            usage = "Verify"
            key = $credentialValue
            displayName = $certificateInfo.Subject
        }
    $keyCredentialsJsonPayload = New-Object -TypeName PSObject -Property $keyCredentialsJsonPayload


    Write-Host "Done with setting up certificate information." 
    #endregion SetCertificateInfo

    #region ServicePrincipalOperations
    #Set CRM Principal Name in Microsoft Entra ID

    $BaseUri = "$graphBaseUri/v1.0/servicePrincipals"

    $crmAppId =  "00000007-0000-0000-c000-000000000000"

    $findServicePrincipalsQuery = "$BaseUri`?`$filter=appId eq '$crmAppId'&`$select=id,appId,servicePrincipalNames,keyCredentials"
    $findServicePrincipalsResponse = Invoke-WebRequest -Headers $headers -Uri $findServicePrincipalsQuery
    $findServicePrincipalsResponseJson = ($findServicePrincipalsResponse.Content | ConvertFrom-Json).Value

    $servicePrincipalCredentials = $findServicePrincipalsResponseJson.keyCredentials
    if ($null -ne $servicePrincipalCredentials -and -not ($servicePrincipalCredentials -is [array])) {
        $servicePrincipalCredentials = @($servicePrincipalCredentials)
    }

    $servicePrincipalNames = $findServicePrincipalsResponseJson.servicePrincipalNames
    if ($null -ne $servicePrincipalNames -and -not ($servicePrincipalNames -is [array])) {
        $servicePrincipalNames = @($servicePrincipalNames)
    }

    if ($null -eq $servicePrincipalNames) {
        $servicePrincipalNames = [System.Collections.Generic.List[object]]::new()
    }

    $servicePrincipalId = $findServicePrincipalsResponseJson.id

    $servicePrincipalCredentialsWorkingCollection = [System.Collections.Generic.List[object]]::new()
    if ($null -ne $servicePrincipalCredentials -and $servicePrincipalCredentials.Count -gt 0) {
        $servicePrincipalCredentialsWorkingCollection.AddRange($servicePrincipalCredentials)
    }

    $createServicePrincipalCredential = $true
    $patchServicePrincipalCredential = $false

    if ($null -ne $servicePrincipalCredentials)
    {
        for ($i = 0; $i -lt $servicePrincipalCredentials.Count; $i++)
        {
            if ($servicePrincipalCredentials[$i].endDateTime -lt $currentDateTime)
            {
                Write-Output("Certificate '" + $servicePrincipalCredentials[$i].displayName + "', with thumbprint '" + $servicePrincipalCredentials[$i].customKeyIdentifier + "' has expired on "+ $servicePrincipalCredentials[$i].endDateTime +". Removing the certificate principal from CRM app with id '" + $crmAppId +"'.")
                $removeID = $servicePrincipalCredentials[$i].keyId
                $servicePrincipalCredentialsWorkingCollection = $servicePrincipalCredentialsWorkingCollection | Where-Object { $_.keyId -ne $removeID }
                $patchServicePrincipalCredential = $true
            }
            else
            {
                if ($servicePrincipalCredentials[$i].key -eq $credentialValue)
                {
                    $createServicePrincipalCredential = $false
                    Write-Output("Given the certificate is already associated with the principal linked to the appId " + $crmAppId + ". Cert thumbprint " + $certificateInfo.Thumbprint + ". Not adding the cert principal.")
                }
            }
        }
    }

    $servicePrincipalCredentialsForPatch = [System.Collections.Generic.List[object]]::new()

    $servicePrincipalCredentialsWorkingCollection | ForEach-Object {
        $obj = $_ | Select-Object * -ExcludeProperty key, displayName
        # Add the modified object to the list
        $servicePrincipalCredentialsForPatch.Add([PSCustomObject]$obj)
    }

    if ($createServicePrincipalCredential)
    {
        $servicePrincipalCredentialsForPatch += $keyCredentialsJsonPayload
        Write-Output("Adding new certificate principal credential for appId " + $crmAppId +". Cert thumbprint " +$certificateInfo.Thumbprint)
    }

    $crmServicePrincipalUri = "$BaseUri`/$servicePrincipalId"

    if ($createServicePrincipalCredential -Or $patchServicePrincipalCredential)
    {
        $jsonPayload = @{
            keyCredentials = $servicePrincipalCredentialsForPatch
        }
        Update-ServicePrincipal $jsonPayload "createOrUpdateServicePrincipalCredential" -uri $crmServicePrincipalUri $headers 
        Write-Output("Successfully updated key Credentials for app "+ $crmAppId +". Cert thumbprint " +$certificateInfo.Thumbprint)
    }

    #Add CRM App Id to the servicePrincipalNames if needed
    if ($servicePrincipalNames.Where({ $_ -eq $crmAppId }, 'First').Count -eq 0)
    {
        $servicePrincipalNames += $crmAppId

        # Create a hashtable that represents JSON structure
        $jsonPayload = @{
            servicePrincipalNames = $servicePrincipalNames
        }
        Update-ServicePrincipal -ServicePrincipal $jsonPayload -operationName "updateServicePrincipalNames" -uri $crmServicePrincipalUri -headers $headers    
        Write-Output("Added new service principal name: " + $crmAppId)
    }
    #endregion ServicePrincipalOperations

    #Configure CRM server for server-based authentication with Online Exchange
       
    $setting = New-Object "Microsoft.Xrm.Sdk.Deployment.ConfigurationEntity" 
    $setting.LogicalName = "ServerSettings" 
    $setting.Attributes = New-Object "Microsoft.Xrm.Sdk.Deployment.AttributeCollection" 
    $attribute1 = New-Object "System.Collections.Generic.KeyValuePair[String, Object]" ("S2SDefaultAuthorizationServerPrincipalId", "00000001-0000-0000-c000-000000000000") 
    $setting.Attributes.Add($attribute1) 
    $attribute2 = New-Object "System.Collections.Generic.KeyValuePair[String, Object]" ("S2SDefaultAuthorizationServerMetadataUrl","https://accounts.accesscontrol.windows.net/metadata/json/1") 
    $setting.Attributes.Add($attribute2) 
    Set-CrmAdvancedSetting -Entity $setting 

    Write-Host "Done with configuration of CRM server for server-based authentication with Online Exchange."

    try
    {
        $orgInfo = Get-CrmOrganization  -Name $organizationName
        $ID =  $orgInfo.id 
    }Catch
    {
        ExitWithError("The specified organization "+$organizationName+" is not a valid CRM organization.")
    }
    if($ID)
    {
         Set-CrmAdvancedSetting -ID $orgInfo.ID -configurationEntityName "Organization" -setting "S2STenantId" -value $TenantID
    }

    Write-Host "S2S Exchange Online Tenant ID is populated in configDB: " $TenantID
    Write-Host "Process succeeded."  -foreground "green"
}
Catch 
{
    Write-Host "Failure Details: $_" -foreground "red"
    ExitWithError($_.Exception.Message)
}
 
  • David Lewis Profile Picture
    115 on at
    Dynamics CRM 2016 to Exchange Online PowerShell Script Question
    @Daivat Vartak (v-9davar) Thanks for the quick response. question regarding:

    "Run the $CertificateScriptWithCommand (after replacing parameters) to configure the certificate in CRM."
     
    If this certificate is already configured on the on-premises server, do i still need to do run this command again? 
     
    From my understanding this is needed for the CRM server only and does not affect Azure in any type of way... Could you please chime in
  • Verified answer
    Daivat Vartak (v-9davar) Profile Picture
    7,336 Super User 2025 Season 1 on at
    Dynamics CRM 2016 to Exchange Online PowerShell Script Question
    Hello David Lewis,
     

    Let's break down the questions regarding connecting your on-premises Dynamics CRM 2016 to Exchange Online using the provided PowerShell scripts.

    Question 1: 365 Hybrid Connector - Installation

    The Microsoft 365 Hybrid Configuration Wizard (HCW) is the tool used to configure a hybrid deployment between your on-premises Exchange organization and Exchange Online.

    • Installation Location: The Hybrid Configuration Wizard is run from a server within your on-premises Exchange environment. It doesn't get installed as a persistent service. You download and run it when you need to configure or modify your hybrid setup.

    • Purpose: The HCW helps establish trust, configure mail flow, manage free/busy sharing, and handle other aspects of a hybrid Exchange environment. While it's primarily for Exchange hybrid, the trust established can be leveraged for other Microsoft 365 integrations.

    • Relevance to CRM: While the HCW itself doesn't directly configure the CRM-to-Exchange Online connection, ensuring a properly functioning Exchange Hybrid environment is a prerequisite for server-side synchronization to work reliably. The trust and authentication mechanisms established by the HCW are often necessary for Dynamics 365 to communicate securely with Exchange Online.

    In summary, the Microsoft 365 Hybrid Configuration Wizard is downloaded and run on an on-premises Exchange server to configure the hybrid environment. It's a one-time (or as-needed) tool, not a continuously running service.

    Question 2: Using the ConfigureCrmServerSideSync.ps1 Script

    Yes, the script block you've quoted from step 4 of the instructions is intended to be used to configure the server-side synchronization between your on-premises Dynamics CRM 2016 and Exchange Online.

    How to Use the Script:

    You need to replace the placeholder parameters within the $ConfigureCrmServerSideSyncWithCommand variable with your actual values. Let's break down each parameter:

    • -privateKeyPassword 'personal_certfile_password': Replace 'personal_certfile_password' with the actual password you used to protect the private key of the certificate (.pfx file).

    • -pfxFilePath c:\Personalcertfile.pfx: Replace c:\Personalcertfile.pfx with the full path to the .pfx certificate file on the server where you are running the PowerShell script.

    • -organizationName organization_name: Replace organization_name with the unique name of your Dynamics CRM 2016 organization. This is the name you see when you access your CRM instance in a browser (e.g., contoso).

    • -microsoftEntraIdTenantIdOrDomainName microsoft_entraid_tenantid_or_domain_name: Replace microsoft_entraid_tenantid_or_domain_name with either your Microsoft Entra ID (Azure AD) Tenant ID (a GUID) or your primary domain name associated with your Microsoft 365 subscription (e.g., contoso.onmicrosoft.com or contoso.com).

    • -ClientID app_id_from_step3: Replace app_id_from_step3 with the Application (client) ID of the Azure AD application registration you created in the previous step of the Microsoft documentation. This application represents your on-premises CRM instance in Azure AD.

    • -ClientSecret -client_secret: Replace -client_secret with the client secret you generated for the Azure AD application registration. Ensure there is a space between -ClientSecret and the actual secret.

    •  

    Do you use the entire script?

    The script block you've quoted from step 4 is designed to be executed after you have downloaded the ConfigureCrmServerSideSync.ps1 script to your local machine.

    Here's the intended workflow:

    1. Download ConfigureCrmServerSideSync.ps1: Obtain the script from the link provided in the Microsoft documentation.

    2. Navigate to the Script Directory: Open a PowerShell window and navigate to the directory where you saved the ConfigureCrmServerSideSync.ps1 file.

    3. Construct the Command: Modify the $ConfigureCrmServerSideSyncWithCommand variable in your PowerShell session by replacing all the placeholder parameters with your specific values, as described above.

    4. Execute the Command: Run the command Invoke-Expression -command $ConfigureCrmServerSideSyncWithCommand. This will execute the ConfigureCrmServerSideSync.ps1 script with the parameters you provided.

    5.  

    Regarding the "STEP 2 SCRIPT" ($CertificateScriptWithCommand):

    The documentation provides this $CertificateScriptWithCommand in Step 2 to configure the certificate within your on-premises CRM. You will likely need to run this script before you run the ConfigureCrmServerSideSync.ps1 script in Step 4.

    For the "STEP 2 SCRIPT", you also need to replace the parameters:

    • -certificateFile c:\Personalcertfile.pfx: Replace with the full path to your .pfx certificate file.

    • -password personal_certfile_password: Replace with the password for your certificate's private key.

    • -updateCrm: This flag indicates that you want to update CRM with the certificate information.

    • -certificateType S2STokenIssuer: This specifies that the certificate will be used for Server-to-Server (S2S) authentication.

    • -serviceAccount contoso\CRMAsyncService: Replace contoso\CRMAsyncService with the domain and user name of the service account under which your Dynamics CRM Asynchronous Processing Service is running.

    • -storeFindType FindBySubjectDistinguishedName: This specifies how to find the certificate in the local certificate store.

    •  

    Therefore, the general process will be:

    1. Run the $CertificateScriptWithCommand (after replacing parameters) to configure the certificate in CRM.

    2. Run the $ConfigureCrmServerSideSyncWithCommand (after replacing parameters) to configure the connection to Exchange Online.


    3.  

    Important Considerations:

    • Permissions: Ensure the user account running these PowerShell scripts has the necessary administrative privileges on the CRM server and within your Microsoft 365 environment.

    • Certificate: You will need a valid SSL certificate that can be used for S2S authentication. The documentation likely outlines the requirements for this certificate.

    • Firewall Rules: Ensure that the necessary firewall rules are in place to allow communication between your on-premises CRM server and Exchange Online.

    • Testing: After running the scripts, thoroughly test the server-side synchronization functionality by sending and receiving emails, tracking appointments, and using other integrated features.

    •  

    By carefully following the steps in the Microsoft documentation and replacing the parameters in both the Step 2 and Step 4 scripts with your specific environment details, you should be able to establish the server-side synchronization between your on-premises Dynamics CRM 2016 and Exchange Online. Remember to perform these steps in a non-production environment first if possible.

     
    If my answer was helpful, please click Like, and if it solved your problem, please mark it as verified to help other community members find more. If you have further questions, please feel free to contact me.
     
    My response was crafted with AI assistance and tailored to provide detailed and actionable guidance for your Microsoft Dynamics 365 query.
     
    Regards,
    Daivat Vartak

Under review

Thank you for your reply! To ensure a great experience for everyone, your content is awaiting approval by our Community Managers. Please check back later.

Helpful resources

Quick Links

🌸 Community Spring Festival 2025 Challenge Winners! 🌸

Congratulations to all our community participants!

Adis Hodzic – Community Spotlight

We are honored to recognize Adis Hodzic as our May 2025 Community…

Kudos to the April Top 10 Community Stars!

Thanks for all your good work in the Community!

Leaderboard > Customer experience | Sales, Customer Insights, CRM

#1
Daivat Vartak (v-9davar) Profile Picture

Daivat Vartak (v-9d... 225 Super User 2025 Season 1

#2
Vahid Ghafarpour Profile Picture

Vahid Ghafarpour 78 Super User 2025 Season 1

#3
Muhammad Shahzad Shafique Profile Picture

Muhammad Shahzad Sh... 72

Overall leaderboard

Product updates

Dynamics 365 release plans