functions/get-d365lcsapitokeninteractive.ps1

<#
    .SYNOPSIS
        Get OAuth 2.0 token for LCS via an interactive sign-in flow
         
    .DESCRIPTION
        Get an OAuth 2.0 access token for the LCS API, using an interactive sign-in flow based on the device authentication flow
         
        Your clipboard will be set with a device code, and your default browser will navigate to "https://microsoft.com/devicelogin"
        You will have to paste in the device code, and complete an ordinary sign-in with your credentials
        When your sign-in is complete, it will pick up the OAuth 2.0 token from Azure AD
         
        The token is requested with the LcsApiUri as the resource / scope, to ensure it is valid against the correct LCS API endpoint
         
        The output object can be piped directly into Set-D365LcsApiConfig to save the token details as default values
         
    .PARAMETER LcsApiUri
        URI / URL to the LCS API you want to use
         
        The value depends on where your LCS project is located. There are multiple valid URI's / URL's
         
        Valid options:
        "https://lcsapi.lcs.dynamics.com"
        "https://lcsapi.eu.lcs.dynamics.com"
        "https://lcsapi.fr.lcs.dynamics.com"
        "https://lcsapi.sa.lcs.dynamics.com"
        "https://lcsapi.uae.lcs.dynamics.com"
        "https://lcsapi.ch.lcs.dynamics.com"
        "https://lcsapi.no.lcs.dynamics.com"
        "https://lcsapi.lcs.dynamics.cn"
        "https://lcsapi.gov.lcs.microsoftdynamics.us"
         
        Default value can be configured using Set-D365LcsApiConfig
         
    .PARAMETER Timeout
        Instruct the cmdlet how long time you need to be able to complete the interactive logon
         
        The default value is: 300 seconds
         
        Note: The parameter doesn't show up in the intellisense when tabbing through all available parameters, as it shouldn't be necessary to change the value
         
    .PARAMETER EnableException
        This parameters disables user-friendly warnings and enables the throwing of exceptions
        This is less user friendly, but allows catching exceptions in calling scripts
         
    .EXAMPLE
        PS C:\> Get-D365LcsApiTokenInteractive
         
        This will start an interactive sign-in process against Azure AD.
        It will utilize the active LCS configuration for the LcsApiUri (Resource).
        It will copy the device code into your clipboard.
        It will start the default browser and navigate to "https://microsoft.com/devicelogin".
        It will wait the default amount of seconds for you to complete the interactive sign-in.
         
        It will use the default LCS configuration details that are stored in the configuration store.
         
    .EXAMPLE
        PS C:\> Get-D365LcsApiTokenInteractive -LcsApiUri "https://lcsapi.eu.lcs.dynamics.com"
         
        This will start an interactive sign-in process against Azure AD.
        It will request a token with the "https://lcsapi.eu.lcs.dynamics.com" LCS API as the resource / scope.
        It will copy the device code into your clipboard.
        It will start the default browser and navigate to "https://microsoft.com/devicelogin".
        It will wait the default amount of seconds for you to complete the interactive sign-in.
         
    .EXAMPLE
        PS C:\> Get-D365LcsApiTokenInteractive | Set-D365LcsApiConfig -ProjectId 123456789
         
        This will start an interactive sign-in process against Azure AD and save the needed details.
        The output object received from Get-D365LcsApiTokenInteractive is piped directly to Set-D365LcsApiConfig.
        Set-D365LcsApiConfig will save the access_token(BearerToken), refresh_token(RefreshToken), expires_on(ActiveTokenExpiresOn) and resource(LcsApiUri).
         
        This should be your default approach, so you don't have to supply the same parameters for every single cmdlet.
         
    .EXAMPLE
        PS C:\> Get-D365LcsApiTokenInteractive -LcsApiUri "https://lcsapi.lcs.dynamics.com" | Set-D365LcsApiConfig
         
        This will start an interactive sign-in process against Azure AD and save the needed details.
        It will request a token with the "https://lcsapi.lcs.dynamics.com" (NON-EUROPE) LCS API as the resource / scope.
        The output object received from Get-D365LcsApiTokenInteractive is piped directly to Set-D365LcsApiConfig.
        Set-D365LcsApiConfig will save the access_token(BearerToken), refresh_token(RefreshToken), expires_on(ActiveTokenExpiresOn) and resource(LcsApiUri).
         
    .LINK
        Get-D365LcsApiConfig
         
    .LINK
        Get-D365LcsApiToken
         
    .LINK
        Set-D365LcsApiConfig
         
    .LINK
        Invoke-D365LcsApiRefreshToken
         
    .NOTES
        Tags: LCS, Api, OAuth, Token, JWT, DeviceAuth, Device, DeviceCode
         
        Inspiration: https://blog.simonw.se/getting-an-access-token-for-azuread-using-powershell-and-device-login-flow/
         
        Author: Mötz Jensen (@Splaxi)
         
