Private/Get-MyWebApiToken.ps1

function Get-MyWebApiToken {
    # Returns a valid bearer token, refreshing via client_credentials if expired.
    [CmdletBinding()]
    param()

    if (-not $script:MyWebApiContext) {
        throw 'Not connected. Call Connect-MyWebApi first.'
    }

    $ctx = $script:MyWebApiContext
    $stillValid = $ctx.AccessToken -and $ctx.ExpiresAt -and
                  ($ctx.ExpiresAt -gt [DateTimeOffset]::UtcNow.AddSeconds(30))
    if ($stillValid) { return $ctx.AccessToken }

    if (-not $ctx.ClientId) {
        # Pre-set token with no refresh credentials -- return as-is (may be expired; server will 401).
        return $ctx.AccessToken
    }

    # Refresh: discover the token endpoint, then request a client_credentials token.
    $disco = Invoke-RestMethod -Method Get -Uri ("{0}/.well-known/openid-configuration" -f $ctx.Authority.TrimEnd('/'))
    $tokenEndpoint = $disco.token_endpoint

    $bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($ctx.ClientSecret)
    try {
        $plainSecret = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr)
        $body = @{
            grant_type    = 'client_credentials'
            client_id     = $ctx.ClientId
            client_secret = $plainSecret
            scope         = $ctx.Scope
        }
        $resp = Invoke-RestMethod -Method Post -Uri $tokenEndpoint -Body $body `
            -ContentType 'application/x-www-form-urlencoded'
    }
    finally {
        # Free the unmanaged BSTR buffer (zeroed) and drop the managed plaintext reference.
        if ($bstr -ne [IntPtr]::Zero) { [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) }
        $plainSecret = $null
    }

    $ctx.AccessToken = $resp.access_token
    $ctx.ExpiresAt   = [DateTimeOffset]::UtcNow.AddSeconds([int]$resp.expires_in)
    return $ctx.AccessToken
}