Inevitably between all of the customers you manage, there is bound to be a few human errors in offboarding users from Microsoft 365. A great practice when offboarding is converting a user’s mailbox to shared so that you can retain data but not have to pay for a subscription. I have heard countless stories of MSPs finding shared mailboxes in which still have a license assigned because a technician forgot to remove it.  In this post, I am going to show you how to use Azure Functions and Powershell to automatically remove licenses from shared mailboxes in all of your customer environments as a scheduled job. I will also provide a different script if you would just want to run this locally instead. 

Prerequisites

In a previous post, I showed you the fundamentals of Azure Functions as well as setting them up with the secure application model for headless authentication. I would suggest reading those articles to get a better understanding of what we are doing here and gather the necessary secrets you will need to authenticate

If you just want to run this script locally instead of using Azure Functions, you will still need to follow the steps in Kelvins guide for getting GUIDs and Tokens for authentication with the secure application model (linked above). The benefit to using Azure Functions is you can just schedule it as a timed job that runs periodically instead of manually running the script.

Steps

1. Follow the steps in the post on Azure Functions to add a Function App and modify the configuration settings. You should have the General Settings set to Platform of 64 Bit and the following Application Settings as environment variables:

  • ApplicationId
  • ApplicationSecret
  • tenantID
  • refreshToken
  • upn
  • ExchangeRefreshToken

2. Go to App Files> Find the Requirements.psd1 File from the dropdown and add the following dependencies:

3. Now click on the functions tab and click on +Add 

4. Select Timer Trigger and add a CRON value. Take a look at the following support article to see what kind of schedule you would like to run. In this example, I am running this script everyday at 930. 

5. After you click on add, Click on Code + Test.  A boiler plate script is shown in the run.ps1 file. What you will want to do here is simply leave the param(Timer) and then include the following text shown to import the modules that we put as our dependencies files. Click on save after these lines are entered.  

6. After you have the file saved, click on Test/Run. A new window will open on the right. Do not modify anything on that page and simply click Run. The script will connect and you will see the message below, telling you that the managed dependency is downloading. 

7. Now that our modules are loaded into our directory, we can perform a test run of our script. This script will loop through all of your customers, find users with licensed shared mailboxes, and remove them. I have include a prompt that will tell you in the log who was a licensed shared mailbox user at the customer site. It may be best to find a licensed shared mailbox user as your test case. Copy and paste the following code into the function:

using namespace System.Net
param($Timer)


Import-Module AzureADPreview -UseWindowsPowershell
Import-Module PartnerCenter -UseWindowsPowershell


$ApplicationId = $ENV:ApplicationId
$ApplicationSecret = $ENV:ApplicationSecret
$secPas = $ApplicationSecret| ConvertTo-SecureString -AsPlainText -Force
$tenantID = $ENV:tenantID
$refreshToken = $ENV:refeshToken
$ExchangeRefreshToken = $ENV:ExchangeRefreshToken
$credential = New-Object System.Management.Automation.PSCredential($ApplicationId, $secPas)
###Connect to your Own Partner Center to get a list of customers/tenantIDs #########
$aadGraphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes 'https://graph.windows.net/.default' -ServicePrincipal -Tenant $tenantID
$graphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes 'https://graph.microsoft.com/.default' -ServicePrincipal -Tenant $tenantID

Connect-AzureAD -AadAccessToken $aadGraphToken.AccessToken -AccountId $upn -MsAccessToken $graphToken.AccessToken

$customers = Get-AzureADContract
 
Write-Host "Found $($customers.Count) customers." -ForegroundColor DarkGreen


foreach ($customer in $customers) {

    Write-Host "Checking Shared Mailboxes for $($Customer.DisplayName)" -ForegroundColor Green
    $token = New-PartnerAccessToken -ApplicationId 'a0c73c16-a7e3-4564-9a95-2bdf47383716'-RefreshToken $ExchangeRefreshToken -Scopes 'https://outlook.office365.com/.default' -Tenant $customer.CustomerContextId
    $tokenValue = ConvertTo-SecureString "Bearer $($token.AccessToken)" -AsPlainText -Force
    $credential1 = New-Object System.Management.Automation.PSCredential($upn, $tokenValue)
    $customerId = $customer.DefaultDomainName
    $session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri "https://ps.outlook.com/powershell-liveid?DelegatedOrg=$($customerId)&BasicAuthToOAuthConversion=true" -Credential $credential1 -Authentication Basic -AllowRedirection
    Import-PSSession $session -AllowClobber
    $sharedMailboxes = Get-Mailbox -ResultSize Unlimited -Filter {recipienttypedetails -eq "SharedMailbox"}
    Remove-PSSession $session

try{
$CustAadGraphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes ‘https://graph.windows.net/.default’ -ServicePrincipal -Tenant $customer.CustomerContextId
    $CustGraphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes ‘https://graph.microsoft.com/.default’ -ServicePrincipal -Tenant $customer.CustomerContextId
   Connect-AzureAD -AadAccessToken $CustAadGraphToken.AccessToken -AccountId $upn -MsAccessToken $CustGraphToken.AccessToken -TenantId $customer.CustomerContextId
   $licensedUsers = Get-AzureADUser -All $true | where  {$_.AssignedLicenses}
}catch{"There was an error"}

foreach ($mailbox in $sharedMailboxes) {
    Add-Type -Path "C:\home\data\ManagedDependencies\210414162202318.r\AzureADPreview\2.0.2.134\Microsoft.Open.AzureADBeta.Graph.PowerShell.dll"
    Add-Type -Path "C:\home\data\ManagedDependencies\210414162202318.r\AzureADPreview\2.0.2.134\Microsoft.Open.AzureAD16.Graph.Client.dll"
        if ($licensedUsers.ObjectId -contains $mailbox.ExternalDirectoryObjectID) {
            Write-Host "$($mailbox.displayname) is a licensed shared mailbox" -ForegroundColor Yellow
             $userUPN="$($mailbox.userPrincipalName)"
             $userList = Get-AzureADUserLicenseDetail -ObjectID $userUPN
             $Skus = $userList.SkuId
             Write-Host $Skus
if($Skus -is [array])
    {
        $licenses = New-Object -TypeName Microsoft.Open.AzureAD.Model.AssignedLicenses
 foreach ($lic in $($Skus -split ' ')) {
            $Licenses.RemoveLicenses = $lic
            Set-AzureADUserLicense -ObjectId $userUPN -AssignedLicenses $licenses
        }

    } else {
        $licenses = New-Object -TypeName Microsoft.Open.AzureAD.Model.AssignedLicenses
        $Licenses.RemoveLicenses =  $Skus
        Set-AzureADUserLicense -ObjectId $userUPN -AssignedLicenses $licenses
    }
Disconnect-AzureAD
}
}
}

