Common/Connect-Service.ps1

<#
.SYNOPSIS
    Connects to Microsoft cloud services with standardized error handling.
.DESCRIPTION
    Wraps Connect-MgGraph, Connect-ExchangeOnline, and Connect-IPPSSession
    with consistent error handling, required module checks, and scope management.
    Supports interactive, certificate, client secret, and managed identity authentication.
.PARAMETER Service
    The service to connect to: Graph, ExchangeOnline, or Purview.
.PARAMETER Scopes
    Microsoft Graph permission scopes. Only used with the Graph service.
    Defaults to 'User.Read.All' if not specified.
.PARAMETER TenantId
    The tenant ID or domain (e.g., 'contoso.onmicrosoft.com'). Optional for
    interactive auth but required for app-only auth.
.PARAMETER ClientId
    Application (client) ID for app-only authentication. Requires TenantId
    and either CertificateThumbprint or ClientSecret.
.PARAMETER CertificateThumbprint
    Certificate thumbprint for app-only authentication.
.PARAMETER ClientSecret
    Client secret for app-only authentication. Less secure than certificate auth.
.PARAMETER UserPrincipalName
    User principal name (e.g., 'admin@contoso.onmicrosoft.com') for interactive
    authentication to Exchange Online or Purview. Bypasses the Windows Authentication
    Manager (WAM) broker which can cause RuntimeBroker errors on some systems.
.PARAMETER ManagedIdentity
    Use Azure managed identity authentication. Requires the script to be running
    on an Azure resource with a system-assigned or user-assigned managed identity
    (e.g., Azure VM, Azure Functions, Azure Automation). Graph uses -Identity,
    Exchange Online uses -ManagedIdentity. Purview and Power BI do not support
    managed identity and will fall back with a warning.
.PARAMETER UseDeviceCode
    Use device code authentication flow instead of browser-based interactive auth.
    Graph uses -UseDeviceCode, Exchange Online uses -Device. Purview does not
    support device code and will fall back to browser/UPN-based auth with a warning.
.PARAMETER M365Environment
    Target cloud environment. Commercial and GCC use standard endpoints.
    GCCHigh and DoD route to sovereign cloud endpoints for Graph, Exchange,
    and Purview. Defaults to 'commercial'.
.EXAMPLE
    PS> .\Common\Connect-Service.ps1 -Service Graph -Scopes 'User.Read.All','Group.Read.All'
 
    Connects to Microsoft Graph interactively with the specified scopes.
.EXAMPLE
    PS> .\Common\Connect-Service.ps1 -Service ExchangeOnline -TenantId 'contoso.onmicrosoft.com'
 
    Connects to Exchange Online for the specified tenant.
.EXAMPLE
    PS> .\Common\Connect-Service.ps1 -Service Graph -TenantId 'contoso.onmicrosoft.com' -ClientId '00000000-0000-0000-0000-000000000000' -CertificateThumbprint 'ABC123'
 
    Connects to Microsoft Graph using certificate-based app-only auth.
.EXAMPLE
    PS> .\Common\Connect-Service.ps1 -Service Purview -UserPrincipalName 'admin@contoso.onmicrosoft.com'
 
    Connects to Purview using the specified UPN (avoids WAM broker issues).
.EXAMPLE
    PS> .\Common\Connect-Service.ps1 -Service Graph -M365Environment gcchigh -TenantId 'contoso.onmicrosoft.us'
 
    Connects to Microsoft Graph in the GCC High sovereign cloud.
#>

[CmdletBinding()]
param(
    [Parameter(Mandatory)]
    [ValidateSet('Graph', 'ExchangeOnline', 'Purview', 'PowerBI')]
    [string]$Service,

    [Parameter()]
    [string[]]$Scopes = @('User.Read.All'),

    [Parameter()]
    [string]$TenantId,

    [Parameter()]
    [string]$ClientId,

    [Parameter()]
    [string]$CertificateThumbprint,

    [Parameter()]
    [SecureString]$ClientSecret,

    [Parameter()]
    [string]$UserPrincipalName,

    [Parameter()]
    [switch]$ManagedIdentity,

    [Parameter()]
    [switch]$UseDeviceCode,

    [Parameter()]
    [ValidateSet('commercial', 'gcc', 'gcchigh', 'dod')]
    [string]$M365Environment = 'commercial'
)

$ErrorActionPreference = 'Stop'

$moduleMap = @{
    'Graph'           = 'Microsoft.Graph.Authentication'
    'ExchangeOnline'  = 'ExchangeOnlineManagement'
    'Purview'         = 'ExchangeOnlineManagement'
    'PowerBI'         = 'MicrosoftPowerBIMgmt'
}

$requiredModule = $moduleMap[$Service]

# Check that the required module is available
if (-not (Get-Module -Name $requiredModule -ListAvailable)) {
    Write-Error "Required module '$requiredModule' is not installed. Run: Install-Module -Name $requiredModule -Scope CurrentUser"
    return
}

