It gets tedious connecting to all of the Office 365 PowerShell components. MSOnline, Exchange Online, Skype for Business Online, and more – they all need to be connected individually. This isn’t always so bad day-to-day, but when you have a big project that involves repeatedly checking data in both, it’s a huge waste of time. Also, our Get-MITUser function currently relies on being connected to both MSOnline and Exchange Online, and we will soon be adding Skype for Business Online. For both Get-MITUser and otherwise, it will be more efficient to automate the process of connecting to them.
Some people may stick something like the below in their PowerShell profile, where $Credential
is previously set – for example either by prompting for it with Get-Credential, or decrypting it from a saved secure string.
Connect-MsolService -Credential $Credential $ExSession = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://outlook.office365.com/powershell-liveid/ -Credential $Credential -Authentication Basic -AllowRedirection Import-PSSession -Session $ExSession -DisableNameChecking $CsOnlineSession = New-CsOnlineSession -Credential $Credential Import-PSSession -Session $CsOnlineSession -DisableNameChecking
This is perfectly fine if it suits your needs. However I like to create more versatile cmdlet that handles all of this. In particular:
- Connecting to Office 365 PowerShell is paramount, but if it can’t connect to Exchange Online PowerShell or Skype for Business Online PowerShell, we want it to just warn the user. This could either be because the required modules aren’t installed, or the 365 tenant does not include Exchange or Skype.
- It’s useful to be able to easily disconnect and reconnect, particularly to different Office 365 tenants using different usernames and passwords. We don’t want to have to close and reopen PowerShell.
- In our module we would like to be able to set some script-scope variables that indicate the connectivity status, allowing other cmdlets like Get-MITUser to rely on this. I also like to export some of these variables so that they can be viewed for troubleshooting.
(Another thing I have done in the past is, for 365 Partners with partner delegation to their customers’ 365 tenants, allow selection of the customer / tenant from a list (a makeshift table using Out-GridView) to avoid using messy -TenantId
parameters and New-PSSession commands. This is very useful for partners but also very complex, and has no use for non-partners, so I won’t be covering it now.)
Putting it together
Script-scope variables
First, outside of any functions we declare and initialise our script variables.
#region Exported Variables # These MilneIT_* variables are all exported. $script:MilneIT_365_MSOnline_Connected = $false # Are we connected to Office 365? $script:MilneIT_365_MSOnline_ConnectionError = $null # Any error connecting to Office 365 $script:MilneIT_365_Exchange_Connected = $false # Are we connected to Exchange Online? $script:MilneIT_365_Exchange_PSSession = $null # The Exchange PSSession $script:MilneIT_365_Exchange_Module = $null # The temporary module created by Import-PSSession $script:MilneIT_365_Exchange_ConnectionError = $null # Any error connecting to Exchange Online $script:MilneIT_365_CsOnline_Connected = $false # Are we connected to Skype for Business Online? $script:MilneIT_365_CsOnline_PSSession = $null # The Skype For Business Online PSSession. $script:MilneIT_365_CsOnline_Module = $null # The temporary module created by Import-PSSession $script:MilneIT_365_CsOnline_ConnectionError = $null # Any error connecting to Skype For Business Online #endregion Exported Variables
At the end of the script, we also modify our Export-ModuleMember line to export any script variables beginning with MilneIT_
. When the module is imported, these can be accessed as global variables. If / when the module is removed, these variables are also removed alongside it.
Export-ModuleMember -Function "*-MIT*" -Variable "MilneIT_*"
Connect-MIT365
Now we begin our Connect-MIT365 cmdlet. This takes five parameters:
- A mandatory
$Credential
parameter, for the username and password for connecting to the Office 365 components. If this is not supplied, the user will be prompted. - Switches
$Office365
,$ExchangeOnline
and$SkypeForBusinessOnline
which determine whether we connect to these individual components. By default we try to connect to everything. - A
$Force
switch, which handles how the cmdlet handles the situation where we are already connected.
Function Connect-MIT365 { [cmdletbinding()] Param( [Parameter(mandatory=$true)] [System.Management.Automation.PSCredential] $Credential, [switch] $Office365 = $true, [switch] $ExchangeOnline = $true, [switch] $SkypeForBusinessOnline = $true, [switch] $Force = $false ) try {
We now connect to our three 365 components one-by-one. You will notice some recurring themes throughout:
- If we are already connected, we warn the user and don’t try to connect. This is unless
-Force
is specified, in which case we disconnect first. (We define the Disconnect-MIT365 cmdlet later on). - If we fail to connect, we either write an error or a warning, but we do not halt the entire command.
First, we try to connect to Office 365 / “MSOnline” PowerShell. This is fairly straightforward as we just need to run Connect-MsolService. There are very few “legitimate” reasons why we would fail to connect to Office 365 PowerShell but succeed in connecting to Exchange Online or Skype for Business Online, so we write an error if we fail (as opposed to just warn).
<# Office 365 #> if ($script:MilneIT_365_MSOnline_Connected -and (-not $Force) ) { Write-Warning 'Office 365: Not attempting as already connected. Use -Force to override.' } elseif ($Office365) { if ($Force) { Disconnect-MIT365 -Office365:$true -ExchangeOnline:$false -SkypeForBusinessOnline:$false } #end if ($Force) Write-Verbose '[Connect-MIT365] Office 365: Attempting to connect...' try { Connect-MsolService -Credential $Credential -ErrorAction Stop Write-Debug '[Connect-MIT365] Office 365: Connected.' $script:MilneIT_365_MSOnline_Connected = $true } catch { <# Unlike with Exchange Online & Skype for Business Online, if we can't connect here we really want to display an error rather than a warning. But we do still want to continue regardless, so we don't throw. #> $script:MilneIT_365_MSOnline_ConnectionError = $_ $ErrorMessage = 'Office 365: Could not connect: {0}. The full error is saved to $MilneIT_365_MSOnline_ConnectionError' -F $_ Write-Error $ErrorMessage } #end catch } else { Write-Verbose '[Connect-MIT365] Office 365: Not attempting to connect.' } #end else
Next we connect to Exchange Online. This is where it gets tricky as we need to connect to and import a PSSession. Furthermore, unlike when importing a PSSession interactively, we need to worry about scope here.
Import-PSSession normally creates a temporary module containing the session’s cmdlets / functions, and imports that module into the local scope. Running this in a function means that the cmdlets and functions will be accessible within the cmdlet/function but not usable by the user on the interactive console. Import-PSSession has no way to change this behaviour. However, we can first run Import-PSSession, and then afterwards run Import-Module on the temporary module and specify to import it into the global scope, meaning that the cmdlets can be accessed on the interactive console.
<# Exchange Online #> if ($script:MilneIT_365_Exchange_Connected -and (-not $Force) ) { Write-Warning 'Exchange Online: Not attempting as already connected. Use -Force to override.' } elseif ($ExchangeOnline) { if ($Force) { Disconnect-MIT365 -Office365:$false -ExchangeOnline:$true -SkypeForBusinessOnline:$false } #end if ($Force) Write-Verbose '[Connect-MIT365] Exchange Online: Attempting to connect...' try { Write-Debug '[Connect-MIT365] Exchange Online: Establishing PSSession.' $script:MilneIT_365_Exchange_PSSession = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://outlook.office365.com/powershell-liveid/ -Credential $Credential -Authentication Basic -AllowRedirection -ErrorAction Stop <# When we run Import-PSSession, a temporary module is created and imported into the *local* scope. As we are running Import-PSSession in a function, we need it in the *global* scope, as otherwise the cmdlets cannot be seen or run by the user. This is not possible with Import-PSSession. However, after running Import-PSSession we can then import the module using Import-Module into the Global Scope. #> Write-Debug '[Connect-MIT365] Exchange Online: Importing PSSession into the local scope.' $script:MilneIT_365_Exchange_Module = Import-PSSession -Session $script:MilneIT_365_Exchange_PSSession -DisableNameChecking -ErrorAction Stop -AllowClobber Write-Debug '[Connect-MIT365] Exchange Online: Importing module into the global scope.' Import-Module $script:MilneIT_365_Exchange_Module -Global -DisableNameChecking -ErrorAction Stop Write-Debug '[Connect-MIT365] Exchange Online: Connected.' $script:MilneIT_365_Exchange_Connected = $true } catch { # Save the error to an exported variable. $script:MilneIT_365_Exchange_ConnectionError = $_ Write-Warning 'Exchange Online: Could not connect. This is expected if the tenant is not licenced for Exchange Online. Otherwise review the error saved to $MilneIT_365_Exchange_ConnectionError.' } #end catch } else { Write-Verbose '[Connect-MIT365] Exchange Online: Not attempting to connect.' } #end else
Now, Skype for Business Online. With Exchange Online, you use the generic New-PSSession cmdlet to establish the session to Microsoft’s servers. With Skype for Business Online, you specifically need the New-CsOnlineSession cmdlet from the SkypeOnlineConnector module. So first off, we check that this module is actually present – and if not give a more helpful warning to the user (or if it’s an entirely different error, throw).
<# Skype for Business Online #> if ($script:MilneIT_365_CsOnline_Connected -and (-not $Force) ) { Write-Warning 'Skype for Business Online: Not attempting as already connected. Use -Force to override.' } elseif ($SkypeForBusinessOnline) { if ($Force) { Disconnect-MIT365 -Office365:$false -ExchangeOnline:$false -SkypeForBusinessOnline:$true } #end if ($Force) Write-Verbose '[Connect-MIT365] Skype for Business Online: Attempting to connect...' # First try to import the connector module. $SkypeConnectorModuleImported = $false try { Write-Debug '[Connect-MIT365] Skype for Business Online: Importing SkypeOnlineConnector module.' Import-Module 'SkypeOnlineConnector' -ErrorAction Stop -Global Write-Debug '[Connect-MIT365] Skype for Business Online: Imported SkypeOnlineConnector module.' $SkypeConnectorModuleImported = $true } catch [System.IO.FileNotFoundException] { $script:MilneIT_365_CsOnline_ConnectionError = $_ if ($_.Exception.Message -eq $CsOnlineModuleError) { Write-Debug '[Connect-MIT365] Skype for Business Online: Import of SkypeOnlineConnector failed as module is not installed.' Write-Warning 'Skype for Business Online: Did not connect as the SkypeOnlineConnector module does not appear to be installed. If this is not expected, review the error saved to $MilneIT_365_CsOnline_ConnectionError.' } else { Write-Debug '[Connect-MIT365] Skype for Business Online: Import of SkypeOnlineConnector failed for unhandled reason.' throw } #end else } #end catch [System.IO.FileNotFoundException]
After this, it’s largely the same as Exchange save for using New-CsOnlineSession instead of New-PSSession.
# Now try to establish the PSSession. if ($SkypeConnectorModuleImported) { # This isn't a lot different from Exchange Online. try { Write-Debug '[Connect-MIT365] Skype for Business Online: Establishing PSSession.' $script:MilneIT_365_CsOnline_PSSession = New-CsOnlineSession -Credential $Credential -ErrorAction Stop Write-Debug '[Connect-MIT365] Skype for Business Online: Importing PSSession into the local scope.' $script:MilneIT_365_CsOnline_Module = Import-PSSession -Session $script:MilneIT_365_CsOnline_PSSession -DisableNameChecking -ErrorAction Stop -AllowClobber Write-Debug '[Connect-MIT365] Skype for Business Online: Importing module into the global scope.' Import-Module $script:MilneIT_365_CsOnline_Module -Global -DisableNameChecking -ErrorAction Stop Write-Debug '[Connect-MIT365] Skype for Business Online: Connected.' $script:MilneIT_365_CsOnline_Connected = $true } catch { $script:MilneIT_365_CsOnline_ConnectionError = $_ Write-Warning 'Skype for Business Online: Could not connect. This is expected if the tenant is not licenced for Skype for Business Online. Otherwise review the error saved to $MilneIT_365_CsOnline_ConnectionError.' } #end catch } #end if ($SkypeConnectorModuleImported) } else { Write-Verbose '[Connect-MIT365] Skype for Business Online: Not attempting to connect.' } #end else
That’s our Connect-MIT365 function completed.
} catch { throw } #end catch } #end Function Connect-MIT365
Disconnect-MIT365
As for the disconnect function, it takes largely the same parameters as Connect-MIT365, though no credential is required.
Function Disconnect-MIT365 { [cmdletbinding()] Param( [switch] $Office365 = $true, [switch] $ExchangeOnline = $true, [switch] $SkypeForBusinessOnline = $true, [switch] $Force = $false )
First, we “disconnect” Office 365 PowerShell. There isn’t actually an opposite cmdlet to Connect-MsolService, nor is there exactly a need for one – you simply run Connect-MsolService again and it will overwrite the previous connection. If you need to change Office 365 tenants, you just run it again with different credentials. But we want the behaviour to appear uniform between this and the other 365 components that can and should be explicitly disconnected.
if ($Office365 -and ($script:MilneIT_365_MSOnline_Connected -or $Force) ) { # This doesn't actually do anything other than unset the variables indicating we are connected. Write-Warning 'Office 365: Note that Office 365 PowerShell cannot be disconnected, but you can now reconnect with Connect-MIT365 or Connect-MsolService.' $script:MilneIT_365_MSOnline_ConnectionError = $null $script:MilneIT_365_MSOnline_Connected = $false } #end if
Now for Exchange Online and Skype for Business Online each, we do three things:
- Remove the module, which removes all functions and cmdlets*.
- Disconnect the PSSession. If we don’t do this the session will remain connected whilst PowerShell is kept open, and will linger for a bit after PowerShell is closed, which can cause issues with concurrent session limits.
- *I said removing the module removes all functions and cmdlets, however it doesn’t remove the proxy functions properly, likely as we imported the module into the global scope and Remove-Module can’t undo that. As a workaround, we locate the proxy functions in the
Functions:\
PSDrive and remove them using Remove-Item.
if ($ExchangeOnline -and ($script:MilneIT_365_Exchange_Connected -or $Force) ) { Write-Debug '[Disconnect-MIT365]: Disconnecting from Exchange Online' Remove-Module $script:MilneIT_365_Exchange_Module -Force Remove-PSSession -Session $script:MilneIT_365_Exchange_PSSession -ErrorAction SilentlyContinue <# Removing the module above does not remove the proxy functions. This is likely as we imported the module into the global scope, which Remove-Module cannot touch here. The workaround is to remove the proxy functions directly. #> Get-ChildItem Function:\ | Where-Object Source -eq $MilneIT_365_Exchange_Module.Name | Remove-Item -Force $script:MilneIT_365_Exchange_PSSession = $null $script:MilneIT_365_Exchange_Module = $null $script:MilneIT_365_Exchange_ConnectionError = $null $script:MilneIT_365_Exchange_Connected = $false } #end if if ($SkypeForBusinessOnline -and ($script:MilneIT_365_CsOnline_Connected -or $Force) ) { Write-Debug '[Disconnect-MIT365]: Disconnecting from Skype for Business Online' Remove-Module $script:MilneIT_365_CsOnline_Module -Force Remove-PSSession -Session $script:MilneIT_365_CsOnline_PSSession -ErrorAction SilentlyContinue Get-ChildItem Function:\ | Where-Object Source -eq $MilneIT_365_CsOnline_Module.Name | Remove-Item -Force $script:MilneIT_365_CsOnline_PSSession = $null $script:MilneIT_365_CsOnline_Module = $null $script:MilneIT_365_CsOnline_ConnectionError = $null $script:MilneIT_365_CsOnline_Connected = $false } #end if } #end Function Disconnect-MIT365
Automatically disconnect sessions
Above I mentioned the problems with lingering PSSessions. One way we can mitigate this is to modify the module so that whenever it is unloaded, it disconnects the sessions for us. It’s easy to specify a ScriptBlock of arbitrary commands that run when we unload the module, and in this case we just need to run Disconnect-MIT365.
$MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = { Disconnect-MIT365 }
Trying it out
Here we can see that we can connect, and immediately run cmdlets from MSOnline, Exchange Online, and Skype for Business Online. And we can also run our custom Get-MITUser cmdlet which currently uses MSOnline and Exchange Online.
PS> $Lab01Credential = Get-Credential PS> Connect-MIT365 -Credential $Lab01Credential PS> Get-MsolUser UserPrincipalName DisplayName isLicensed ----------------- ----------- ---------- Roy@milneitlab01.onmicrosoft.com Roy Hitchcock True Brady2@milneitlab01.onmicrosoft.com Brady True Brady@milneitlab01.onmicrosoft.com Brady Tyree True Will2@milneitlab01.onmicrosoft.com Will Cook True Sandra2@milneitlab01.onmicrosoft.com Sandra Martinez True Sandra3@milneitlab01.onmicrosoft.com Sandra Phillips True Mary@milneitlab01.onmicrosoft.com Mary Johnson True Sandra@milneitlab01.onmicrosoft.com Sandra Martinez True Will@milneitlab01.onmicrosoft.com Will Richard True Vicki@milneitlab01.onmicrosoft.com Vicki Marsh True PS> Get-Mailbox Name Alias ServerName ProhibitSendQuota ---- ----- ---------- ----------------- Brady2 Brady2 mmxp123mb0062 99 GB (106,300,440,576 bytes) Brady Brady loxp123mb1062 99 GB (106,300,440,576 bytes) DiscoverySearchMailbox... DiscoverySearchMa... mm1p123mb1068 50 GB (53,687,091,200 bytes) Mary Mary mm1p123mb1084 99 GB (106,300,440,576 bytes) Sandra2 Sandra2 mmxp123mb0653 99 GB (106,300,440,576 bytes) Sandra Sandra mmxp123mb0959 99 GB (106,300,440,576 bytes) Sandra3 SandraP loxp123mb1335 99 GB (106,300,440,576 bytes) Vicki Vicki mm1p12301mb1609 99 GB (106,300,440,576 bytes) Will2 Will mmxp123mb0751 99 GB (106,300,440,576 bytes) PS> Get-CsOnlineUser | ft DisplayName,SipAddress DisplayName SipAddress ----------- ---------- Brady Tyree sip:Brady@milneitlab01.onmicrosoft.com Roy Hitchcock sip:Roy@milneitlab01.onmicrosoft.com Sandra Martinez sip:Sandra2@milneitlab01.onmicrosoft.com Mary Ford sip:c4f2b7c39cbb42f18dcffce8f82f44b6Mary2@milneitlab01.onmicrosoft.com Vicki Marsh sip:Vicki@milneitlab01.onmicrosoft.com Sandra Martinez sip:Sandra@milneitlab01.onmicrosoft.com Sandra Phillips sip:Sandra3@milneitlab01.onmicrosoft.com Mary Johnson sip:Mary@milneitlab01.onmicrosoft.com Brady sip:Brady2@milneitlab01.onmicrosoft.com Will Cook sip:Will2@milneitlab01.onmicrosoft.com Will Richard sip:Will@milneitlab01.onmicrosoft.com PS> Get-MITUser Vicki DisplayName UserPrincipalName PrimarySmtpAddress MsolUserFound MailboxFound ----------- ----------------- ------------------ ------------- ------------ Vicki Marsh Vicki@milneitlab01.onmicrosoft.com Vicki@milneitlab01.onmicrosoft.com True True
We can now disconnect and then connect to another tenant using a different credential. This tenant doesn’t have Skype for Business Online – so it fails to connect and thus warns us, but we know it’s not a problem.
PS> Disconnect-MIT365 WARNING: Office 365: Note that Office 365 PowerShell cannot be disconnected, but you can now reconnect with Connect-MIT365 or Connect-MsolService. PS> $OtherTenantCredential = Get-Credential PS> Connect-MIT365 -Credential $OtherTenantCredential WARNING: Skype for Business Online: Could not connect. This is expected if the tenant is not licenced for Skype for Business Online. Otherwise review the error saved to $MilneIT_365_CsOnline_ConnectionError. PS> Get-MITUser john@example.com DisplayName UserPrincipalName PrimarySmtpAddress MsolUserFound MailboxFound ----------- ----------------- ------------------ ------------- ------------ John Smith john@example.com john@example.som True True
If we run Get-PSSession we can see that two sessions are currently open (one for Exchange Online, one for Skype for Business Online). We could disconnect them using Disconnect-MIT365; but here we see that if we unload the entire MilneIT module, it automatically disconnects the PSSessions for us.
PS> Get-PSSession Id Name ComputerName ComputerType State ConfigurationName Availability -- ---- ------------ ------------ ----- ----------------- ------------ 5 Session5 admingb1.onl... RemoteMachine Opened Microsoft.PowerShell Available 4 Session4 outlook.offi... RemoteMachine Opened Microsoft.Exchange Available PS> Remove-Module MilneIT WARNING: Office 365: Note that Office 365 PowerShell cannot be disconnected, but you can now reconnect with Connect-MIT365 or Connect-MsolService. PS> Get-PSSession PS>
The full MilneIT.psm1 module as of this blog post can be viewed on Bitbucket here. You can always view the latest version of the full repository here.