Public/Get-MsalToken.ps1

<#
.SYNOPSIS
    Gets an OAuth token from Microsoft Entra ID, in the public cloud or a sovereign cloud.
.DESCRIPTION
    Gets a token through MSAL.NET for a public client (a user signs in) or a confidential client (the
    application signs in with a secret, a certificate or an assertion). Unless an application is
    piped in, the cmdlet uses the session's application for these settings, as
    Select-MsalClientApplication does, so the tokens it caches are reused by later calls.
 
    The flow follows the parameters. A confidential client uses the client credentials, or the
    authorization code with AuthorizationCode, or on-behalf-of with UserAssertion. A public client
    signs in with UserCredential (username and password), DeviceCode, Interactive,
    IntegratedWindowsAuth or Silent; with none of them, it tries the token cache, then integrated
    Windows authentication, then an interactive sign-in. Integrated Windows authentication and
    username and password are deprecated by MSAL.NET and warn when asked for.
 
    The cloud comes from Environment, AzureCloudInstance or Authority (the public cloud by default);
    parameters that point at different clouds are an error. Without Scopes, the scope is the Graph
    endpoint of the cloud followed by /.default. On an application piped in, only the tenant can
    change per request.
 
    Interactive sign-in uses MSAL's embedded window on Windows PowerShell 5.1 and the system browser
    on PowerShell 7, which needs an http://localhost redirect URI on the app registration; without
    one, PowerShell 7 falls back to device code. With the system browser, the cmdlet gives up after
    2 minutes unless Timeout says otherwise. Device code messages go to the information stream.
 
    A failure is a terminating error: GetMsalTokenFailureAuthenticationError (the MSAL.NET exception
    is in the error record), GetMsalTokenFailureOperationTimeout or GetMsalTokenFailureOperationStopped.
.EXAMPLE
    Get-MsalToken -ClientId '00000000-0000-0000-0000-000000000000' -TenantId 'contoso.onmicrosoft.com' -ClientSecret (Read-Host -AsSecureString -Prompt 'Client secret')
    Gets an application token for Microsoft Graph in the public cloud, with a client secret.
.EXAMPLE
    Get-MsalToken -ClientId '00000000-0000-0000-0000-000000000000' -TenantId 'contoso.onmicrosoft.com' -ClientCertificate (Get-Item 'Cert:\CurrentUser\My\0000000000000000000000000000000000000000') -Environment 'BleuCloud'
    Gets an application token in Bleu with a certificate; the scope is the Bleu Graph default.
.EXAMPLE
    Get-MsalToken -ClientId '00000000-0000-0000-0000-000000000000' -TenantId 'contoso.onmicrosoft.com' -Interactive -Scopes 'https://graph.microsoft.com/User.Read' -LoginHint 'user@contoso.com'
    Signs a user in interactively, with the user name filled in.
.EXAMPLE
    Get-MsalToken -ClientId '00000000-0000-0000-0000-000000000000' -TenantId 'contoso.onmicrosoft.com' -Silent -Scopes 'https://graph.microsoft.com/User.Read'
    Gets a user token from the token cache, refreshing it when needed, without any sign-in.
.EXAMPLE
    Get-MsalToken -ClientId '00000000-0000-0000-0000-000000000000' -TenantId 'contoso.onmicrosoft.com' -DeviceCode
    Signs a user in on another device, for a machine with no browser.
.EXAMPLE
    Get-MsalClientApplication -ClientId '00000000-0000-0000-0000-000000000000' | Get-MsalToken -TenantId 'fabrikam.onmicrosoft.com'
    Uses the session's application with this client id for another tenant of the same cloud.
.INPUTS
    Microsoft.Identity.Client.IPublicClientApplication, Microsoft.Identity.Client.IConfidentialClientApplication, System.String
.OUTPUTS
    Microsoft.Identity.Client.AuthenticationResult
.LINK
    Select-MsalClientApplication
.LINK
    Get-MsalEnvironment
.LINK
    Enable-MsalTokenCacheOnDisk
#>

