Back
Featured image of post Remove access to all Exchange Online Mailboxes

Remove access to all Exchange Online Mailboxes

During the COVID-19 crisis, I onboarded several organizations to Microsoft Office 365. Many of them started new because the want Microsoft Teams for Video Conferencing and collaboration.

Some of these organizations decided to start with some workloads, but doesn’t want to use Exchange Online, at least not yet! Some decided to wait, some because of the given timeframe.

The easiest way: Remove the Exchange Online licensing option! Fine, but without an Exchange Online license option some of the key features in Microsoft Teams and/or Planner will not work. Or at least doesn’t work as expected.

So, we tried Conditional access first (would require an additional license but was one of the first ideas). Not an option, because blocking the Exchange online will also cause many issues with Microsoft Teams. Even if you allow (exclude) Microsoft Teams, SharePoint and Planner.

I wasn’t able change the CAS Mailbox policies! Please don’t get me wrong here: The policies have the values, but i wasn’t able to find a way to change it.

But I was able to remove the access via Powershell, so we decided to create something that can run as a scheduled job:

#requires -Version 3.0

<#
      .SYNOPSIS
      Remove the access to Outlook for all Mailboxes in an Microsoft Office 365 Tenant

      .DESCRIPTION
      Remove the access to Outlook for all Mailboxes in an Microsoft Office 365 Tenant
      It will remove access to OWA (Outlook Web Application), Exchange Active Sync (EAS), Outlook App and Outlook (part of the Office Suite).

      .PARAMETER CredentialUser
      The UPN of the admin user

      .PARAMETER CredentialFile
      File where the credential will be stored

      Make sure that this is secured!

      .PARAMETER ProxyAccessType
      Determines which mechanism is used to resolve the host name. The acceptable values for this parameter are:
      - IEConfig
      - WinHttpConfig
      - AutoDetect
      - NoProxyServer
      - None

      The default value is None.

      For information about the values of this parameter, see the description of the System.Management.Automation.Remoting.ProxyAccessTypehttp://go.microsoft.com/fwlink/?LinkId=144756 (http://go.microsoft.com/fwlink/?LinkId=144756) enumeration in the Microsoft Developer Network (MSDN) library.

      .EXAMPLE
      PS C:\> .\Approve-CASMailboxSettings.ps1

      .EXAMPLE
      PS C:\> .\Approve-CASMailboxSettings.ps1 -verbose

      .NOTES
      I created the script to run automated (via Windows scheduler) and it will save the password in a plain text file.
      You might want to use another option to gain access to Exchaneg Online

      Please check all values before using the script!

      TODO: Run the script once before using it as scheduled task! This will create and save the credentials.
#>
[CmdletBinding(ConfirmImpact = 'None')]
param
(
   [Parameter(ValueFromPipeline,
   ValueFromPipelineByPropertyName)]
   [ValidateNotNullOrEmpty()]
   [Alias('Username', 'AdminUser')]
   [string]
   $CredentialUser = 'youradmin.user@contoso.com',
   [Parameter(ValueFromPipeline,
   ValueFromPipelineByPropertyName)]
   [ValidateNotNullOrEmpty()]
   [Alias('CredFile', 'SecretFile')]
   [string]
   $CredentialFile = ($env:LOCALAPPDATA + '\exocreds.txt'),
   [Parameter(ValueFromPipeline,
   ValueFromPipelineByPropertyName)]
   [ValidateSet('IEConfig', 'WinHttpConfig', 'AutoDetect', 'NoProxyServer', 'None', IgnoreCase = $true)]
   [ValidateNotNullOrEmpty()]
   [Alias('PSSessionOptionProxy')]
   [string]
   $ProxyAccessType = 'None'
)

begin
{
   # Admin User (Global Admin or min. Exchange Online Admin role)
   if (-not ($CredentialUser))
   {
      $CredentialUser = 'youradmin.user@contoso.com'
   }

   # Where to store the password?
   if (-not ($CredentialFile))
   {
      $CredentialFile = ($env:LOCALAPPDATA + '\exocreds.txt')
   }
}

