src/Get-AZTIGraphToken.ps1

<#
.Synopsis
    Acquire a Microsoft Graph bearer token via Azure CLI.
 
.DESCRIPTION
    Uses Azure CLI (az account get-access-token) to obtain a bearer token
    for Microsoft Graph API calls. Caches the token in a script-scope variable
    and refreshes automatically when within 5 minutes of expiry.
 
    Requires Azure CLI to be logged in ('az login'). Azure CLI automatically
    requests proper Graph API scopes during authentication, unlike Az PowerShell.
 
.PARAMETER TenantID
    Optional tenant ID to scope the token to. Without this, 'az account get-access-token'
    returns a token for whatever tenant Azure CLI's ambient context currently has active,
    which is not necessarily the tenant the caller is auditing or collecting against --
    on a multi-tenant/delegated identity (Lighthouse, GDAP, or simply an operator who is
    signed into several customer tenants) that can silently be the wrong tenant. Pass the
    same TenantID given to Invoke-AzureScout / Invoke-AZSCPermissionAudit to pin it.
 
.OUTPUTS
    [hashtable] Authorization headers ready for Invoke-RestMethod:
    @{ 'Authorization' = 'Bearer <token>'; 'Content-Type' = 'application/json' }
 
.LINK
    https://github.com/thisismydemo/azure-scout
 
.COMPONENT
    This PowerShell Module is part of Azure Scout (AZSC)
 
.NOTES
    Version: 1.1.0
    Authors: thisismydemo
    Modified: 2026-02-24 - Changed from Get-AzAccessToken to Azure CLI for proper Graph scopes
    Modified: 2026-08-08 - AB#7100 -- Added -TenantID so the token targets the tenant being
              audited/collected instead of az CLI's ambient default; cache keyed per tenant so
              a run touching multiple tenants can't return one tenant's cached token for another.
#>

function Get-AZSCGraphToken {
    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    param(
        [string]$TenantID
    )

    # Cache key: empty string means "az CLI's ambient/default tenant", same as the old
    # single-slot cache. A distinct key per TenantID prevents a token minted for tenant A
    # from being handed back for a subsequent call scoped to tenant B.
    $cacheKey = if ($TenantID) { $TenantID } else { '' }

    if (-not (Get-Variable -Name '_AZSCGraphTokenCache' -Scope Script -ErrorAction SilentlyContinue)) {
        Set-Variable -Name '_AZSCGraphTokenCache' -Scope Script -Value @{}
    }

    $now = [DateTimeOffset]::UtcNow
    $cache = $Script:_AZSCGraphTokenCache[$cacheKey]

    # Reuse cached token if still valid (more than 5 min from expiry)
    if ($cache -and $cache.ExpiresOn -gt $now.AddMinutes(5)) {
        Write-Debug ((Get-Date -Format 'yyyy-MM-dd_HH_mm_ss') + ' - Reusing cached Graph token for tenant ' + $(if ($TenantID) { $TenantID } else { '(ambient)' }) + ' (expires ' + $cache.ExpiresOn.ToString('HH:mm:ss') + ' UTC)')
        return $cache.Headers
    }

    Write-Debug ((Get-Date -Format 'yyyy-MM-dd_HH_mm_ss') + ' - Acquiring new Microsoft Graph token for tenant ' + $(if ($TenantID) { $TenantID } else { '(ambient)' }))

    try {
        # Use Azure CLI to get Graph token with proper scopes
        # Azure CLI device code authentication includes Graph API scopes by default
        $tenantArgs = @()
        if ($TenantID) { $tenantArgs = @('--tenant', $TenantID) }
        $azTokenJson = az account get-access-token --resource https://graph.microsoft.com @tenantArgs 2>&1 | Out-String

        if ($LASTEXITCODE -ne 0) {
            throw "Azure CLI failed to get Graph token. Ensure you are logged in with 'az login'. Error: $azTokenJson"
        }

        $tokenData = $azTokenJson | ConvertFrom-Json
        $plainToken = $tokenData.accessToken
        $expiresOn = [DateTimeOffset]::Parse($tokenData.expiresOn)

        $headers = @{
            'Authorization' = "Bearer $plainToken"
            'Content-Type'  = 'application/json'
        }

        # Cache for reuse, keyed per tenant
        $Script:_AZSCGraphTokenCache[$cacheKey] = [PSCustomObject]@{
            Headers   = $headers
            ExpiresOn = $expiresOn
        }

        Write-Debug ((Get-Date -Format 'yyyy-MM-dd_HH_mm_ss') + ' - Graph token acquired via Azure CLI, expires ' + $expiresOn.ToString('HH:mm:ss') + ' UTC')

        return $headers
    }
    catch {
        $errorMessage = "Failed to acquire Microsoft Graph token. Ensure Azure CLI is logged in with 'az login' and has Graph API permissions. Error: $($_.Exception.Message)"
        Write-Warning $errorMessage
        throw $errorMessage
    }
}