8. When you run this script, here is a sample of what you should expect to see:

Error Handling

I think one of the most common errors you might see is the following:

This simply means there is no exchange license within the tenant. It is unlikely you have many customers like this but it did come up for me in my test environment. 

Running the Script Locally

Link to Github Page with Script

If you want to run this script locally instead, I have a different version you can use for that(Note you will need to fill in the first variable with your values from the secure application model):

$ApplicationId = ""
$ApplicationSecret = ""
$secPas = $ApplicationSecret| ConvertTo-SecureString -AsPlainText -Force
$tenantID = ""
$refreshToken = ''
$ExchangeRefreshToken = ''
$upn = ''

$credential = New-Object System.Management.Automation.PSCredential($ApplicationId, $secPas)
 
$aadGraphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes 'https://graph.windows.net/.default' -ServicePrincipal -Tenant $tenantID
$graphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes 'https://graph.microsoft.com/.default' -ServicePrincipal -Tenant $tenantID
 
Connect-MsolService -AdGraphAccessToken $aadGraphToken.AccessToken -MsGraphAccessToken $graphToken.AccessToken
 
$customers = Get-MsolPartnerContract -All
 
Write-Host "Found $($customers.Count) customers for $((Get-MsolCompanyInformation).displayname)." -ForegroundColor DarkGreen
 
foreach ($customer in $customers) {

     #Get ALL Licensed Users and Find Shared  Mailboxes#
    try{
    $licensedUsers = Get-MsolUser -TenantId $customer.TenantId | Where-Object {$_.islicensed}
    Write-Host "Checking Shared Mailboxes for $($Customer.Name)" -ForegroundColor Green
    $token = New-PartnerAccessToken -ApplicationId 'a0c73c16-a7e3-4564-9a95-2bdf47383716'-RefreshToken $ExchangeRefreshToken -Scopes 'https://outlook.office365.com/.default' -Tenant $customer.TenantId -ErrorAction SilentlyContinue
    $tokenValue = ConvertTo-SecureString "Bearer $($token.AccessToken)" -AsPlainText -Force
    $credential = New-Object System.Management.Automation.PSCredential($upn, $tokenValue)
    $customerId = $customer.DefaultDomainName
    $InitialDomain = Get-MsolDomain -TenantId $customer.TenantId | Where-Object {$_.IsInitial -eq $true}
    $session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri "https://ps.outlook.com/powershell-liveid?DelegatedOrg=$($InitialDomain)&BasicAuthToOAuthConversion=true" -Credential $credential -Authentication Basic -AllowRedirection -ErrorAction SilentlyContinue
    Import-PSSession $session 
    $sharedMailboxes = Get-Mailbox -ResultSize Unlimited -Filter {recipienttypedetails -eq "SharedMailbox"}
    Remove-PSSession $session
    foreach ($mailbox in $sharedMailboxes) {
        if ($licensedUsers.ObjectId -contains $mailbox.ExternalDirectoryObjectID) {
            Write-Host "$($mailbox.displayname) is a licensed shared mailbox" -ForegroundColor Yellow
            $licenses = ($licensedUsers | Where-Object {$_.objectid -contains $mailbox.ExternalDirectoryObjectId}).Licenses
            $licenseArray = $licenses | foreach-Object {$_.AccountSkuId}
            Write-Host "Removing License" -ForegroundColor Yellow
            $mailbox | ForEach-Object {
            Set-MsolUserLicense -UserPrincipalName "$($mailbox.UserPrincipalName)" -TenantId $($customer.TenantId) -removelicenses $licenseArray -ErrorAction SilentlyContinue
            } 

}
}
}catch { "An error occurred."}
}

Final Thoughts

Azure Functions are the future of scheduled task with Powershell. I think they still have some time to evolve as I had to create additional lines of code to achieve the same functionality in the function as I can locally. I hope you find this implementation useful. Comment below if you would like to see additions or with any questions you might have. 

Share with the Community