Private/Get-WindowsDeviceLinkDeviceCodeToken.ps1

function Get-WindowsDeviceLinkDeviceCodeToken {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][ValidateNotNullOrEmpty()][string]$TenantId,
        [ValidateNotNullOrEmpty()][string]$ClientId = '14d82eec-204b-4c2f-b7e8-296a70dab67e',
        [ValidateNotNullOrEmpty()][string]$Scope = 'https://graph.microsoft.com/DeviceManagementServiceConfig.ReadWrite.All offline_access openid profile',
        [scriptblock]$RequestScript,
        [scriptblock]$SleepScript
    )

    [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
    $authority = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0"
    $deviceCodeUri = "$authority/devicecode"
    $tokenUri = "$authority/token"

    if ($RequestScript) {
        $deviceCodeResponse = & $RequestScript 'DeviceCode' $deviceCodeUri @{ client_id=$ClientId; scope=$Scope }
    }
    else {
        $deviceCodeResponse = Invoke-RestMethod -Method POST -Uri $deviceCodeUri -ContentType 'application/x-www-form-urlencoded' -Body @{ client_id=$ClientId; scope=$Scope } -ErrorAction Stop
    }

    $verificationUri = if ($deviceCodeResponse.verification_uri) { $deviceCodeResponse.verification_uri } else { $deviceCodeResponse.verification_url }
    Write-Information -InformationAction Continue -MessageData ("To sign in, open {0} and enter code {1}." -f $verificationUri, $deviceCodeResponse.user_code)

    $interval = if ($deviceCodeResponse.interval) { [int]$deviceCodeResponse.interval } else { 5 }
    $expiresAt = [DateTime]::UtcNow.AddSeconds([int]$deviceCodeResponse.expires_in)
    $lastStatusAt = [DateTime]::MinValue

    while ([DateTime]::UtcNow -lt $expiresAt) {
        if ($SleepScript) { & $SleepScript $interval } else { Start-Sleep -Seconds $interval }
        $remaining = $expiresAt - [DateTime]::UtcNow
        if (($remaining.TotalSeconds -gt 0) -and (([DateTime]::UtcNow - $lastStatusAt).TotalSeconds -ge 30)) {
            Write-Information -InformationAction Continue -MessageData ('Waiting for authentication... {0:mm\:ss} remaining.' -f $remaining)
            $lastStatusAt = [DateTime]::UtcNow
        }

        try {
            $tokenBody = @{ grant_type='urn:ietf:params:oauth:grant-type:device_code'; client_id=$ClientId; device_code=$deviceCodeResponse.device_code }
            if ($RequestScript) {
                $tokenResponse = & $RequestScript 'Token' $tokenUri $tokenBody
            }
            else {
                $tokenResponse = Invoke-RestMethod -Method POST -Uri $tokenUri -ContentType 'application/x-www-form-urlencoded' -Body $tokenBody -ErrorAction Stop
            }
            $tokenBody.device_code = $null

            if ($tokenResponse.access_token) {
                Write-Information -InformationAction Continue -MessageData 'Device code authentication succeeded.'
                return [pscustomobject]@{ AccessToken=$tokenResponse.access_token; TokenType=$tokenResponse.token_type; ExpiresIn=$tokenResponse.expires_in; Scope=$tokenResponse.scope; ClientId=$ClientId; TenantId=$TenantId }
            }
        }
        catch {
            if ($tokenBody) { $tokenBody.device_code = $null }
            $errorCode = $null; $errorDescription = $null
            $rawBody = if ($_.ErrorDetails -and $_.ErrorDetails.Message) { [string]$_.ErrorDetails.Message } else { $null }
            if ($rawBody) {
                try { $errorJson=$rawBody|ConvertFrom-Json; $errorCode=[string]$errorJson.error; $errorDescription=[string]$errorJson.error_description }
                catch {
                    if ($rawBody -match 'authorization_pending|AADSTS70016') { $errorCode='authorization_pending' }
                    elseif ($rawBody -match 'slow_down') { $errorCode='slow_down' }
                    elseif ($rawBody -match 'authorization_declined|access_denied') { $errorCode='authorization_declined' }
                    elseif ($rawBody -match 'expired_token') { $errorCode='expired_token' }
                }
            }
            if (-not $errorCode) {
                $combinedErrorText=@([string]$_.Exception.Message,[string]$_,[string]$_.FullyQualifiedErrorId)-join ' '
                if ($combinedErrorText -match 'authorization_pending|AADSTS70016') { $errorCode='authorization_pending' }
                elseif ($combinedErrorText -match 'slow_down') { $errorCode='slow_down' }
                elseif ($combinedErrorText -match 'authorization_declined|access_denied') { $errorCode='authorization_declined' }
                elseif ($combinedErrorText -match 'expired_token') { $errorCode='expired_token' }
            }

            switch ($errorCode) {
                'authorization_pending' { continue }
                'slow_down' { $interval += 5; continue }
                'authorization_declined' { throw 'Device code authentication was declined by the user.' }
                'access_denied' { throw 'Device code authentication was denied.' }
                'expired_token' { throw 'The device code expired before authentication completed.' }
                default {
                    $safeDescription = Protect-WindowsDeviceLinkSensitiveText -Text $errorDescription -SensitiveValue @([string]$deviceCodeResponse.device_code)
                    if (-not [string]::IsNullOrWhiteSpace($safeDescription)) { throw "Device code authentication failed: $safeDescription" }
                    $safeDetail = Protect-WindowsDeviceLinkSensitiveText -Text ([string]$_.Exception.Message) -SensitiveValue @([string]$deviceCodeResponse.device_code)
                    if ([string]::IsNullOrWhiteSpace($safeDetail)) { throw 'Device code authentication failed.' }
                    throw "Device code authentication failed. $safeDetail"
                }
            }
        }
    }

    throw 'The device code expired before authentication completed.'
}