Private/Get-Office365AccessToken.ps1

function Get-Office365AccessToken {
    <#
    .SYNOPSIS
    Acquires an OAuth2 access token for the specified resource scope using MSAL.NET directly.

    .DESCRIPTION
    Loads Microsoft.Identity.Client from whichever installed module ships it (Az.Accounts,
    Microsoft.Graph.Authentication, or MSAL.PS). If none is found, emits a one-time warning
    and returns $null so callers fall back to legacy PSCredential authentication.

    On first call a PublicClientApplication is built and cached in the module state so the
    MSAL token cache persists across calls within the same session. Subsequent calls for the
    same account are satisfied silently without a browser prompt.

    On successful interactive login the function populates:
        Office365UPN — user principal name extracted from the token account
        MsalAccount — MSAL IAccount object used for subsequent silent requests
        TenantID — tenant GUID from the token (only when not already set)

    .PARAMETER Scope
    The OAuth2 scope / resource URI to request, e.g. 'https://graph.microsoft.com/.default'
    or 'https://outlook.office365.com/.default'.
    #>

    param(
        [Parameter(Mandatory)]
        [string]$Scope
    )

    # Ensure Microsoft.Identity.Client is loaded. It ships inside Az.Accounts,
    # Microsoft.Graph.Authentication, and the archived MSAL.PS module; load it from
    # whichever is installed rather than requiring a separate download.
    if (-not ([System.AppDomain]::CurrentDomain.GetAssemblies() |
              Where-Object { $_.GetName().Name -eq 'Microsoft.Identity.Client' })) {

        $local:searchBases = @(
            (Get-Module Az.Accounts               -ListAvailable | Sort-Object Version -Descending | Select-Object -First 1).ModuleBase,
            (Get-Module Microsoft.Graph.Authentication -ListAvailable | Sort-Object Version -Descending | Select-Object -First 1).ModuleBase,
            (Get-Module MSAL.PS                   -ListAvailable | Sort-Object Version -Descending | Select-Object -First 1).ModuleBase
        ) | Where-Object { $_ }

        foreach ($local:base in $local:searchBases) {
            $local:dll = Get-ChildItem -Path $local:base -Filter 'Microsoft.Identity.Client.dll' `
                             -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1
            if ($local:dll) {
                try { Add-Type -Path $local:dll.FullName -ErrorAction Stop } catch { }
                break
            }
        }
    }

    # If still unavailable, warn once and return $null so callers use legacy auth.
    if (-not ([System.AppDomain]::CurrentDomain.GetAssemblies() |
              Where-Object { $_.GetName().Name -eq 'Microsoft.Identity.Client' })) {
        if (-not $script:myOffice365Services['MsalNetWarned']) {
            Write-Warning ('MSAL.NET (Microsoft.Identity.Client) not detected. ' +
                'Install Az, Microsoft.Graph, or MSAL.PS for modern authentication; ' +
                'you may be prompted to sign in multiple times.')
            $script:myOffice365Services['MsalNetWarned'] = $true
        }
        return $null
    }

    $local:clientId = $script:myOffice365Services['MsalClientId']

    # Build or reuse the PublicClientApplication (the MSAL token cache lives on the app instance).
    if (-not $script:myOffice365Services['MsalApp']) {
        $script:myOffice365Services['MsalApp'] =
            [Microsoft.Identity.Client.PublicClientApplicationBuilder]::Create($local:clientId).
                WithAuthority('https://login.microsoftonline.com/common').
                WithDefaultRedirectUri().
                Build()
    }

    $local:app     = $script:myOffice365Services['MsalApp']
    $local:account = $script:myOffice365Services['MsalAccount']
    $local:token   = $null

    # Try silent acquisition first (uses the MSAL token cache; no browser prompt).
    if ($local:account) {
        try {
            $local:token = $local:app.AcquireTokenSilent([string[]]@($Scope), $local:account).
                               ExecuteAsync().GetAwaiter().GetResult()
        }
        catch { }
    }

    # Interactive fallback — opens the system browser for sign-in.
    if (-not $local:token) {
        try {
            $local:token = $local:app.AcquireTokenInteractive([string[]]@($Scope)).
                               ExecuteAsync().GetAwaiter().GetResult()
        }
        catch {
            Write-Warning ('Modern auth token acquisition failed: {0}' -f $_.Exception.Message)
            return $null
        }

        if ($local:token) {
            $script:myOffice365Services['MsalAccount']  = $local:token.Account
            $script:myOffice365Services['Office365UPN'] = $local:token.Account.Username
            if (-not $script:myOffice365Services['TenantID']) {
                $script:myOffice365Services['TenantID'] = $local:token.TenantId
            }
        }
    }

    if ($local:token) { return $local:token.AccessToken }
    return $null
}