process
{
   #region CredentialHandler
   try
   {
      if (-not (Test-Path -Path $CredentialFile -ErrorAction SilentlyContinue))
      {
         # Do we have any credentials in memory (variable)
         if (-not ($ExoCreds))
         {
            #
            $paramGetCredential = @{
               Message     = 'Bitte mit einem Exchange Online Admin Benutzer anmelden'
               UserName    = $CredentialUser
               ErrorAction = 'Stop'
            }
            $ExoCreds = (Get-Credential @paramGetCredential)
         }

         # Splat the parameters
         $paramOutFile = @{
            FilePath    = $CredentialFile
            Force       = $true
            Encoding    = 'utf8'
            ErrorAction = 'Stop'
            Confirm     = $false
         }

         # Save the file
         $null = ($ExoCreds.Password | ConvertFrom-SecureString | Out-File @paramOutFile)
      }
      else
      {
         # Splat the parameters
         $paramGetContent = @{
            Path        = $CredentialFile
            Force       = $true
            ErrorAction = 'Stop'
         }
         $paramConvertToSecureString = @{
            ErrorAction = 'Stop'
         }

         # Read and convert the file wit the password
         $PwdSecureString = (Get-Content @paramGetContent | ConvertTo-SecureString @paramConvertToSecureString)

         # Splat the parameters
         $paramNewObject = @{
            TypeName     = 'System.Management.Automation.PSCredential'
            ArgumentList = $CredentialUser, $PwdSecureString
         }

         # Create the credential object
         $ExoCreds = (New-Object @paramNewObject)

         # Remove the password string from memory
         $PwdSecureString = $null
      }
   }
   catch
   {
      #region ErrorHandler
      # get error record
      [Management.Automation.ErrorRecord]$e = $_

      # retrieve information about runtime error
      $info = [PSCustomObject]@{
         Exception = $e.Exception.Message
         Reason    = $e.CategoryInfo.Reason
         Target    = $e.CategoryInfo.TargetName
         Script    = $e.InvocationInfo.ScriptName
         Line      = $e.InvocationInfo.ScriptLineNumber
         Column    = $e.InvocationInfo.OffsetInLine
      }

      $info | Out-String | Write-Verbose

      Write-Error -Message ($info.Exception) -ErrorAction Stop

      # Only here to catch a global ErrorAction overwrite
      break
      #endregion ErrorHandler
   }
   #endregion CredentialHandler

   #region ConnectExchangeOnline
   try
   {
      # Proxy Handling
      <#
            -ProxyAccessType <ProxyAccessType>
            Determines which mechanism is used to resolve the host name. The acceptable values for this parameter are:
            - IEConfig
            - WinHttpConfig
            - AutoDetect
            - NoProxyServer
            - None

            The default value is None.
            For information about the values of this parameter, see the description of the System.Management.Automation.Remoting.ProxyAccessTypehttp://go.microsoft.com/fwlink/?LinkId=144756 (http://go.microsoft.com/fwlink/?LinkId=144756) enumeration in the Microsoft Developer Network (MSDN) library.

            Source:
            Get-Help New-PSSessionOption -Detailed
      #>
      if ($ProxyAccessType)
      {
         # Splat the parameters
         $paramNewPSSessionOption = @{
            ProxyAccessType = $ProxyAccessType
            ErrorAction     = 'Stop'
         }

         # Do we need a proxy to access Office 365?
         $ProxyOptions = (New-PSSessionOption @paramNewPSSessionOption)
      }

      # Cleanup
      $ExoSession = $null

      # Splat the parameters
      $paramGetPSSession = @{
         ErrorAction = 'SilentlyContinue'
      }
      $paramRemovePSSession = @{
         ErrorAction = 'SilentlyContinue'
         Confirm     = $false
      }

      # Remove all existing Exchange Online Sessions
      $null = (Get-PSSession @paramGetPSSession | Where-Object {
            $_.ComputerName -eq 'outlook.office365.com'
      } | Remove-PSSession @paramRemovePSSession)

      # Splat the parameters
      $paramNewPSSession = @{
         ConfigurationName = 'Microsoft.Exchange'
         ConnectionUri     = 'https://outlook.office365.com/powershell-liveid/'
         Credential        = $ExoCreds
         Authentication    = 'Basic'
         AllowRedirection  = $true
         ErrorAction       = 'Stop'
      }

      # Proxy settings needed?
      if ($ProxyOptions)
      {
         $paramNewPSSession.SessionOption = $ProxyOptions
      }

      # Create the session
      $ExoSession = (New-PSSession @paramNewPSSession)

      # Splat the parameters
      $paramImportPSSession = @{
         Session             = $ExoSession
         DisableNameChecking = $true
         AllowClobber        = $true
         ErrorAction         = 'Stop'
         WarningAction       = 'Continue'
      }

      # Create the Session
      $null = (Import-PSSession @paramImportPSSession)
   }
   catch
   {
      #region ErrorHandler
      # get error record
      [Management.Automation.ErrorRecord]$e = $_

      # retrieve information about runtime error
      $info = [PSCustomObject]@{
         Exception = $e.Exception.Message
         Reason    = $e.CategoryInfo.Reason
         Target    = $e.CategoryInfo.TargetName
         Script    = $e.InvocationInfo.ScriptName
         Line      = $e.InvocationInfo.ScriptLineNumber
         Column    = $e.InvocationInfo.OffsetInLine
      }

      $info | Out-String | Write-Verbose

      Write-Error -Message ($info.Exception) -ErrorAction Stop

      # Only here to catch a global ErrorAction overwrite
      break
      #endregion ErrorHandler
   }
   #endregion ConnectExchangeOnline

   #region SetCASMailbox
   try
   {
      # Check if the session is alive
      if (-not (Get-Command -Name Get-CASMailbox))
      {
         # Splat the parameters
         $paramWriteError = @{
            Exception   = 'Es scheint ein Problem mit der Exchange Online Verbindung zu geben!'
            Message     = 'Die erforderlichen Exchnage Online Befehle wurden nicht gefunden!'
            Category    = 'ResourceUnavailable'
            ErrorAction = 'Stop'
         }
         Write-Error @paramWriteError

         # Make sure we are done!
         throw
      }

      # Splat the parameters
      $paramGetCASMailbox = @{
         ResultSize    = 'unlimited'
         Filter        = {
            (name -notlike 'DiscoverysearchMailbox*')
         }
         ErrorAction   = 'Stop'
         WarningAction = 'Continue'
      }
      $paramSetCASMailbox = @{
         ActiveSyncEnabled                = $false
         ImapEnabled                      = $false
         MAPIEnabled                      = $false
         OutlookMobileEnabled             = $false
         OWAEnabled                       = $false
         OWAforDevicesEnabled             = $false
         PopEnabled                       = $false
         SmtpClientAuthenticationDisabled = $false
         UniversalOutlookEnabled          = $false
         Confirm                          = $false
         ErrorAction                      = 'Continue'
         WarningAction                    = 'Continue'
      }

      # Remove the outlook access from all mailboxes
      $null = (Get-CASMailbox @paramGetCASMailbox | Set-CASMailbox @paramSetCASMailbox)
   }
   catch
   {
      #region ErrorHandler
      # get error record
      [Management.Automation.ErrorRecord]$e = $_

      # retrieve information about runtime error
      $info = [PSCustomObject]@{
         Exception = $e.Exception.Message
         Reason    = $e.CategoryInfo.Reason
         Target    = $e.CategoryInfo.TargetName
         Script    = $e.InvocationInfo.ScriptName
         Line      = $e.InvocationInfo.ScriptLineNumber
         Column    = $e.InvocationInfo.OffsetInLine
      }

      $info | Out-String | Write-Verbose

      Write-Error -Message ($info.Exception) -ErrorAction Stop

      # Only here to catch a global ErrorAction overwrite
      break
      #endregion ErrorHandler
   }
   #endregion SetCASMailbox
}

