Private/Get-AACAccessToken.ps1

function Get-AACAccessToken {
    <#
    .SYNOPSIS
        Returns a valid access token for the current session - for Azure
        Resource Manager, or with -Resource for another Azure API - silently
        refreshing it first if it is expired or about to expire.
    .DESCRIPTION
        Every command that calls an Azure REST API goes through this function
        rather than reading $script:AACSession.AccessToken directly, so the
        refresh-on-expiry behavior is guaranteed to be applied consistently.
 
        -Resource names another API by its token audience, e.g.
        https://api.loganalytics.io (Log Analytics queries) or
        https://api.applicationinsights.io (Application Insights queries).
        Its token comes from the sign-in's refresh token - no second browser
        sign-in - and is kept in the session per API until it nears expiry.
        With the default client (the Azure CLI's) these APIs are already
        consented; an App Registration of your own (Connect-AAC -ClientId)
        needs their delegated Data.Read permission.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param(
        [string] $Resource = 'https://management.azure.com'
    )

    # No web-request progress bar over a Spectre display on a token refresh.
    $ProgressPreference = 'SilentlyContinue'

    if (-not $script:AACSession) {
        throw 'Not connected to Azure. Run Connect-AAC first.'
    }
    $session = $script:AACSession
    $isArm = $Resource.TrimEnd('/') -eq 'https://management.azure.com'

    # Refresh a little early (2 minutes of slack) so a token that is valid right
    # now doesn't expire mid-flight during a slow REST call.
    if ($isArm) {
        if ((Get-Date) -lt $session.ExpiresOn.AddSeconds(-120)) {
            return $session.AccessToken
        }
    }
    else {
        $tokens = Get-AACPropertyValue -InputObject $session -Name 'Tokens'
        if ($null -eq $tokens) {
            $tokens = @{}
            $session | Add-Member -NotePropertyName 'Tokens' -NotePropertyValue $tokens -Force
        }
        $cached = $tokens[$Resource]
        if ($cached -and (Get-Date) -lt $cached.ExpiresOn.AddSeconds(-120)) {
            return $cached.AccessToken
        }
    }

    if (-not $session.RefreshToken) {
        throw 'The Azure sign-in has expired and no refresh token is available. Run Connect-AAC again.'
    }

    $tokenEndpoint = "https://login.microsoftonline.com/$($session.TenantId)/oauth2/v2.0/token"
    $scope = if ($isArm) { $session.Scope -join ' ' } else { "$($Resource.TrimEnd('/'))/.default offline_access" }
    $body = @{
        grant_type    = 'refresh_token'
        refresh_token = $session.RefreshToken
        client_id     = $session.ClientId
        scope         = $scope
    }

    try {
        $response = Invoke-RestMethod -Uri $tokenEndpoint -Method Post -Body $body -ContentType 'application/x-www-form-urlencoded' -ErrorAction Stop -Verbose:$false
    }
    catch {
        # Entra ID's own reason (AADSTS...) says more than "400 Bad Request".
        $reason = $_.Exception.Message
        if ($_.ErrorDetails -and $_.ErrorDetails.Message) {
            $details = $_.ErrorDetails.Message | ConvertFrom-Json -ErrorAction Ignore
            $description = if ($details) { Get-AACPropertyValue -InputObject $details -Name 'error_description' }
            if ($description) { $reason = ([string]$description -split '\r?\n')[0] }
        }
        if ($isArm) {
            throw "Failed to refresh the Azure access token: $($reason.TrimEnd('.')). Run Connect-AAC again."
        }
        throw "Could not get a token for $Resource from the Azure sign-in: $($reason.TrimEnd('.')). Run Connect-AAC again; with an App Registration of your own (-ClientId), give it this API's delegated Data.Read permission."
    }

    $refreshToken = Get-AACPropertyValue -InputObject $response -Name 'refresh_token'
    if ($refreshToken) {
        $session.RefreshToken = $refreshToken
    }
    $expiresOn = (Get-Date).AddSeconds([int]$response.expires_in)
    if ($isArm) {
        $session.AccessToken = $response.access_token
        $session.ExpiresOn = $expiresOn
    }
    else {
        $tokens[$Resource] = @{ AccessToken = $response.access_token; ExpiresOn = $expiresOn }
    }

    return $response.access_token
}