Private/Test-AzRetirementMonitorToken.ps1

function Test-AzRetirementMonitorToken {
    <#
    .SYNOPSIS
    Tests if the stored access token is valid and not expired
    .DESCRIPTION
    Decodes the JWT token and validates:
    1. Token structure (3 parts: header.payload.signature)
    2. Audience claim (must be https://management.azure.com or https://management.core.windows.net)
    3. Expiration claim (must not be expired, with 5-minute buffer)
    
    This function performs basic JWT validation without signature verification.
    Signature verification is not performed because:
    - Tokens come from trusted Azure authentication sources (Azure CLI or Az.Accounts)
    - Azure validates signatures when tokens are used for API calls
    - We only use tokens immediately and don't persist them
    
    Returns $true if token is valid, $false if expired, invalid, or incorrectly scoped
    .OUTPUTS
    System.Boolean
    #>

    [CmdletBinding()]
    [OutputType([bool])]
    param()

    if (-not $script:AccessToken) {
        return $false
    }

    try {
        # JWT tokens have 3 parts separated by dots: header.payload.signature
        $tokenParts = $script:AccessToken -split '\.'
        
        if ($tokenParts.Count -ne 3) {
            Write-Verbose "Token format is invalid"
            return $false
        }

        # Decode the payload (second part)
        $payload = $tokenParts[1]
        
        # Base64URL to Base64 conversion (add padding if needed)
        $base64 = $payload.Replace('-', '+').Replace('_', '/')
        switch ($base64.Length % 4) {
            0 { break }
            2 { $base64 += '==' }
            3 { $base64 += '=' }
            default { 
                Write-Verbose "Invalid Base64URL string length"
                return $false
            }
        }

        # Decode from Base64 and convert from JSON
        $payloadJson = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($base64))
        $tokenData = $payloadJson | ConvertFrom-Json

        # Validate audience claim - must be scoped to Azure Resource Manager
        # The audience (aud) claim identifies the intended recipient of the token
        if ($tokenData.aud) {
            # Support both current and legacy Azure Resource Manager endpoints
            # https://management.azure.com - Current standard endpoint
            # https://management.core.windows.net - Legacy endpoint for backward compatibility with older Azure CLI versions
            $validAudiences = @(
                'https://management.azure.com',
                'https://management.azure.com/',
                'https://management.core.windows.net',
                'https://management.core.windows.net/'
            )
            
            if ($tokenData.aud -notin $validAudiences) {
                Write-Verbose "Token audience '$($tokenData.aud)' is not valid for Azure Resource Manager API calls"
                return $false
            }
            
            Write-Verbose "Token audience validated: $($tokenData.aud)"
        }
        else {
            Write-Verbose "Token does not contain audience (aud) claim"
            return $false
        }

        # Check expiration time (exp claim is in Unix timestamp format)
        if ($tokenData.exp) {
            $expirationTime = [DateTimeOffset]::FromUnixTimeSeconds($tokenData.exp)
            $currentTime = [DateTimeOffset]::UtcNow
            $expirationBuffer = [TimeSpan]::FromMinutes(5)

            if ($currentTime -ge $expirationTime.Subtract($expirationBuffer)) {
                Write-Verbose "Token has expired or is about to expire at $($expirationTime.DateTime) UTC"
                return $false
            }
            
            Write-Verbose "Token is valid until $($expirationTime.DateTime) UTC"
            return $true
        }
        else {
            Write-Verbose "Token does not contain expiration claim"
            return $false
        }
    }
    catch {
        Write-Verbose "Failed to decode token: $_"
        return $false
    }
}