Src/Private/Connect-IntuneSession.ps1

function Connect-IntuneSession {
    <#
    .SYNOPSIS
    Establishes authenticated connections to Microsoft Graph for Intune reporting.
    Supports both interactive delegated auth and app-only (cert/secret) auth.
    Validates that all required scopes are present in the token, reconnecting if needed.
    Supports Global, USGov, and USGovDoD sovereign cloud environments.

    .NOTES
        Version: 0.1.2
        Author: Pai Wei Sing
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]$UserPrincipalName
    )

    #region Validate UPN
    if (-not (Test-UserPrincipalName -UserPrincipalName $UserPrincipalName)) {
        $errorMsg = "Invalid User Principal Name format: '$UserPrincipalName'. Expected format: user@domain.com"
        Write-TranscriptLog $errorMsg 'ERROR' 'AUTH'
        throw $errorMsg
    }
    #endregion

    Write-TranscriptLog "Starting connection to Microsoft Graph for Intune report: $UserPrincipalName" 'INFO' 'AUTH'

    #region Graph endpoint -- sovereign cloud support
    $EnvSetting = if ($script:Options.GraphEnvironment) { $script:Options.GraphEnvironment } else { 'Global' }
    $script:GraphEndpoint = switch ($EnvSetting) {
        'USGov'    { 'https://graph.microsoft.us' }
        'USGovDoD' { 'https://dod-graph.microsoft.us' }
        default    { 'https://graph.microsoft.com' }
    }
    if ($EnvSetting -ne 'Global') {
        Write-Host " - Graph endpoint: $($script:GraphEndpoint) ($EnvSetting)" -ForegroundColor Cyan
    }
    Write-AbrDebugLog "Graph endpoint: $($script:GraphEndpoint) (env: $EnvSetting)" 'INFO' 'AUTH'
    #endregion

    $GraphScopes = @(
        'Organization.Read.All'
        'Directory.Read.All'
        'DeviceManagementConfiguration.Read.All'   # Config profiles, compliance, scripts, baselines
        'DeviceManagementApps.Read.All'            # MAM, apps, proactive remediations
        'DeviceManagementManagedDevices.Read.All'  # Managed devices
        'DeviceManagementServiceConfig.Read.All'   # Enrollment, Autopilot
        'DeviceManagementRBAC.Read.All'            # RBAC / scope tags
        'Group.Read.All'                           # Assignment group resolution + member counts
        'User.Read.All'                            # User lookups
        'Reports.Read.All'                         # Reporting data
        'CloudPC.Read.All'                         # Windows 365 / Cloud PC
    )

    #region App-only authentication (cert or client secret)
    $AppAuthConfig = $script:Options.AppOnlyAuth
    $useAppOnly = ($AppAuthConfig -and
                   $AppAuthConfig.AppId -and
                   $AppAuthConfig.TenantId -and
                   ($AppAuthConfig.CertificateThumbprint -or $AppAuthConfig.ClientSecret))

    if ($useAppOnly) {
        Write-Host " - Authentication: App-only (AppId: $($AppAuthConfig.AppId))" -ForegroundColor Cyan
        Write-TranscriptLog "App-only auth configured -- AppId: $($AppAuthConfig.AppId)" 'INFO' 'AUTH'

        $connectParams = @{
            ClientId    = $AppAuthConfig.AppId
            TenantId    = $AppAuthConfig.TenantId
            NoWelcome   = $true
            ErrorAction = 'Stop'
        }

        if ($AppAuthConfig.CertificateThumbprint) {
            $connectParams['CertificateThumbprint'] = $AppAuthConfig.CertificateThumbprint
            Write-Host " - Auth method: Certificate (thumbprint: $($AppAuthConfig.CertificateThumbprint.Substring(0,[Math]::Min(8,$AppAuthConfig.CertificateThumbprint.Length)))...)" -ForegroundColor Cyan
        }
        elseif ($AppAuthConfig.ClientSecret) {
            # ClientSecret auth requires converting to credential
            $secSecret  = ConvertTo-SecureString $AppAuthConfig.ClientSecret -AsPlainText -Force
            $clientCred = [System.Management.Automation.PSCredential]::new($AppAuthConfig.AppId, $secSecret)
            $connectParams['ClientSecretCredential'] = $clientCred
            $connectParams.Remove('ClientId')
            Write-Host " - Auth method: Client Secret" -ForegroundColor Cyan
        }

        if ($EnvSetting -ne 'Global') {
            $connectParams['Environment'] = $EnvSetting
        }

        try {
            Invoke-WithRetry -ScriptBlock {
                Connect-MgGraph @connectParams
            } -OperationName 'Connect to Microsoft Graph (app-only)'
            Write-Host " - App-only authentication successful." -ForegroundColor Green
            $script:GraphScopesFullyConfirmed = $true
            Write-TranscriptLog "App-only auth successful" 'SUCCESS' 'AUTH'
        }
        catch {
            throw "App-only authentication failed: $($_.Exception.Message)"
        }
        return
    }
    #endregion

    #region Interactive / delegated auth with scope validation
    $ExistingGraph = $null
    try { $ExistingGraph = Get-MgContext -ErrorAction SilentlyContinue } catch { }

    if ($ExistingGraph -and $ExistingGraph.TenantId) {
        $GrantedScopes = @($ExistingGraph.Scopes)
        $MissingScopes = $GraphScopes | Where-Object { $GrantedScopes -notcontains $_ }

        if ($MissingScopes -and $MissingScopes.Count -gt 0) {
            Write-Host " - Existing session missing scope(s) -- reconnecting:" -ForegroundColor Yellow
            foreach ($s in $MissingScopes) { Write-Host " Missing: $s" -ForegroundColor Yellow }
            Write-TranscriptLog "Session scope mismatch -- missing: $($MissingScopes -join ', '). Reconnecting." 'WARNING' 'AUTH'
            try { $null = Disconnect-MgGraph -ErrorAction SilentlyContinue } catch { }
            $ExistingGraph = $null
        } else {
            Write-Host " - Reusing existing Graph session (all required scopes confirmed)." -ForegroundColor Green
            Write-TranscriptLog "Reusing existing Graph session -- all scopes verified (TenantId: $($ExistingGraph.TenantId))" 'SUCCESS' 'AUTH'
            $null = ($script:GraphScopesFullyConfirmed = $true)
        }
    }

    if (-not $ExistingGraph -or -not $ExistingGraph.TenantId) {
        Write-Host " - Connecting to Microsoft Graph..."
        Write-TranscriptLog "Connecting to Microsoft Graph with required Intune scopes" 'INFO' 'AUTH'

        $connectParams = @{
            Scopes      = $GraphScopes
            NoWelcome   = $true
            ErrorAction = 'Stop'
        }
        if ($EnvSetting -ne 'Global') { $connectParams['Environment'] = $EnvSetting }

        Invoke-WithRetry -ScriptBlock {
            Connect-MgGraph @connectParams
        } -OperationName 'Connect to Microsoft Graph (Intune)'

        $MgCtx = Get-MgContext -ErrorAction SilentlyContinue
        if ($MgCtx -and $MgCtx.TenantId) {
            Write-TranscriptLog "Microsoft Graph connection verified (TenantId: $($MgCtx.TenantId))" 'SUCCESS' 'AUTH'

            $GrantedScopes = @($MgCtx.Scopes)
            $StillMissing  = $GraphScopes | Where-Object { $GrantedScopes -notcontains $_ }
            if ($StillMissing -and $StillMissing.Count -gt 0) {
                Write-Host " - WARNING: The following scopes were not granted:" -ForegroundColor Yellow
                foreach ($s in $StillMissing) { Write-Host " Not granted: $s" -ForegroundColor Yellow }
                Write-TranscriptLog "Post-connect: still missing -- $($StillMissing -join ', ')" 'WARNING' 'AUTH'
                $null = ($script:GraphScopesFullyConfirmed = $false)
            } else {
                Write-Host " - All required scopes confirmed in token." -ForegroundColor Green
                Write-TranscriptLog "All required scopes confirmed in token" 'SUCCESS' 'AUTH'
                $null = ($script:GraphScopesFullyConfirmed = $true)
            }
        } else {
            throw "Connect-MgGraph succeeded but Get-MgContext returned no context."
        }
    }
    #endregion

    Write-Host " - Microsoft Graph connected successfully." -ForegroundColor Green
    Write-TranscriptLog "Microsoft Graph connection established for Intune report: $UserPrincipalName" 'SUCCESS' 'AUTH'
}

function Disconnect-IntuneSession {
    [CmdletBinding()]
    param()
    Write-TranscriptLog "Disconnecting Microsoft Graph session" 'INFO' 'AUTH'
    try {
        $GraphCtx = Get-MgContext -ErrorAction SilentlyContinue
        if ($GraphCtx) {
            Write-Host " - Disconnecting Microsoft Graph..."
            $null = Disconnect-MgGraph -ErrorAction SilentlyContinue
            Write-TranscriptLog "Microsoft Graph session disconnected" 'SUCCESS' 'AUTH'
        }
    } catch {
        Write-TranscriptLog "Microsoft Graph disconnect warning: $($_.Exception.Message)" 'WARNING' 'AUTH'
    }
    Write-Host " - Session disconnected." -ForegroundColor Green
    Write-TranscriptLog "Intune session disconnected" 'SUCCESS' 'AUTH'
}