Public/Connect-M365Admin.ps1

<#
.SYNOPSIS
    Connects to Microsoft Graph, Exchange Online, and Security & Compliance (Purview) PowerShell.

.DESCRIPTION
    Dot-source this file, then call Connect-M365Admin. Supports two modes:
      * Certificate (app-only): pass -ClientId, -TenantId, -Organization and either
        -CertificateThumbprint (cert in the CurrentUser\My store on Windows) or
        -CertificatePath (PFX file; password read from the M365_CERT_PASSWORD env var).
      * Interactive (delegated): pass -Interactive and optionally -UserPrincipalName.
    Values can also come from environment variables: M365_TENANT_ID, M365_CLIENT_ID,
    M365_CERT_THUMBPRINT, M365_ORGANIZATION. No secrets are stored in this repo.

.PARAMETER Service
    Which services to connect. Defaults to Graph, ExchangeOnline and Purview.

.PARAMETER GraphScopes
    Delegated scopes requested in interactive mode.

.EXAMPLE
    Connect-M365Admin
    Connect-M365Admin -Interactive -UserPrincipalName admin@contoso.onmicrosoft.com

.EXAMPLE
    $env:M365_TENANT_ID = '00000000-0000-0000-0000-000000000000'
    $env:M365_CLIENT_ID = '00000000-0000-0000-0000-000000000000'
    $env:M365_CERT_THUMBPRINT = '0000000000000000000000000000000000000000'
    $env:M365_ORGANIZATION = 'contoso.onmicrosoft.com'
    Connect-M365Admin

.NOTES
    Requires Microsoft.Graph.Authentication (SDK v2) and ExchangeOnlineManagement v3.
#>


function Connect-M365Admin {
    [CmdletBinding(DefaultParameterSetName = 'Certificate')]
    param(
        [ValidateSet('Graph', 'ExchangeOnline', 'Purview')]
        [string[]]$Service = @('Graph', 'ExchangeOnline', 'Purview'),

        [Parameter(ParameterSetName = 'Certificate')]
        [string]$TenantId = $env:M365_TENANT_ID,

        [Parameter(ParameterSetName = 'Certificate')]
        [string]$ClientId = $env:M365_CLIENT_ID,

        [Parameter(ParameterSetName = 'Certificate')]
        [string]$CertificateThumbprint = $env:M365_CERT_THUMBPRINT,

        [Parameter(ParameterSetName = 'Certificate')]
        [string]$CertificatePath,

        [Parameter(ParameterSetName = 'Certificate')]
        [string]$Organization = $env:M365_ORGANIZATION,

        [Parameter(Mandatory, ParameterSetName = 'Interactive')]
        [switch]$Interactive,

        [Parameter(ParameterSetName = 'Interactive')]
        [string]$UserPrincipalName,

        [Parameter(ParameterSetName = 'Interactive')]
        [string[]]$GraphScopes = @(
            'User.Read.All', 'Directory.Read.All', 'Policy.Read.All',
            'AuditLog.Read.All', 'Reports.Read.All', 'RoleManagement.Read.Directory',
            'SecurityEvents.Read.All', 'UserAuthenticationMethod.Read.All'
        )
    )

    $ErrorActionPreference = 'Stop'

    # Build the certificate object once when a PFX file is used.
    $certificate = $null
    if ($PSCmdlet.ParameterSetName -eq 'Certificate') {
        foreach ($name in 'TenantId', 'ClientId', 'Organization') {
            if (-not (Get-Variable -Name $name -ValueOnly)) {
                throw "Missing $name. Pass it as a parameter or set the matching M365_* environment variable."
            }
        }
        if ($CertificatePath) {
            $pfxPassword = $env:M365_CERT_PASSWORD
            $certificate = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new(
                (Resolve-Path $CertificatePath).Path, $pfxPassword)
        }
        elseif (-not $CertificateThumbprint) {
            throw 'Provide -CertificateThumbprint or -CertificatePath for app-only auth.'
        }
    }

    if ('Graph' -in $Service) {
        Write-Verbose 'Connecting to Microsoft Graph...'
        if ($Interactive) {
            Connect-MgGraph -Scopes $GraphScopes -NoWelcome
        }
        elseif ($certificate) {
            Connect-MgGraph -TenantId $TenantId -ClientId $ClientId -Certificate $certificate -NoWelcome
        }
        else {
            Connect-MgGraph -TenantId $TenantId -ClientId $ClientId -CertificateThumbprint $CertificateThumbprint -NoWelcome
        }
        $ctx = Get-MgContext
        Write-Information "Graph: $($ctx.AuthType) as $($ctx.Account ?? $ctx.AppName) in tenant $($ctx.TenantId)" -InformationAction Continue
    }

    # Common splat for EXO and IPPS (same auth model, different endpoint).
    $exoParams = @{ ShowBanner = $false }
    if ($Interactive) {
        if ($UserPrincipalName) { $exoParams.UserPrincipalName = $UserPrincipalName }
    }
    else {
        $exoParams.AppId = $ClientId
        $exoParams.Organization = $Organization
        if ($certificate) { $exoParams.Certificate = $certificate }
        else { $exoParams.CertificateThumbprint = $CertificateThumbprint }
    }

    if ('ExchangeOnline' -in $Service) {
        Write-Verbose 'Connecting to Exchange Online...'
        Connect-ExchangeOnline @exoParams
        Write-Information 'Exchange Online: connected' -InformationAction Continue
    }

    if ('Purview' -in $Service) {
        Write-Verbose 'Connecting to Security & Compliance (Purview)...'
        $ippsParams = $exoParams.Clone()
        $ippsParams.Remove('ShowBanner')
        Connect-IPPSSession @ippsParams
        Write-Information 'Purview (IPPS): connected' -InformationAction Continue
    }
}