Best Fix Missing Teams Calendar Issue in Exchange On-Premises

By Ahmet Tolga KAYA 5 min read
Best Fix Missing Teams Calendar Issue in Exchange On-Premises

Resolving the missing Microsoft Teams Calendar tab for users hosted on an On-Premises Exchange Server requires establishing a secure OAuth trust and configuring Intra-Organization connectors between your local environment and Microsoft 365. When mailboxes reside on-premises, the Teams application (which operates entirely in the cloud) cannot natively pull calendar data without explicitly granted permissions to query your local Exchange Web Services (EWS).

Prerequisites for Teams Calendar Integration

  • Exchange Server 2016 or newer.
  • An active Azure subscription.
  • An administrative account assigned the Global Admin role in your Microsoft 365 tenant.

Phase 1: Environment Preparation and Authentication

All subsequent steps must be executed directly on an Exchange Server that meets the prerequisites. You will need to interact with both the local Exchange Management Shell (EMS) and standard Windows PowerShell to bridge the on-premises and cloud environments.

First, launch a standard PowerShell session as an Administrator. You must connect to the Microsoft Graph service using your Global Admin credentials to manage the cloud-side service principals.

Connect-MgGraph -Scopes Application.ReadWrite.AllCode language: CSS (css)
Microsoft Teams Calender


Once authenticated to your tenant, install the Exchange Online Management module, which will be required for configuring the cloud-side connectors later in the process.

Install-Module ExchangeOnlineManagement

Phase 2: Generating the Automation Scripts

To establish the OAuth trust, Microsoft 365 needs a copy of your on-premises Exchange authentication certificate. You will need to create three distinct PowerShell (.ps1) scripts to handle the extraction, cloud upload, and endpoint registration. Save these three scripts to your server.

1. ExportAuthCert.ps1 This script queries your local Exchange configuration for the active OAuth certificate, extracts it, and saves it as a .cer file to a new directory on your system drive (C:\OAuthConfig).

$thumbprint = (Get-AuthConfig).CurrentCertificateThumbprint
if((Test-Path $env:SYSTEMDRIVE\OAuthConfig) -eq $false)
{
    New-Item -Path $env:SYSTEMDRIVE\OAuthConfig -Type Directory
}
Set-Location -Path $env:SYSTEMDRIVE\OAuthConfig
$oAuthCert = (dir Cert:\LocalMachine\My) | Where-Object {$_.Thumbprint -match $thumbprint}
$certType = [System.Security.Cryptography.X509Certificates.X509ContentType]::Cert
$certBytes = $oAuthCert.Export($certType)
$CertFile = "$env:SYSTEMDRIVE\OAuthConfig\OAuthCert.cer"
[System.IO.File]::WriteAllBytes($CertFile, $certBytes)Code language: PHP (php)

2. RegisterEndpoint.ps1 This script informs Azure Active Directory where to route requests when Teams tries to access calendar data. The Application ID 00000002-0000-0ff1-ce00-000000000000 is the universal identifier for Exchange Online. Note: You must revise the URLs in this script (tolgakaya.tr and autodiscover.tolgakaya.tr) to match your organization’s specific domain and Autodiscover endpoints before executing it.

$ServiceName = "00000002-0000-0ff1-ce00-000000000000";
$x = Get-MgServicePrincipal -Filter "AppId eq '$ServiceName'"
$x.ServicePrincipalNames += "https://tolgakaya.tr/"
$x.ServicePrincipalNames += "https://autodiscover.tolgakaya.tr/"
Update-MgServicePrincipal -ServicePrincipalId $x.Id -ServicePrincipalNames $x.ServicePrincipalNamesCode language: PHP (php)

3. UploadAuthCert.ps1 This final script takes the .cer file you exported in the first step and uploads it to the Exchange Online Service Principal in Azure. This step ensures that when your on-premises server communicates with the cloud, Microsoft 365 cryptographically verifies and trusts the connection.

Connect-MgGraph -Scopes Application.ReadWrite.All
$CertFile = "$env:SYSTEMDRIVE\OAuthConfig\OAuthCert.cer"
$objFSO = New-Object -ComObject Scripting.FileSystemObject
$CertFile = $objFSO.GetAbsolutePathName($CertFile)
$cer = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($CertFile)
$binCert = $cer.GetRawCertData()
$credValue = [System.Convert]::ToBase64String($binCert)
$ServiceName = "00000002-0000-0ff1-ce00-000000000000"

Write-Host "[+] Trying to query the service principals for service: $ServiceName" -ForegroundColor Cyan
$p = Get-MgServicePrincipal -Filter "AppId eq '$ServiceName'"

Write-Host "[+] Trying to query the keyCredentials for service: $ServiceName" -ForegroundColor Cyan
$servicePrincipalKeyInformation = Get-MgServicePrincipal -Filter "AppId eq '$ServiceName'" -Select "keyCredentials"
$keyCredentialsLength = $servicePrincipalKeyInformation.KeyCredentials.Length

