Installing the Teams Outlook Add-In on Systems with WDAC and/or AppLocker

Deploying the Teams Outlook Add-In on systems where WDAC or AppLocker block installation

Installing the Teams Outlook Add-In on Systems with WDAC and/or AppLocker
Photo by Dimitri Karastelev / Unsplash

As a follow up to my series on implementing WDAC (see: https://www.mrgtech.net/implementing-wdac-and-applocker/), I thought I would share a solution to a problem my organisation was experiencing with the Teams Outlook add-in.

The way Microsoft deploys this add-in is... well... weird. When Teams starts, it attempts to install an MSI containing the add-in as the currently logged in user.

Allowing users to install MSI's (even signed ones) is generally frowned upon from a security point of view.

While this MSI could be installed via InTune as system (it would need then need to be registered using regsvr32.exe so that Outlook sees it), it does mean that you'll have to manage the installer on every release. Another issue we found is that not everyone runs the same version of Teams or the add-in, so the version you push may not be right for the currently installed version of Teams.

Using the WDAC policies I described, the MSI will actually fail to run as the user as it's using a component that's not signed and trusted by the WDAC policies.

The solution we came up with is to run a script on user logon, which does the following:

  • Hash the MSI (located at C:\Program Files\WindowsApps\MSTeams_24231.512.3106.6573_x64__8wekyb3d8bbwe\MicrosoftTeamsMeetingAddinInstaller.msi) - if it matches a stored hash, then do nothing. Otherwise...
  • Get the MSI version number
  • Extract the MSI using the /a switch - this extracts rather than executes the MSI
  • Record if Outlook is open, and close any Outlook processes if running
  • Copy the extracted MSI content to C:\Users\<username>\AppData\Local\Microsoft\TeamsMeetingAdd-in\<versionnumber>
  • Run regsvr32.exe to register the library as the user
  • Make some registry changes to ensure Outlook is enabling the add-in
  • Start Outlook if it was running before.

Here's the script:

script.ps1

$TeamsPath = $(Get-AppxPackage -Name "MSTeams").InstallLocation
$AddinPath = "$TeamsPath\MicrosoftTeamsMeetingAddinInstaller.msi"
if (-not (Test-Path -Path $AddinPath)) {
    throw "Add-in not found at $AddinPath"
}
else {
    Write-Host "Add-in found at $AddinPath"
}
$TargetRootDirectory = "$env:LocalAppData\Microsoft\TeamsMeetingAdd-in"
# get the msi version
$AddinFileInformation = Get-AppLockerFileInformation -Path $AddinPath
# the version number is the last element of the publisher attribute
$AddinVersion = $AddinFileInformation.Publisher.ToString().Split(',')[-1]
# test that $addInVersion is a valid version number by parsing with [version]
if (-not ([version]::TryParse($AddinVersion, [ref]$null))) {
    throw "Invalid version number: $AddinVersion"
}
[version]$AddinVersion_Parsed = [version]::Parse($AddinVersion)
Write-Host "Add-in version: $AddinVersion"
$TargetDirectory = "$TargetRootDirectory\$($AddinVersion_Parsed.Major).$($AddinVersion_Parsed.Minor).$($AddinVersion_Parsed.Build)"
$LoaderDll = "$TargetDirectory\x64\Microsoft.Teams.AddinLoader.dll"
# msi
$Executable = "msiexec.exe"
$Arguments = @(
    "/qn",
    "/a ""$AddinPath""",
    "TARGETDIR=""$TargetDirectory"""
)

# get the msi hash to use as a flag to know when to apply an update
$AddinHash = Get-FileHash -Path $AddinPath -Algorithm SHA256
$FlagFile = "$env:ProgramData\Scripts\TeamsMeetingAddinInstalled.$env:USERNAME"
# check if content of the flag file is the same as the hash of the msi
$ProceedWithInstall = $false
if (-not (Test-Path -Path $FlagFile)) {
    $ProceedWithInstall = $true
}
else {
    $FlagHash = Get-Content -Path $FlagFile
    if ($FlagHash -ne $AddinHash.Hash) {
        $ProceedWithInstall = $true
    }
}

# stop installation if the add-in is already installed
if (-not $ProceedWithInstall) {
    Write-Host "Add-in is already installed. Skipping installation."
    Exit 0
}

# check if Outlook is running
$OutlookProcess = Get-Process -Name "OUTLOOK" -ErrorAction SilentlyContinue
$OutlookWasRunning = $false
$OutlookProcessPath = ""
if ($OutlookProcess) {
    # warn the user about what is about to happen
    $Message = "The Microsoft Teams Meeting Add-in for Microsoft Office will be installed. Outlook will be closed during the installation. Please save your work and close Outlook before proceeding. Click OK to continue."
    $Title = "Microsoft Teams Meeting Add-in for Microsoft Office"
    Add-Type -AssemblyName PresentationCore, PresentationFramework
    $Result = [System.Windows.MessageBox]::Show($Message, $Title, [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Warning)

    $OutlookWasRunning = $true
    $OutlookProcessPath = $OutlookProcess.Path

    Write-Host "Outlook is running. Stopping Outlook..."

    # stop Outlook
    Stop-Process $OutlookProcess -Force
    
    # loop for up to 10 seconds to wait for Outlook to close
    $OutlookProcess = Get-Process -Name "OUTLOOK" -ErrorAction SilentlyContinue
    $i = 0
    while ($OutlookProcess -and $i -lt 10) {
        Start-Sleep -Seconds 1
        $OutlookProcess = Get-Process -Name "OUTLOOK" -ErrorAction SilentlyContinue
        $i++
    }
    if ($OutlookProcess) {
        throw "Outlook did not close. Exiting."
    }

    Write-Host "Outlook was running. Stopped Outlook."
    Write-Host "Outlook path: $OutlookProcessPath"
}

# delete the existing add-in
if (Test-Path -Path $TargetRootDirectory) {
    Remove-Item -Path $TargetRootDirectory -Recurse -Force
}

# install the add-in
Start-Process -FilePath $Executable -ArgumentList $Arguments -Wait
		
# register new add-in
Start-Process -FilePath "C:\Windows\System32\regsvr32.exe" -ArgumentList "/s /n /i:user '$LoaderDll'" -Wait
		
$RegistryKey = "HKCU:\Software\Microsoft\Office\Outlook\Addins\TeamsAddin.FastConnect"
if (-not (Test-Path -Path $RegistryKey)) {
    New-Item -Path $RegistryKey -Force
}

if (-not (Get-Item $RegistryKey -EA Ignore).Property -contains "LoadBehavior") {
    New-ItemProperty -Path $RegistryKey -Name "LoadBehavior" -PropertyType Dword -Value 3
}
else {
    Set-ItemProperty -Path $RegistryKey -Name "LoadBehavior" -Value 3
}
if (-not (Get-Item $RegistryKey -EA Ignore).Property -contains "Description") {
    New-ItemProperty -Path $RegistryKey -Name "Description" -PropertyType String -Value "Microsoft Teams Meeting Add-in for Microsoft Office"
}
else {
    Set-ItemProperty -Path $RegistryKey -Name "Description" -Value "Microsoft Teams Meeting Add-in for Microsoft Office"
}
if (-not (Get-Item $RegistryKey -EA Ignore).Property -contains "FriendlyName") {
    New-ItemProperty -Path $RegistryKey -Name "FriendlyName" -PropertyType String -Value "Microsoft Teams Meeting Add-in for Microsoft Office"
}
else {
    Set-ItemProperty -Path $RegistryKey -Name "FriendlyName" -Value "Microsoft Teams Meeting Add-in for Microsoft Office"
}

# create the flag file
$AddinHash.Hash | Set-Content -Path $FlagFile

# start Outlook if it was running
if ($OutlookWasRunning) {
    Write-Host "Starting Outlook..."
    Write-Host "Outlook path: $OutlookProcessPath"
    Start-Process "$OutlookProcessPath"
}

To deploy this script, I've created installation and uninstallation scripts:

install.ps1

$InstallationDirectory = "$env:ProgramData\Scripts\TeamsMeetingAddin"
$ScriptRevision = 1

# copy all files to the installation directory - deleting any existing directory
Write-Host "Copying files to $InstallationDirectory"
if (Test-Path -Path $InstallationDirectory) {
    Remove-Item -Path $InstallationDirectory -Recurse -Force
}

Copy-Item -Path $PSScriptRoot -Destination $InstallationDirectory -Recurse

# create scheduled task to run the script
Write-Host "Creating scheduled task to run the script"
$taskAction = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-ExecutionPolicy Bypass -WindowStyle Hidden -File ""$InstallationDirectory\script.ps1"""
$taskTrigger = @(
    $(New-ScheduledTaskTrigger `
            -AtLogOn
    )
)
$taskSettings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -RunOnlyIfNetworkAvailable
$taskPrincipal = New-ScheduledTaskPrincipal -GroupId 'BUILTIN\Users'

Register-ScheduledTask `
    -TaskName "TeamsMeetingAddinInstall" `
    -Action $taskAction `
    -Trigger $taskTrigger `
    -Settings $taskSettings `
    -Principal $TaskPrincipal

# create a flag file to indicate that the add-in has been installed
$FlagFile = "$InstallationDirectory\$ScriptRevision"
$ScriptRevision | Set-Content -Path $FlagFile

# unregister existing add-in
# check if C:\Program Files (x86)\Microsoft\TeamsMeetingAddin exists, and then loop each sub-directory, unregistering each add-in inside
$AddinDirectory = "C:\Program Files (x86)\Microsoft\TeamsMeetingAddin"
if (Test-Path -Path $AddinDirectory) {
    $AddinDirectories = Get-ChildItem -Path $AddinDirectory -Directory
    foreach ($Addin in $AddinDirectories) {
        Write-Host "Unregistering add-in in $AddinDirectory\$Addin"
        if (Test-Path -Path "$AddinDirectory\$Addin\AddinInstaller.dll") {
            $LoaderDll = "$AddinDirectory\$Addin\AddinInstaller.dll"
            Write-Host "Unregistering $LoaderDll"
            Start-Process -FilePath "C:\Windows\System32\regsvr32.exe" -ArgumentList "/s /u:computer '$LoaderDll'" -Wait
            Start-Process -FilePath "C:\Windows\System32\regsvr32.exe" -ArgumentList "/s /u:user '$LoaderDll'" -Wait
        }

        if (Test-Path -Path "$AddinDirectory\$Addin\x86\Microsoft.Teams.AddinLoader.dll") {
            $LoaderDll = "$AddinDirectory\$Addin\x86\Microsoft.Teams.AddinLoader.dll"
            Write-Host "Unregistering $LoaderDll"
            Start-Process -FilePath "C:\Windows\System32\regsvr32.exe" -ArgumentList "/s /u:computer '$LoaderDll'" -Wait
            Start-Process -FilePath "C:\Windows\System32\regsvr32.exe" -ArgumentList "/s /u:user '$LoaderDll'" -Wait
        }

        if (Test-Path -Path "$AddinDirectory\$Addin\x64\Microsoft.Teams.AddinLoader.dll") {
            $LoaderDll = "$AddinDirectory\$Addin\x64\Microsoft.Teams.AddinLoader.dll"
            Write-Host "Unregistering $LoaderDll"
            Start-Process -FilePath "C:\Windows\System32\regsvr32.exe" -ArgumentList "/s /u:computer '$LoaderDll'" -Wait
            Start-Process -FilePath "C:\Windows\System32\regsvr32.exe" -ArgumentList "/s /u:user '$LoaderDll'" -Wait
        }
    }
}

# uninstall the add-in
Write-Host "Uninstalling Teams Meeting Add-in"
Start-Process -FilePath "msiexec.exe" -ArgumentList '/x "{A7AB73A3-CB10-4AA5-9D38-6AEFFBDE4C91}" /qn"' -Wait

# remove the add-in program directory
if (Test-Path -Path $AddinDirectory) {
    Remove-Item -Path $AddinDirectory -Recurse -Force
}

# remove any flags
$FlagFile = "$env:ProgramData\Scripts\TeamsMeetingAddinInstalled.*"
Remove-Item -Path $FlagFile -Force

uninstall.ps1

$InstallationDirectory = "$env:ProgramData\Scripts\TeamsMeetingAddin"

# delete the existing add-in
if (Test-Path -Path $InstallationDirectory) {
    Remove-Item -Path $InstallationDirectory -Recurse -Force
}

# remove the scheduled task
$TaskName = "TeamsMeetingAddinInstall"
if (Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue) {
    Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false
}

# remove flags
$FlagFile = "$env:ProgramData\Scripts\TeamsMeetingAddinInstalled.*"
Remove-Item -Path $FlagFile -Force

Package these three scripts up into a *.intunewin file and deploy from Intune.

Once deployed, on the next user logon after the Teams client has updated, the add-in will automatically update to the most recently supplied version.