Private/Invoke-TokenRequest.ps1

function Invoke-TokenRequest {
    <#
    .SYNOPSIS
        Sends a token request to the Microsoft identity platform v2.0 token endpoint.
 
    .DESCRIPTION
        Centralised wrapper around Invoke-RestMethod for all OAuth2 token requests.
        Handles error parsing and returns the raw token response object.
        Never logs token values — only diagnostic metadata.
 
    .PARAMETER TokenEndpoint
        The full URL of the token endpoint, e.g.
        https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token
 
    .PARAMETER Body
        A hashtable containing the form-encoded body parameters for the token request.
 
    .NOTES
        Author: Nickolaj Andersen & Jan Ketil Skanke
        Contact: @NickolajA @JankeSkanke
        Created: 2026-02-19
 
        Version history:
        1.0.0 - (2026-02-19) Script created
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$TokenEndpoint,

        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [hashtable]$Body
    )
    Process {
        Write-Verbose -Message "Requesting token from endpoint: $TokenEndpoint"
        Write-Verbose -Message "Grant type: $($Body['grant_type'])"

        try {
            $response = Invoke-RestMethod -Uri $TokenEndpoint -Method Post -Body $Body -ContentType 'application/x-www-form-urlencoded' -ErrorAction Stop
            Write-Verbose -Message "Token request successful. Token expires in $($response.expires_in) seconds."
            return $response
        }
        catch [System.Exception] {
            $errorMessage = $PSItem.Exception.Message
            
            # Try to parse error details from the response
            try {
                $errorDetails = $null
                if ($PSVersionTable.PSVersion.Major -ge 6) {
                    # PowerShell 7+
                    if ($PSItem.ErrorDetails.Message) {
                        $errorDetails = $PSItem.ErrorDetails.Message | ConvertFrom-Json
                    }
                }
                else {
                    # PowerShell 5.1
                    if ($PSItem.Exception.Response) {
                        $streamReader = [System.IO.StreamReader]::new($PSItem.Exception.Response.GetResponseStream())
                        $streamReader.BaseStream.Position = 0
                        $streamReader.DiscardBufferedData()
                        $errorDetails = $streamReader.ReadToEnd() | ConvertFrom-Json
                    }
                }

                if ($errorDetails) {
                    $errorMessage = "Token request failed: $($errorDetails.error) - $($errorDetails.error_description)"
                }
            }
            catch {
                # If we can't parse the error body, use the original exception message
            }

            throw $errorMessage
        }
    }
}