if ($keyCredentialsLength -gt 0) {
    Write-Host "[+] $keyCredentialsLength existing key(s) found – we keep them if they have not expired" -ForegroundColor Cyan
    $newCertAlreadyExists = $false
    $servicePrincipalObj = New-Object -TypeName Microsoft.Graph.PowerShell.Models.MicrosoftGraphServicePrincipal
    $keyCredentialsArray = @()
    
    foreach ($cred in $servicePrincipalKeyInformation.KeyCredentials) {
        $thumbprint = [System.Convert]::ToBase64String($cred.CustomKeyIdentifier)
        Write-Host "[+] Processing existing key: $($cred.DisplayName) thumbprint: $thumbprint" -ForegroundColor Cyan
        if ($newCertAlreadyExists -ne $true) {
            $newCertAlreadyExists = ($cer.Thumbprint).Equals($thumbprint, [System.StringComparison]::OrdinalIgnoreCase)
        }
        if ($cred.EndDateTime -lt (Get-Date)) {
            Write-Host "[+] This key has expired on $($cred.EndDateTime) and will not be retained" -ForegroundColor Yellow
            continue
        }
        $keyCredential = New-Object -TypeName Microsoft.Graph.PowerShell.Models.MicrosoftGraphKeyCredential
        $keyCredential.Type = "AsymmetricX509Cert"
        $keyCredential.Usage = "Verify"
        $keyCredential.Key = $cred.Key
        $keyCredentialsArray += $keyCredential
    }
    if ($newCertAlreadyExists -eq $false) {
        Write-Host "[+] New key: $($cer.Subject) thumbprint: $($cer.Thumbprint) will be added" -ForegroundColor Cyan
        $keyCredential = New-Object -TypeName Microsoft.Graph.PowerShell.Models.MicrosoftGraphKeyCredential
        $keyCredential.Type = "AsymmetricX509Cert"
        $keyCredential.Usage = "Verify"
        $keyCredential.Key = [System.Text.Encoding]::ASCII.GetBytes($credValue)
        $keyCredentialsArray += $keyCredential
        $servicePrincipalObj.KeyCredentials = $keyCredentialsArray
        Update-MgServicePrincipal -ServicePrincipalId $p.Id -BodyParameter $servicePrincipalObj
    } else {
        Write-Host "[+] New key: $($cer.Subject) thumbprint: $($cer.Thumbprint) already exists and will not be uploaded again" -ForegroundColor Yellow
    }
} else {
    $params = @{
        type = "AsymmetricX509Cert"
        usage = "Verify"
        key = [System.Text.Encoding]::ASCII.GetBytes($credValue)
    }
    Write-Host "[+] This is the first key which will be added to this service principal" -ForegroundColor Cyan
    Update-MgServicePrincipal -ServicePrincipalId $p.Id -KeyCredentials $params
}Code language: PHP (php)

Phase 3: Executing the Configuration

With the scripts ready, proceed to execute them in the correct sequence.

  1. Open the Exchange Management Shell (EMS) and run the ExportAuthCert.ps1 script. This will output a certificate file to your local drive.
  2. Switch back to your standard PowerShell window (where you are authenticated to MgGraph) and execute UploadAuthCert.ps1 to push the certificate to Azure.
  3. Finally, execute your modified RegisterEndpoint.ps1 script in the same standard PowerShell window to bind your domains to the cloud service.

Phase 4: Establishing Intra-Organization Connectors

The last technical requirement is to bridge the two environments by creating reciprocal Intra-Organization Connectors. This configuration allows the Teams application backend to seamlessly query the on-premises Autodiscover service for free/busy and meeting data.

Return to the Exchange Management Shell and run the following command to create the outbound connector from your local environment to the Microsoft 365 cloud. Make sure to replace the target domain with your actual .onmicrosoft.com tenant address:

New-IntraOrganizationConnector -Name ExchangeHybridOnPremisesToOnline -DiscoveryEndpoint https://outlook.office365.com/autodiscover/autodiscover.svc -TargetAddressDomains "tolgakayatr.mail.onmicrosoft.com"Code language: PHP (php)

Next, open a PowerShell session connected to Exchange Online and execute the inverse command. This connector directs cloud traffic back to your on-premises Autodiscover endpoint. Adjust the discovery endpoint URL and target domain to match your local infrastructure setup:

New-IntraOrganizationConnector -Name ExchangeHybridOnlineToOnPremises -DiscoveryEndpoint "https://mail.trt.net.tr/autodiscover/autodiscover.svc" -TargetAddressDomains "tolgakaya.tr"Code language: PHP (php)

Once the Active Directory synchronization processes these changes and the OAuth trust propagates, the Teams application will successfully resolve local EWS endpoints. Users hosted on the On-Premises Exchange server will see the Calendar tab appear in their Teams client, fully populated and functional.

Official Microsoft: Microsoft Learn

Read More: How to Best Using SCCM Filter Devices in a Specific Organizational Unit (OU)

Ahmet Tolga KAYA

Systems Engineer and Technical Writer focused on Windows, Microsoft technologies, infrastructure, cybersecurity, automation, and platform reliability.