end
{
   # Cleanup
   $ExoSession = $null

   # Splat the parameters
   $paramGetPSSession = @{
      ErrorAction = 'SilentlyContinue'
   }
   $paramRemovePSSession = @{
      ErrorAction = 'SilentlyContinue'
      Confirm     = $false
   }

   # Remove all existing Exchange Online Sessions
   $null = (Get-PSSession @paramGetPSSession | Where-Object {
         $_.ComputerName -eq 'outlook.office365.com'
   } | Remove-PSSession @paramRemovePSSession)
}

There is also a GIST available for the script above.

As you might see in the script above, the connection is automated. But it will only work if you have legacy authentication enabled for the user that will connect to Office 365! If you have modern authentication enabled, the script above will not work! A possible workaround for this: Create a Conditional Access Rule with an exception for the user! But you will need a AzureAD Premium P1, or higher subscription to enable custom policies like this.

The only drawback: The Tile on the Office portal is always available!

Office Portal will show the Outlook tile

But the access is not possible!

Access to Outlook Web Application is disabled

We also tried to access this mailbox from a mobile device and from Outlook (Office App).

But it seems to work fine within Microsoft Teams:

Calendar in Microsoft Teams Microsoft Teams calendar entry

Please note: The Invites work! Even the answers work (See above).