function Get-MsalToken {
    [CmdletBinding(DefaultParameterSetName = 'PublicClient')]
    [OutputType([Microsoft.Identity.Client.AuthenticationResult])]
    param
    (
        # Identifier of the client requesting the token.
        [Parameter(Mandatory = $true, ParameterSetName = 'PublicClient', Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Parameter(Mandatory = $true, ParameterSetName = 'PublicClient-Interactive', Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Parameter(Mandatory = $true, ParameterSetName = 'PublicClient-IntegratedWindowsAuth', Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Parameter(Mandatory = $true, ParameterSetName = 'PublicClient-Silent', Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Parameter(Mandatory = $true, ParameterSetName = 'PublicClient-UsernamePassword', Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Parameter(Mandatory = $true, ParameterSetName = 'PublicClient-DeviceCode', Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Parameter(Mandatory = $true, ParameterSetName = 'ConfidentialClientSecret', Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Parameter(Mandatory = $true, ParameterSetName = 'ConfidentialClientSecret-AuthorizationCode', Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Parameter(Mandatory = $true, ParameterSetName = 'ConfidentialClientSecret-OnBehalfOf', Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Parameter(Mandatory = $true, ParameterSetName = 'ConfidentialClientCertificate', Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Parameter(Mandatory = $true, ParameterSetName = 'ConfidentialClientCertificate-AuthorizationCode', Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Parameter(Mandatory = $true, ParameterSetName = 'ConfidentialClientCertificate-OnBehalfOf', Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [string] $ClientId,

        # Secure secret of the client requesting the token.
        [Parameter(Mandatory = $true, ParameterSetName = 'ConfidentialClientSecret', ValueFromPipelineByPropertyName = $true)]
        [Parameter(Mandatory = $true, ParameterSetName = 'ConfidentialClientSecret-AuthorizationCode', ValueFromPipelineByPropertyName = $true)]
        [Parameter(Mandatory = $true, ParameterSetName = 'ConfidentialClientSecret-OnBehalfOf', ValueFromPipelineByPropertyName = $true)]
        [securestring] $ClientSecret,

        # Client assertion certificate of the client requesting the token.
        [Parameter(Mandatory = $true, ParameterSetName = 'ConfidentialClientCertificate', ValueFromPipelineByPropertyName = $true)]
        [Parameter(Mandatory = $true, ParameterSetName = 'ConfidentialClientCertificate-AuthorizationCode', ValueFromPipelineByPropertyName = $true)]
        [Parameter(Mandatory = $true, ParameterSetName = 'ConfidentialClientCertificate-OnBehalfOf', ValueFromPipelineByPropertyName = $true)]
        [System.Security.Cryptography.X509Certificates.X509Certificate2] $ClientCertificate,

        # Specifies if the x5c claim (public key of the certificate) should be sent to the STS.
        [Parameter(Mandatory = $false, ParameterSetName = 'ConfidentialClient-InputObject')]
        [Parameter(Mandatory = $false, ParameterSetName = 'ConfidentialClientCertificate')]
        [Parameter(Mandatory = $false, ParameterSetName = 'ConfidentialClientCertificate-AuthorizationCode')]
        [Parameter(Mandatory = $false, ParameterSetName = 'ConfidentialClientCertificate-OnBehalfOf')]
        [switch] $SendX5C,

        # The authorization code received from service authorization endpoint.
        [Parameter(Mandatory = $false, ParameterSetName = 'ConfidentialClient-InputObject')]
        [Parameter(Mandatory = $true, ParameterSetName = 'ConfidentialClientSecret-AuthorizationCode')]
        [Parameter(Mandatory = $true, ParameterSetName = 'ConfidentialClientCertificate-AuthorizationCode')]
        [string] $AuthorizationCode,

        # Assertion representing the user.
        [Parameter(Mandatory = $false, ParameterSetName = 'ConfidentialClient-InputObject', ValueFromPipelineByPropertyName = $true)]
        [Parameter(Mandatory = $true, ParameterSetName = 'ConfidentialClientSecret-OnBehalfOf', ValueFromPipelineByPropertyName = $true)]
        [Parameter(Mandatory = $true, ParameterSetName = 'ConfidentialClientCertificate-OnBehalfOf', ValueFromPipelineByPropertyName = $true)]
        [string] $UserAssertion,

        # Type of the assertion representing the user.
        [Parameter(Mandatory = $false, ParameterSetName = 'ConfidentialClient-InputObject', ValueFromPipelineByPropertyName = $true)]
        [Parameter(Mandatory = $false, ParameterSetName = 'ConfidentialClientSecret-OnBehalfOf', ValueFromPipelineByPropertyName = $true)]
        [Parameter(Mandatory = $false, ParameterSetName = 'ConfidentialClientCertificate-OnBehalfOf', ValueFromPipelineByPropertyName = $true)]
        [string] $UserAssertionType,

        # Address to return to upon receiving a response from the authority.
        [Parameter(Mandatory = $false, ParameterSetName = 'PublicClient', ValueFromPipelineByPropertyName = $true)]
        [Parameter(Mandatory = $false, ParameterSetName = 'PublicClient-Interactive', ValueFromPipelineByPropertyName = $true)]
        [Parameter(Mandatory = $false, ParameterSetName = 'PublicClient-IntegratedWindowsAuth', ValueFromPipelineByPropertyName = $true)]
        [Parameter(Mandatory = $false, ParameterSetName = 'PublicClient-Silent', ValueFromPipelineByPropertyName = $true)]
        [Parameter(Mandatory = $false, ParameterSetName = 'PublicClient-UsernamePassword', ValueFromPipelineByPropertyName = $true)]
        [Parameter(Mandatory = $false, ParameterSetName = 'PublicClient-DeviceCode', ValueFromPipelineByPropertyName = $true)]
        [Parameter(Mandatory = $false, ParameterSetName = 'ConfidentialClientSecret', ValueFromPipelineByPropertyName = $true)]
        [Parameter(Mandatory = $false, ParameterSetName = 'ConfidentialClientSecret-AuthorizationCode', ValueFromPipelineByPropertyName = $true)]
        [Parameter(Mandatory = $false, ParameterSetName = 'ConfidentialClientSecret-OnBehalfOf', ValueFromPipelineByPropertyName = $true)]
        [Parameter(Mandatory = $false, ParameterSetName = 'ConfidentialClientCertificate', ValueFromPipelineByPropertyName = $true)]
        [Parameter(Mandatory = $false, ParameterSetName = 'ConfidentialClientCertificate-AuthorizationCode', ValueFromPipelineByPropertyName = $true)]
        [Parameter(Mandatory = $false, ParameterSetName = 'ConfidentialClientCertificate-OnBehalfOf', ValueFromPipelineByPropertyName = $true)]
        [uri] $RedirectUri,

        # Cloud to sign in to, as an MSAL.NET AzureCloudInstance value; -Environment covers more clouds. AzureGermany is refused (closed in 2021).
        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)]
        [Microsoft.Identity.Client.AzureCloudInstance] $AzureCloudInstance,

        # Cloud to sign in to: a built-in or user-defined environment (Get-MsalEnvironment). Gives the authority and the default scope.
        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)]
        [ArgumentCompleter( { $Word = $args[2]; (Get-MsalEnvironment).Name | Where-Object { $_ -like "$Word*" } })]
        [string] $Environment,

        # Tenant identifier of the authority to issue token. It can also contain the value "consumers" or "organizations".
        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)]
        [string] $TenantId,

        # Address of the authority to issue token.
        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)]
        [uri] $Authority,

        # Sign in through the Windows authentication broker (WAM); Windows only.
        [Parameter(Mandatory = $false, ParameterSetName = 'PublicClient', ValueFromPipelineByPropertyName = $true)]
        [Parameter(Mandatory = $false, ParameterSetName = 'PublicClient-Interactive', ValueFromPipelineByPropertyName = $true)]
        [Parameter(Mandatory = $false, ParameterSetName = 'PublicClient-InputObject', ValueFromPipelineByPropertyName = $true)]
        [switch] $AuthenticationBroker,

        # Public client application to use, from New-MsalClientApplication or Select-MsalClientApplication.
        [Parameter(Mandatory = $true, ParameterSetName = 'PublicClient-InputObject', Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Microsoft.Identity.Client.IPublicClientApplication] $PublicClientApplication,

        # Confidential client application to use, from New-MsalClientApplication or Select-MsalClientApplication.
        [Parameter(Mandatory = $true, ParameterSetName = 'ConfidentialClient-InputObject', Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Microsoft.Identity.Client.IConfidentialClientApplication] $ConfidentialClientApplication,

        # Interactive request to acquire a token for the specified scopes.
        [Parameter(Mandatory = $true, ParameterSetName = 'PublicClient-Interactive')]
        [Parameter(Mandatory = $false, ParameterSetName = 'PublicClient-InputObject')]
        [switch] $Interactive,

        # Non-interactive request to acquire a security token for the signed-in user in Windows, via Integrated Windows Authentication.
        [Parameter(Mandatory = $true, ParameterSetName = 'PublicClient-IntegratedWindowsAuth')]
        [Parameter(Mandatory = $false, ParameterSetName = 'PublicClient-InputObject')]
        [switch] $IntegratedWindowsAuth,

        # Attempts to acquire an access token from the user token cache.
        [Parameter(Mandatory = $true, ParameterSetName = 'PublicClient-Silent')]
        [Parameter(Mandatory = $false, ParameterSetName = 'PublicClient-InputObject')]
        [switch] $Silent,

        # Acquires a security token on a device without a Web browser, by letting the user authenticate on another device.
        [Parameter(Mandatory = $true, ParameterSetName = 'PublicClient-DeviceCode')]
        [Parameter(Mandatory = $false, ParameterSetName = 'PublicClient-Interactive')]
        [Parameter(Mandatory = $false, ParameterSetName = 'PublicClient-InputObject')]
        [switch] $DeviceCode,

        # Array of scopes requested for resource. Default: https://<GraphEndpoint>/.default of the environment.
        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)]
        [string[]] $Scopes,

        # Array of scopes for which a developer can request consent upfront.
        [Parameter(Mandatory = $false, ParameterSetName = 'PublicClient', ValueFromPipelineByPropertyName = $true)]
        [Parameter(Mandatory = $false, ParameterSetName = 'PublicClient-Interactive', ValueFromPipelineByPropertyName = $true)]
        [Parameter(Mandatory = $false, ParameterSetName = 'PublicClient-InputObject', ValueFromPipelineByPropertyName = $true)]
        [string[]] $ExtraScopesToConsent,

        # Identifier of the user. Generally a UPN.
        [Parameter(Mandatory = $false, ParameterSetName = 'PublicClient', ValueFromPipelineByPropertyName = $true)]
        [Parameter(Mandatory = $false, ParameterSetName = 'PublicClient-Interactive', ValueFromPipelineByPropertyName = $true)]
        [Parameter(Mandatory = $false, ParameterSetName = 'PublicClient-IntegratedWindowsAuth', ValueFromPipelineByPropertyName = $true)]
        [Parameter(Mandatory = $false, ParameterSetName = 'PublicClient-Silent', ValueFromPipelineByPropertyName = $true)]
        [Parameter(Mandatory = $false, ParameterSetName = 'PublicClient-InputObject', ValueFromPipelineByPropertyName = $true)]
        [string] $LoginHint,

        # Specifies the what the interactive experience is for the user. To force an interactive authentication, use the -Interactive switch.
        [Parameter(Mandatory = $false, ParameterSetName = 'PublicClient', ValueFromPipelineByPropertyName = $true)]
        [Parameter(Mandatory = $false, ParameterSetName = 'PublicClient-Interactive', ValueFromPipelineByPropertyName = $true)]
        [Parameter(Mandatory = $false, ParameterSetName = 'PublicClient-InputObject', ValueFromPipelineByPropertyName = $true)]
        [ArgumentCompleter( {
                $wordToComplete = $args[2]
                [Microsoft.Identity.Client.Prompt].DeclaredFields | Where-Object { $_.IsPublic -eq $true -and $_.IsStatic -eq $true -and $_.Name -like "$wordToComplete*" } | Select-Object -ExpandProperty Name
            })]
        [string] $Prompt,

        # Identifier of the user with associated password.
        [Parameter(Mandatory = $true, ParameterSetName = 'PublicClient-UsernamePassword', ValueFromPipelineByPropertyName = $true)]
        [Parameter(Mandatory = $false, ParameterSetName = 'PublicClient-InputObject', ValueFromPipelineByPropertyName = $true)]
        [pscredential]
        [System.Management.Automation.Credential()]
        $UserCredential,

        # Correlation id to be used in the authentication request.
        [Parameter(Mandatory = $false)]
        [guid] $CorrelationId,

        # This parameter will be appended as is to the query string in the HTTP authentication request to the authority.
        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)]
        [hashtable] $ExtraQueryParameters,

        # Ignore any access token in the user token cache and attempt to acquire new access token using the refresh token for the account if one is available.
        [Parameter(Mandatory = $false, ParameterSetName = 'PublicClient')]
        [Parameter(Mandatory = $false, ParameterSetName = 'PublicClient-Silent')]
        [Parameter(Mandatory = $false, ParameterSetName = 'PublicClient-InputObject')]
        [Parameter(Mandatory = $false, ParameterSetName = 'ConfidentialClientSecret')]
        [Parameter(Mandatory = $false, ParameterSetName = 'ConfidentialClientCertificate')]
        [Parameter(Mandatory = $false, ParameterSetName = 'ConfidentialClient-InputObject')]
        [switch] $ForceRefresh,

        # Specifies if the public client application should used an embedded web browser or the system default browser
        [Parameter(Mandatory = $false, ParameterSetName = 'PublicClient', ValueFromPipelineByPropertyName = $true)]
        [Parameter(Mandatory = $false, ParameterSetName = 'PublicClient-Interactive', ValueFromPipelineByPropertyName = $true)]
        [Parameter(Mandatory = $false, ParameterSetName = 'PublicClient-InputObject', ValueFromPipelineByPropertyName = $true)]
        [switch] $UseEmbeddedWebView,

        # Specifies the timeout threshold for MSAL.net operations.
        [Parameter(Mandatory = $false)]
        [timespan] $Timeout
    )


    process {
        ## The application: passed in, or the session's one for these settings (built when missing).
        switch -Wildcard ($PSCmdlet.ParameterSetName) {
            "PublicClient-InputObject" {
                [Microsoft.Identity.Client.IPublicClientApplication] $ClientApplication = $PublicClientApplication
                break
            }
            "ConfidentialClient-InputObject" {
                [Microsoft.Identity.Client.IConfidentialClientApplication] $ClientApplication = $ConfidentialClientApplication
                break
            }
            "PublicClient*" {
                $paramSelectMsalClientApplication = Select-MsalBoundParameter $PSBoundParameters -CommandName Select-MsalClientApplication -CommandParameterSets "PublicClient"
                [Microsoft.Identity.Client.IPublicClientApplication] $ClientApplication = Select-MsalClientApplication @paramSelectMsalClientApplication
                break
            }
            "ConfidentialClientSecret*" {
                $paramSelectMsalClientApplication = Select-MsalBoundParameter $PSBoundParameters -CommandName Select-MsalClientApplication -CommandParameterSets "ConfidentialClientSecret"
                [Microsoft.Identity.Client.IConfidentialClientApplication] $ClientApplication = Select-MsalClientApplication @paramSelectMsalClientApplication
                break
            }
            "ConfidentialClientCertificate*" {
                $paramSelectMsalClientApplication = Select-MsalBoundParameter $PSBoundParameters -CommandName Select-MsalClientApplication -CommandParameterSets "ConfidentialClientCertificate"
                [Microsoft.Identity.Client.IConfidentialClientApplication] $ClientApplication = Select-MsalClientApplication @paramSelectMsalClientApplication
                break
            }
        }

        ## Cloud of the request, checked against the application's; the default scope comes from it.
        $CloudParameters = @{}
        foreach ($Key in 'Environment', 'AzureCloudInstance', 'Authority', 'TenantId') { if ($PSBoundParameters.ContainsKey($Key)) { $CloudParameters[$Key] = $PSBoundParameters[$Key] } }
        # A piped application binds its own Authority property to -Authority: that one asks for nothing.
        $ApplicationAuthority = $ClientApplication.AppConfig.Authority.AuthorityInfo.CanonicalAuthority
        if ($CloudParameters.ContainsKey('Authority') -and $PSCmdlet.ParameterSetName -like '*-InputObject' -and ([uri] $CloudParameters['Authority']).AbsoluteUri.TrimEnd('/') -eq $ApplicationAuthority.AbsoluteUri.TrimEnd('/')) {
            $CloudParameters.Remove('Authority')
        }
        try { $Resolved = Resolve-MsalEnvironment @CloudParameters -ApplicationAuthority $ApplicationAuthority }
        catch { $PSCmdlet.ThrowTerminatingError((New-Object System.Management.Automation.ErrorRecord ($_.Exception, 'MsalEnvironmentResolution', [System.Management.Automation.ErrorCategory]::InvalidArgument, $null))) }
        if (-not $PSBoundParameters.ContainsKey('Scopes')) {
            if (-not $Resolved.DefaultScope) {
                $Message = 'No environment has the authority host {0}, so there is no default scope: give -Scopes, or add the cloud with Add-MsalEnvironment.' -f $ClientApplication.AppConfig.Authority.AuthorityInfo.Host
                $PSCmdlet.ThrowTerminatingError((New-Object System.Management.Automation.ErrorRecord ((New-Object System.ArgumentException $Message), 'MsalNoDefaultScope', [System.Management.Automation.ErrorCategory]::InvalidArgument, $null)))
            }
            $Scopes = $Resolved.DefaultScope
        }

        $Flow = Resolve-MsalTokenFlow $PSCmdlet.ParameterSetName $PSBoundParameters $ClientApplication
        Write-Debug ('Acquiring a token for application [{0}] with the {1} flow' -f $ClientApplication.AppConfig.ClientId, $Flow)

        if ($Flow -eq 'Default') {
            ## The token cache first, then integrated Windows authentication, then interactive.
            $paramGetMsalToken = Select-MsalBoundParameter -NamedParameter $PSBoundParameters -CommandName 'Get-MsalToken' -CommandParameterSet 'PublicClient-InputObject' -ExcludeParameters 'PublicClientApplication'
            Write-Verbose ('Attempting Silent Authentication to Application with ClientId [{0}]' -f $ClientApplication.AppConfig.ClientId)
            try {
                $AuthenticationResult = Get-MsalToken -Silent -PublicClientApplication $ClientApplication @paramGetMsalToken
                if (Test-MsalMissingScope $AuthenticationResult $Scopes) {
                    $AuthenticationResult = Get-MsalToken -Interactive -PublicClientApplication $ClientApplication @paramGetMsalToken
                }
            }
            catch [Microsoft.Identity.Client.MsalUiRequiredException] {
                Write-Debug ('{0}: {1}' -f $_.Exception.GetType().Name, $_.Exception.Message)
                Write-Verbose ('Attempting Integrated Windows Authentication to Application with ClientId [{0}]' -f $ClientApplication.AppConfig.ClientId)
                try {
                    # An attempt the caller did not ask for: no deprecation warning for it.
                    $AuthenticationResult = Get-MsalToken -IntegratedWindowsAuth -PublicClientApplication $ClientApplication -WarningAction SilentlyContinue @paramGetMsalToken
                    if (Test-MsalMissingScope $AuthenticationResult $Scopes) {
                        $AuthenticationResult = Get-MsalToken -Interactive -PublicClientApplication $ClientApplication @paramGetMsalToken
                    }
                }
                catch {
                    Write-Debug ('{0}: {1}' -f $_.Exception.GetType().Name, $_.Exception.Message)
                    Write-Verbose ('Attempting Interactive Authentication to Application with ClientId [{0}]' -f $ClientApplication.AppConfig.ClientId)
                    $AuthenticationResult = Get-MsalToken -Interactive -PublicClientApplication $ClientApplication @paramGetMsalToken
                }
            }
            return $AuthenticationResult
        }

        ## One request builder per flow.
        $ForceRefreshValue = $null
        if ($PSBoundParameters.ContainsKey('ForceRefresh')) { $ForceRefreshValue = [bool] $ForceRefresh }
        $DeviceCodeCallback = $null
        # -WarningAction does not reach the module's private functions on its own: pass it on.
        $WarningParameters = @{}
        if ($PSBoundParameters.ContainsKey('WarningAction')) { $WarningParameters['WarningAction'] = $PSBoundParameters['WarningAction'] }
        switch ($Flow) {
            'UsernamePassword' { $Request = Get-MsalUsernamePasswordRequest $ClientApplication -Scopes $Scopes -Credential $UserCredential @WarningParameters }
            'DeviceCode' {
                $DeviceCodeCallback = Get-MsalDeviceCodeCallback
                $Request = Get-MsalDeviceCodeRequest $ClientApplication -Scopes $Scopes -Callback $DeviceCodeCallback.Callback
            }
            'Interactive' {
                $EmbeddedWebView = $null
                if ($PSBoundParameters.ContainsKey('UseEmbeddedWebView')) { $EmbeddedWebView = [bool] $UseEmbeddedWebView }
                $Request = Get-MsalInteractiveRequest $ClientApplication -Scopes $Scopes -ExtraScopesToConsent $ExtraScopesToConsent -LoginHint $LoginHint -Prompt $Prompt -UseEmbeddedWebView $EmbeddedWebView
                # The system browser cannot tell when the person gives up: do not wait forever.
                if (!$Timeout -and ($EmbeddedWebView -eq $false -or $PSVersionTable.PSEdition -eq 'Core')) { $Timeout = New-TimeSpan -Minutes 2 }
            }
            'IntegratedWindowsAuth' { $Request = Get-MsalIntegratedWindowsAuthRequest $ClientApplication -Scopes $Scopes -LoginHint $LoginHint @WarningParameters }
            'Silent' { $Request = Get-MsalSilentRequest $ClientApplication -Scopes $Scopes -LoginHint $LoginHint -ForceRefresh $ForceRefreshValue }
            'ClientCredential' { $Request = Get-MsalClientCredentialRequest $ClientApplication -Scopes $Scopes -ForceRefresh $ForceRefreshValue -SendX5C:$SendX5C }
            'OnBehalfOf' { $Request = Get-MsalOnBehalfOfRequest $ClientApplication -Scopes $Scopes -UserAssertion $UserAssertion -UserAssertionType $UserAssertionType -SendX5C:$SendX5C }
            'AuthorizationCode' { $Request = Get-MsalAuthorizationCodeRequest $ClientApplication -Scopes $Scopes -AuthorizationCode $AuthorizationCode -SendX5C:$SendX5C }
        }

        $Options = @{}
        # Only the tenant can change per request; an application built here already has it.
        if ($Resolved.TenantId -and $PSCmdlet.ParameterSetName -like '*-InputObject') { $Options['TenantId'] = $Resolved.TenantId }
        if ($PSBoundParameters.ContainsKey('CorrelationId')) { $Options['CorrelationId'] = $CorrelationId }
        if ($ExtraQueryParameters) { $Options['ExtraQueryParameters'] = $ExtraQueryParameters }
        [void] (Add-MsalRequestOption $Request @Options)

        if (!$Timeout) { $Timeout = [timespan]::Zero }
        $Queue = $null
        if ($DeviceCodeCallback) { $Queue = $DeviceCodeCallback.Queue }
        try {
            return (Invoke-MsalRequest $Request -Timeout $Timeout -DeviceCodeQueue $Queue)
        }
        catch {
            $Exception = $_.Exception
            if ($Exception -is [System.TimeoutException]) { $ErrorId = 'GetMsalTokenFailureOperationTimeout'; $Category = [System.Management.Automation.ErrorCategory]::OperationTimeout }
            elseif ($Exception -is [System.Threading.Tasks.TaskCanceledException]) { $ErrorId = 'GetMsalTokenFailureOperationStopped'; $Category = [System.Management.Automation.ErrorCategory]::OperationStopped }
            else { $ErrorId = 'GetMsalTokenFailureAuthenticationError'; $Category = [System.Management.Automation.ErrorCategory]::AuthenticationError }
            $PSCmdlet.ThrowTerminatingError((New-Object System.Management.Automation.ErrorRecord ($Exception, $ErrorId, $Category, $Request)))
        }
    }
}