Public/Connect-EXO.ps1
|
function Connect-EXO { [CmdletBinding()] param() dynamicparam { if (-not (Get-Module -Name ExchangeOnlineManagement -ErrorAction SilentlyContinue)) { Import-Module -Name ExchangeOnlineManagement -ErrorAction SilentlyContinue } $local:cmd = Get-Command -Name ExchangeOnlineManagement\Connect-ExchangeOnline -ErrorAction SilentlyContinue if ($local:cmd) { $local:dict = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new() foreach ($local:p in $local:cmd.Parameters.Values) { if ([System.Management.Automation.Cmdlet]::CommonParameters -contains $local:p.Name) { continue } $local:dict.Add($local:p.Name, [System.Management.Automation.RuntimeDefinedParameter]::new( $local:p.Name, $local:p.ParameterType, $local:p.Attributes)) } return $local:dict } } process { if (-not (Get-Module -Name ExchangeOnlineManagement -ErrorAction SilentlyContinue)) { Import-Module -Name ExchangeOnlineManagement -ErrorAction SilentlyContinue } if (-not (Get-Command -Name ExchangeOnlineManagement\Connect-ExchangeOnline -ErrorAction SilentlyContinue)) { Write-Error -Message 'Cannot connect to Exchange Online - module not installed or not loading.' return } # EOM v3 uses REST mode by default; do NOT inject the legacy /PowerShell-LiveId ConnectionUri — # that forces RPS mode where delegated AccessToken auth returns 401. # For sovereign clouds use -ExchangeEnvironmentName so EOM v3 picks the right REST endpoint. # AzurePPE has no standard EOM environment name, so fall back to ConnectionUri there. if (-not $PSBoundParameters.ContainsKey('ConnectionUri') -and -not $PSBoundParameters.ContainsKey('ExchangeEnvironmentName')) { $local:eomEnv = $script:myOffice365Services['EOMEnvironmentName'] if ($local:eomEnv -and $local:eomEnv -ne 'O365Default') { $PSBoundParameters['ExchangeEnvironmentName'] = $local:eomEnv } elseif (-not $local:eomEnv -and $script:myOffice365Services['ConnectionEndpointUri']) { # AzurePPE or unknown: use legacy ConnectionUri $PSBoundParameters['ConnectionUri'] = $script:myOffice365Services['ConnectionEndpointUri'] $PSBoundParameters['AzureADAuthorizationEndpointUri'] = $script:myOffice365Services['AzureADAuthorizationEndpointUri'] } # O365Default (worldwide + GCC): omit both — EOM v3 auto-discovers the REST endpoint } if (-not $PSBoundParameters.ContainsKey('PSSessionOption')) { $PSBoundParameters['PSSessionOption'] = $script:myOffice365Services['SessionOptions'] } # Credential handling — skip when modern/cert/app auth params were supplied if ( $PSBoundParameters.ContainsKey('UserPrincipalName') -or $PSBoundParameters.ContainsKey('Certificate') -or $PSBoundParameters.ContainsKey('CertificateFilePath') -or $PSBoundParameters.ContainsKey('CertificateThumbprint') -or $PSBoundParameters.ContainsKey('AppId')) { Write-Host ('Connecting to Exchange Online ..') } else { if ( $PSBoundParameters.ContainsKey('Credential')) { Write-Host ('Connecting to Exchange Online using {0} ..' -f $PSBoundParameters['Credential'].UserName) $script:myOffice365Services['Office365Credential'] = $PSBoundParameters['Credential'] } else { # Ensure we have an account cached (MSAL) or credentials (legacy) if ( -not $script:myOffice365Services['Office365UPN'] -and -not $script:myOffice365Services['Office365Credential']) { if ($script:myOffice365Services['NoAutoConnect']) { Write-Error 'No credentials cached. Run Get-Office365Credential first or supply credentials explicitly.' return } Get-Office365Credential } if ( $script:myOffice365Services['Office365UPN']) { # Pass UPN so EOM's own internal MSAL acquires the EXO token silently via WAM SSO. # External MSAL clients cannot request EXO tokens — AAD blocks first-party to # first-party token requests from external apps (AADSTS65002). $PSBoundParameters['UserPrincipalName'] = $script:myOffice365Services['Office365UPN'] Write-Host ('Connecting to Exchange Online using {0} ..' -f $script:myOffice365Services['Office365UPN']) } elseif ( $script:myOffice365Services['Office365Credential']) { # Legacy PSCredential path Write-Host ('Connecting to Exchange Online using {0} ..' -f $script:myOffice365Services['Office365Credential'].UserName) $PSBoundParameters['Credential'] = $script:myOffice365Services['Office365Credential'] } else { Write-Host ('Connecting to Exchange Online ..') } } } try { $script:myOffice365Services['Session365'] = Connect-ExchangeOnline @PSBoundParameters } catch { # WAM (Windows Web Account Manager) broker requires a native window handle that # PowerShell console and terminal hosts never supply, causing a timeout. # Fall back to device code flow which only needs the console. if ($_.Exception.Message -like '*Operation did not start in the allotted time*' -or $_.Exception.Message -like '*Error Acquiring Token*') { Write-Warning 'WAM broker timed out — retrying with device code flow (check console for URL + code) ..' $PSBoundParameters.Remove('UserPrincipalName') | Out-Null $PSBoundParameters['Device'] = $true $script:myOffice365Services['Session365'] = Connect-ExchangeOnline @PSBoundParameters } else { throw } } if ( $script:myOffice365Services['Session365']) { Import-PSSession -Session $script:myOffice365Services['Session365'] -AllowClobber } $script:myOffice365Services['ConnectedEXO'] = $true } } |