#>


function Get-D365LcsApiTokenInteractive {
    [CmdletBinding()]
    [OutputType()]
    param (
        [string] $LcsApiUri = $Script:LcsApiLcsApiUri,

        [Parameter(DontShow = $true)]
        [int] $Timeout = 300,

        [switch] $EnableException
    )

    begin {
        Invoke-TimeSignal -Start

        if ([System.String]::IsNullOrEmpty($LcsApiUri)) {
            $messageString = "It seems that you didn't supply a valid value for the LcsApiUri parameter. You need to specify the LcsApiUri parameter or add a configuration with the <c='em'>Set-D365LcsApiConfig</c> cmdlet."
            Write-PSFMessage -Level Host -Message $messageString
            Stop-PSFFunction -Message "Stopping because of errors." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', ''))) -ErrorRecord $_
            return
        }

        if ($LcsApiUri.Substring($LcsApiUri.Length - 1) -eq "/") {
            Write-PSFMessage -Level Verbose -Message "The LcsApiUri parameter had a trailing slash, which shouldn't be there. Removing the trailing slash." -Target $LcsApiUri
            $LcsApiUri = $LcsApiUri.Substring(0, $LcsApiUri.Length - 1)
        }

        # Known ClientId for PowerShell in Azure AD
        $clientId = '1950a258-227b-4e31-a9cf-717495945fc2'
    }

    process {
        if (Test-PSFFunctionInterrupt) { return }

        $azureUriToken = $Script:AADOAuthEndpoint
        if ([System.String]::IsNullOrEmpty($azureUriToken)) {
            $azureUriToken = "https://login.microsoftonline.com/common/oauth2/token"
        }

        $azureUriDeviceCode = $azureUriToken -replace "/token$", "/devicecode"

        $DeviceCodeRequestParams = @{
            Method = 'POST'
            Uri    = $azureUriDeviceCode
            Body   = @{
                resource  = $LcsApiUri
                client_id = $clientId
            }
        }

        $DeviceCodeRequest = Invoke-RestMethod @DeviceCodeRequestParams

        $DeviceCodeRequest.user_code | Set-Clipboard
        Write-PSFMessage -Level Host -Message "The device code <c='em'>$($DeviceCodeRequest.user_code)</c> has been copied into your clipboard."
        Start-Sleep -Seconds 2
        Write-PSFMessage -Level Host -Message "Will start the default browser and have it open the <c='em'>$($DeviceCodeRequest.verification_url)</c> page, where you need to <c='em'>paste</c> the code in and complete sign-in with your credentials."
        Start-Sleep -Seconds 2
        Start-Process $DeviceCodeRequest.verification_url

        $TokenRequestParams = @{
            Method = 'POST'
            Uri    = $azureUriToken
            Body   = @{
                grant_type = "urn:ietf:params:oauth:grant-type:device_code"
                code       = $DeviceCodeRequest.device_code
                client_id  = $clientId
            }
        }

        $TimeoutTimer = [System.Diagnostics.Stopwatch]::StartNew()
        $tokenObj = $null
        while ([string]::IsNullOrEmpty($tokenObj.access_token)) {
            if (Test-PSFFunctionInterrupt) { return }

            if ($TimeoutTimer.Elapsed.TotalSeconds -gt $Timeout) {
                $messageString = "The login session <c='em'>timed out</c>. You have <c='em'>$Timeout seconds</c> to complete the sign-in operation."
                Write-PSFMessage -Level Host -Message $messageString
                Stop-PSFFunction -Message "Stopping because login took too long." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', '')))
                return
            }
            $tokenObj = try {
                Invoke-RestMethod @TokenRequestParams -ErrorAction Stop
            }
            catch {
                $Message = $_.ErrorDetails.Message | ConvertFrom-Json
                if ($Message.error -ne "authorization_pending") {
                    throw
                }
            }
            Start-Sleep -Seconds 1
        }

        if ($null -ne $tokenObj -and -not ($tokenObj.PSObject.Properties.Name -contains "resource")) {
            Add-Member -InputObject $tokenObj -MemberType NoteProperty -Name "resource" -Value $LcsApiUri -Force
        }

        $tokenObj
    }

    end {
        Invoke-TimeSignal -End
    }
}