Private/Msal/Invoke-MsalRequest.ps1
|
<#
.SYNOPSIS Runs an MSAL request builder and returns its result; the only call to MSAL's ExecuteAsync. .DESCRIPTION Waits for the request, writing the device code messages of DeviceCodeQueue meanwhile. A Timeout of zero waits without limit; when it runs out the request is cancelled, MSAL is given 5 seconds to stop, and a TimeoutException is thrown. Otherwise it throws the MSAL exception itself (MsalUiRequiredException, MsalServiceException and the like) or a TaskCanceledException. Unit tests mock this function. .EXAMPLE Invoke-MsalRequest $Request -Timeout (New-TimeSpan -Minutes 2) Runs the request, giving up after 2 minutes. .OUTPUTS Microsoft.Identity.Client.AuthenticationResult #> function Invoke-MsalRequest { [CmdletBinding()] [OutputType([Microsoft.Identity.Client.AuthenticationResult])] param ( # An MSAL request builder (AcquireToken*ParameterBuilder), ready to run. [Parameter(Mandatory = $true, Position = 0)] [object] $Request, # Zero waits without limit. [Parameter(Mandatory = $false)] [timespan] $Timeout = [timespan]::Zero, # Messages queued by the device-code callback, written to the information stream while waiting. [Parameter(Mandatory = $false)] [AllowNull()] [System.Collections.Concurrent.ConcurrentQueue[string]] $DeviceCodeQueue ) ## The only call to MSAL's ExecuteAsync: unit tests mock this function. Returns the result, or ## throws the MSAL exception itself (MsalUiRequiredException, MsalServiceException, ...), a ## TimeoutException or a TaskCanceledException. $TokenSource = New-Object System.Threading.CancellationTokenSource $Task = $null try { $Task = $Request.ExecuteAsync($TokenSource.Token) $EndTime = [datetime]::Now.Add($Timeout) while (-not $Task.IsCompleted) { if ($DeviceCodeQueue) { Write-MsalDeviceCodeMessage $DeviceCodeQueue } if ($Timeout -ne [timespan]::Zero -and [datetime]::Now -ge $EndTime) { $TokenSource.Cancel() # MSAL ends a cancelled request quickly; do not wait on one that does not. try { [void] $Task.Wait([timespan]::FromSeconds(5)) } catch { # The cancelled task always faults; the timeout is what the caller gets. Write-Debug $_.Exception.Message } throw (New-Object System.TimeoutException ('The request did not complete within {0}.' -f $Timeout)) } Start-Sleep -Milliseconds 250 } } finally { if ($DeviceCodeQueue) { Write-MsalDeviceCodeMessage $DeviceCodeQueue } if (-not $Task -or -not $Task.IsCompleted) { $TokenSource.Cancel() } $TokenSource.Dispose() } if ($Task.IsFaulted) { $Exception = $Task.Exception while ($Exception -is [System.AggregateException] -and $Exception.InnerException) { $Exception = $Exception.InnerException } throw $Exception } if ($Task.IsCanceled) { throw (New-Object System.Threading.Tasks.TaskCanceledException $Task) } return $Task.Result } |