Private/Authentication/Acronis/Get-AcronisO365AuthorizationCode.ps1

function Get-AcronisO365AuthorizationCode {
    <#
    .SYNOPSIS
        Headlessly mints a fresh OAuth authorization code for a customer M365
        tenant using the cached partner session cookie + prompt=none (no browser).
        Returns @{ Code; SessionState } or throws a descriptive error.
 
    .PARAMETER TenantId
        The CUSTOMER M365 tenant GUID. Must be used as the authority (NOT /organizations/).
 
    .PARAMETER Cookie
        The AAD session Cookie header value (from Get-AcronisO365SessionCookie).
    #>

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

    $state = 'eyJkY0lkIjoibzM2NXdvcmxkd2lkZSJ9'
    $redirectEncoded = [uri]::EscapeDataString('https://cloud.acronis.com/api/1/oauth_o365_cb')
    $scopeEncoded    = [uri]::EscapeDataString('offline_access https://graph.microsoft.com/.default')

    # IMPORTANT: target the CUSTOMER tenant as the authority and always build the
    # URL with -f (the "$Var?" interpolation bug truncates query strings).
    $authorizeUrl = 'https://login.microsoftonline.com/{0}/oauth2/v2.0/authorize?client_id={1}&prompt=none&redirect_uri={2}&response_mode=query&response_type=code&scope={3}&state={4}' -f $TenantId, '912b3045-82b8-40a4-9da9-cff4dc5290af', $redirectEncoded, $scopeEncoded, $state

    Write-ModuleLog -Message "Minting authorization code for tenant $TenantId via prompt=none" -Level Verbose -Component 'AcronisO365Auth'

    $handler = [System.Net.Http.HttpClientHandler]::new()
    $handler.AllowAutoRedirect = $false
    $client = [System.Net.Http.HttpClient]::new($handler)
    $client.Timeout = [TimeSpan]::FromSeconds(60)

    $status = 0
    $loc = ''
    try {
        $req = [System.Net.Http.HttpRequestMessage]::new([System.Net.Http.HttpMethod]::Get, $authorizeUrl)
        $null = $req.Headers.TryAddWithoutValidation('Cookie', $Cookie)
        $null = $req.Headers.TryAddWithoutValidation('User-Agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)')
        $resp = $client.SendAsync($req).Result
        $status = [int]$resp.StatusCode
        if ($resp.Headers.Location) { $loc = $resp.Headers.Location.AbsoluteUri }
        $resp.Dispose()
    }
    catch {
        throw "Authorization request for tenant $TenantId failed: $($_.Exception.Message)"
    }
    finally {
        $client.Dispose()
        $handler.Dispose()
    }

    if (-not $loc) {
        throw "Authorization for tenant $TenantId returned no redirect (HTTP $status)."
    }

    $q = @{}
    foreach ($pair in ([uri]$loc).Query.TrimStart('?').Split('&')) {
        if ($pair) {
            $kv = $pair -split '=', 2
            if ($kv.Count -eq 2) { $q[$kv[0]] = [uri]::UnescapeDataString($kv[1]) }
        }
    }

    if ($q.ContainsKey('code') -and $q['code']) {
        Write-ModuleLog -Message "Got authorization code for tenant $TenantId" -Level Verbose -Component 'AcronisO365Auth'
        return [PSCustomObject]@{ Code = $q['code']; SessionState = $q['session_state'] }
    }

    if ($q.ContainsKey('error')) {
        $err  = $q['error']
        $desc = $q['error_description']
        switch ($err) {
            'interaction_required' { throw "No silent session for tenant $TenantId - the partner GA may need to sign in once at this tenant, or the session cookie is expired. Re-run Save-AcronisO365Session -ForceRefresh. ($desc)" }
            'consent_required'     { throw "Admin consent for the Acronis app is not granted in tenant $TenantId. ($desc)" }
            'login_required'       { throw "Not signed in - re-run Save-AcronisO365Session to refresh the session cookie. ($desc)" }
            default                { throw "Authorization failed for tenant ${TenantId}: $err - $desc" }
        }
    }

    throw "Authorization for tenant ${TenantId} returned an unexpected redirect: $loc"
}