internal/functions/authentication/Connect-ServiceDeviceCode.ps1

function Connect-ServiceDeviceCode {
    <#
    .SYNOPSIS
        Connects to Azure AD using the Device Code authentication workflow.
    
    .DESCRIPTION
        Connects to Azure AD using the Device Code authentication workflow.
    
    .PARAMETER Resource
        The resource owning the api permissions / scopes requested.

    .PARAMETER ClientID
        The ID of the registered app used with this authentication request.
    
    .PARAMETER TenantID
        The ID of the tenant connected to with this authentication request.
    
    .PARAMETER Scopes
        The scopes to request.
        Automatically scoped to the service specified via Service Url.
        Defaults to ".Default"

    .PARAMETER NoReconnect
        Disables automatic reconnection.
        By default, this module will automatically try to reaquire a new token before the old one expires.

    .PARAMETER AuthenticationUrl
        The url used for the authentication requests to retrieve tokens.
    
    .EXAMPLE
        PS C:\> Connect-ServiceDeviceCode -Resource 'https://graph.microsoft.com' -ClientID '<ClientID>' -TenantID '<TenantID>' -AuthenticationUrl 'https://login.microsoftonline.com'
    
        Connects to the specified tenant using the specified client, prompting the user to authorize via Device Code workflow.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $Resource,

        [Parameter(Mandatory = $true)]
        [string]
        $ClientID,
        
        [Parameter(Mandatory = $true)]
        [string]
        $TenantID,
        
        [AllowEmptyCollection()]
        [string[]]
        $Scopes,

        [switch]
        $NoReconnect,

        [Parameter(Mandatory = $true)]
        [string]
        $AuthenticationUrl
    )

    if (-not $Scopes) { $Scopes = @('.default') }
    $actualScopes = $Scopes | Resolve-ScopeName -Resource $Resource

    if (-not $NoReconnect) {
        $actualScopes = @($actualScopes) + 'offline_access'
    }

    try {
        $initialResponse = Invoke-RestMethod -Method POST -Uri "$AuthenticationUrl/$TenantID/oauth2/v2.0/devicecode" -Body @{
            client_id = $ClientID
            scope     = $actualScopes -join " "
        } -ErrorAction Stop
    }
    catch {
        throw
    }

    Write-Host $initialResponse.message

    $paramRetrieve = @{
        Uri         = "$AuthenticationUrl/$TenantID/oauth2/v2.0/token"
        Method      = "POST"
        Body        = @{
            grant_type  = "urn:ietf:params:oauth:grant-type:device_code"
            client_id   = $ClientID
            device_code = $initialResponse.device_code
        }
        ErrorAction = 'Stop'
    }
    $limit = (Get-Date).AddSeconds($initialResponse.expires_in)
    while ($true) {
        if ((Get-Date) -gt $limit) {
            Invoke-TerminatingException -Cmdlet $PSCmdlet -Message "Timelimit exceeded, device code authentication failed" -Category AuthenticationError
        }
        Start-Sleep -Seconds $initialResponse.interval
        try { $authResponse = Invoke-RestMethod @paramRetrieve }
        catch {
            if ($_ -match '"error":\s*"authorization_pending"') { continue }
            $PSCmdlet.ThrowTerminatingError($_)
        }
        if ($authResponse) {
            break
        }
    }

    Read-AuthResponse -AuthResponse $authResponse
}