src/_Get-CciEntraToken.ps1

function _Get-CciEntraToken {
<#
.SYNOPSIS
    Acquires an Entra access token via the OAuth 2.0 device authorization grant,
    using plain REST.
.DESCRIPTION
    Deliberately does NOT use Az.Accounts. On a freshly built Windows machine
    (Windows PowerShell 5.1) Connect-AzAccount -UseDeviceAuthentication fails
    with "The type initializer for
    'Microsoft.Identity.Client.Platforms.net.MsalJsonSerializerContext' threw an
    exception", and installing Az.Accounts + Az.Storage costs hundreds of MB
    during OOBE for two REST calls. The device-code grant is a handful of HTTP
    requests, so cciget speaks it directly.
.PARAMETER TenantId
    Entra tenant to authenticate against.
.PARAMETER Scope
    Resource scope, e.g. https://storage.azure.com/.default
.PARAMETER ClientId
    Public client application id used for the device-code flow.
.OUTPUTS
    The access token string, or $null on failure.
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$TenantId,
        [Parameter(Mandatory)][string]$Scope,
        [Parameter(Mandatory)][string]$ClientId
    )

    $authority = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0"
    try {
        $dc = Invoke-RestMethod -Method Post -Uri "$authority/devicecode" `
            -ContentType 'application/x-www-form-urlencoded' `
            -Body @{ client_id = $ClientId; scope = "$Scope offline_access" } -ErrorAction Stop
    } catch {
        Write-Warning "cciget: could not start device-code sign-in: $_"
        return $null
    }

    Write-Host ''
    Write-Host ' ------------------------------------------------------------'
    Write-Host " To sign in, open $($dc.verification_uri)"
    Write-Host " and enter the code: $($dc.user_code)"
    Write-Host ' ------------------------------------------------------------'
    Write-Host ''

    $interval = [int]$dc.interval
    if ($interval -lt 1) { $interval = 5 }
    $deadline = (Get-Date).AddSeconds([int]$dc.expires_in)

    while ((Get-Date) -lt $deadline) {
        Start-Sleep -Seconds $interval
        try {
            $tok = Invoke-RestMethod -Method Post -Uri "$authority/token" `
                -ContentType 'application/x-www-form-urlencoded' `
                -Body @{
                    grant_type  = 'urn:ietf:params:oauth:grant-type:device_code'
                    client_id   = $ClientId
                    device_code = $dc.device_code
                } -ErrorAction Stop
            if ($tok.access_token) {
                Write-Host 'cciget: sign-in complete.'
                return $tok.access_token
            }
        } catch {
            # The token endpoint answers 400 with an error code until the user
            # finishes; only authorization_pending/slow_down are non-fatal.
            $body = $null
            try {
                $stream = $_.Exception.Response.GetResponseStream()
                $reader = New-Object System.IO.StreamReader($stream)
                $body = $reader.ReadToEnd() | ConvertFrom-Json
            } catch { }
            $code = if ($body) { $body.error } else { $null }
            switch ($code) {
                'authorization_pending' { continue }
                'slow_down'             { $interval += 5; continue }
                default {
                    $desc = if ($body) { $body.error_description } else { $_.Exception.Message }
                    Write-Warning "cciget: device-code sign-in failed ($code): $desc"
                    return $null
                }
            }
        }
    }
    Write-Warning 'cciget: device-code sign-in timed out.'
    return $null
}