try {
    # ------------------------------------------------------------------
    # Environment endpoint configuration
    # GCC uses the same endpoints as commercial (tenant is in the GCC
    # partition but API surface is identical). GCC High and DoD route
    # to sovereign cloud endpoints.
    # ------------------------------------------------------------------
    $envConfig = @{
        'commercial' = @{ GraphEnvironment = $null; ExoEnvironment = $null; PurviewParams = @{} }
        'gcc'        = @{ GraphEnvironment = $null; ExoEnvironment = $null; PurviewParams = @{} }
        'gcchigh'    = @{
            GraphEnvironment = 'USGov'
            ExoEnvironment   = 'O365USGovGCCHigh'
            PurviewParams    = @{
                ConnectionUri                   = 'https://ps.compliance.protection.office365.us/powershell-liveid/'
                AzureADAuthorizationEndpointUri = 'https://login.microsoftonline.us/common'
            }
        }
        'dod'        = @{
            GraphEnvironment = 'USGovDoD'
            ExoEnvironment   = 'O365USGovDoD'
            PurviewParams    = @{
                ConnectionUri                   = 'https://l5.ps.compliance.protection.office365.us/powershell-liveid/'
                AzureADAuthorizationEndpointUri = 'https://login.microsoftonline.us/common'
            }
        }
    }

    $currentEnv = $envConfig[$M365Environment]

    switch ($Service) {
        'Graph' {
            $connectParams = @{}
            if ($TenantId) { $connectParams['TenantId'] = $TenantId }

            if ($ManagedIdentity) {
                $connectParams['Identity'] = $true
            }
            elseif ($ClientId -and $CertificateThumbprint) {
                $connectParams['ClientId'] = $ClientId
                $connectParams['CertificateThumbprint'] = $CertificateThumbprint
            }
            elseif ($ClientId -and $ClientSecret) {
                $credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $ClientId, $ClientSecret
                $connectParams['ClientSecretCredential'] = $credential
            }
            else {
                $connectParams['Scopes'] = $Scopes
                if ($UseDeviceCode) {
                    $connectParams['UseDeviceCode'] = $true
                }
            }

            if ($currentEnv.GraphEnvironment) {
                $connectParams['Environment'] = $currentEnv.GraphEnvironment
            }

            # Suppress Graph SDK welcome banner (available in v2.x+)
            if ((Get-Command Connect-MgGraph -ErrorAction SilentlyContinue) -and
                (Get-Command Connect-MgGraph).Parameters.ContainsKey('NoWelcome')) {
                $connectParams['NoWelcome'] = $true
            }

            Connect-MgGraph @connectParams
            Write-Verbose "Connected to Microsoft Graph ($M365Environment)"
        }

        'ExchangeOnline' {
            $connectParams = @{
                ShowBanner = $false
            }
            if ($TenantId) { $connectParams['Organization'] = $TenantId }

            if ($ManagedIdentity) {
                $connectParams['ManagedIdentity'] = $true
            }
            elseif ($ClientId -and $CertificateThumbprint) {
                $connectParams['AppId'] = $ClientId
                $connectParams['CertificateThumbprint'] = $CertificateThumbprint
            }
            elseif ($ClientId -and $ClientSecret) {
                throw "Exchange Online does not support client secret authentication. Use -CertificateThumbprint for app-only auth."
            }
            elseif ($UseDeviceCode) {
                $connectParams['Device'] = $true
            }
            elseif ($UserPrincipalName) {
                $connectParams['UserPrincipalName'] = $UserPrincipalName
            }

            if ($currentEnv.ExoEnvironment) {
                $connectParams['ExchangeEnvironmentName'] = $currentEnv.ExoEnvironment
            }

            Connect-ExchangeOnline @connectParams
            Write-Verbose "Connected to Exchange Online ($M365Environment)"
        }

        'Purview' {
            $connectParams = @{}
            if ($TenantId) { $connectParams['Organization'] = $TenantId }

            if ($ManagedIdentity) {
                Write-Warning "Purview (Connect-IPPSSession) does not support managed identity auth. Falling back to browser-based login."
            }

            if ($ClientId -and $CertificateThumbprint) {
                $connectParams['AppId'] = $ClientId
                $connectParams['CertificateThumbprint'] = $CertificateThumbprint
            }
            elseif ($ClientId -and $ClientSecret) {
                throw "Purview does not support client secret authentication. Use -CertificateThumbprint for app-only auth."
            }
            elseif ($UserPrincipalName) {
                $connectParams['UserPrincipalName'] = $UserPrincipalName
            }

            if ($UseDeviceCode) {
                Write-Warning "Purview (Connect-IPPSSession) does not support device code auth. Falling back to browser-based login."
            }

            foreach ($key in $currentEnv.PurviewParams.Keys) {
                $connectParams[$key] = $currentEnv.PurviewParams[$key]
            }

            Connect-IPPSSession @connectParams
            Write-Verbose "Connected to Purview (Security & Compliance) ($M365Environment)"
        }

        'PowerBI' {
            $connectParams = @{}
            if ($TenantId) { $connectParams['Tenant'] = $TenantId }

            if ($ManagedIdentity) {
                throw "Power BI (Connect-PowerBIServiceAccount) does not support managed identity auth. Use -ClientId and -CertificateThumbprint for non-interactive auth."
            }
            elseif ($ClientId -and $CertificateThumbprint) {
                $connectParams['ServicePrincipal'] = $true
                $connectParams['ApplicationId'] = $ClientId
                $connectParams['CertificateThumbprint'] = $CertificateThumbprint
            }
            elseif ($ClientId -and $ClientSecret) {
                $credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $ClientId, $ClientSecret
                $connectParams['ServicePrincipal'] = $true
                $connectParams['Credential'] = $credential
            }

            Connect-PowerBIServiceAccount @connectParams -WarningAction SilentlyContinue
            Write-Verbose "Connected to Power BI ($M365Environment)"
        }
    }
}
catch {
    Write-Error "Failed to connect to $Service`: $_"
}