Private/Authentication/Acronis/Save-AcronisO365SessionCookie.ps1

function Save-AcronisO365SessionCookie {
    <#
    .SYNOPSIS
        Seeds the jlhosting.dk AAD session cookie by opening a Chrome/Edge window
        for the supporter to sign in, capturing the microsoftonline.com cookies
        via the Chrome DevTools Protocol, and storing them encrypted
        (SecureString -> DPAPI via Export-Clixml) in the module token cache.
 
    .PARAMETER TenantId
        Optional tenant to sign in at. Defaults to 'common' (partner home tenant).
 
    .PARAMETER ForceRefresh
        Overwrite an existing valid cookie instead of skipping.
 
    .PARAMETER CdpPort
        Chrome DevTools Protocol port. Default 9222.
    #>

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

        [Parameter()]
        [switch]$ForceRefresh,

        [Parameter()]
        [int]$CdpPort = 9222
    )

    if (-not $script:TokenCacheConfig.ContainsKey('CookieTtlDays')) {
        $script:TokenCacheConfig.CookieTtlDays = 90
    }

    # If a valid cookie already exists, skip unless forced
    if (-not $ForceRefresh -and (Get-AcronisO365SessionCookie)) {
        Write-ModuleLog -Message "A valid Acronis O365 session cookie already exists - use -ForceRefresh to re-seed." -Level Info -Component 'AcronisO365Session'
        return
    }

    # Authority to sign in at. Default to 'common' (partner home tenant).
    $authority = if ($TenantId) { $TenantId } else { 'common' }

    $redirectEncoded = [uri]::EscapeDataString('https://cloud.acronis.com/api/1/oauth_o365_cb')
    $scopeEncoded    = [uri]::EscapeDataString('offline_access https://graph.microsoft.com/.default')
    $state           = 'eyJkY0lkIjoibzM2NXdvcmxkd2lkZSJ9'
    $authorizeUrl = 'https://login.microsoftonline.com/{0}/oauth2/v2.0/authorize?client_id={1}&redirect_uri={2}&response_type=code&scope={3}&state={4}&prompt=login' -f $authority, '912b3045-82b8-40a4-9da9-cff4dc5290af', $redirectEncoded, $scopeEncoded, $state

    Write-ModuleLog -Message "Opening browser to capture the jlhosting.dk session cookie (authority: $authority)..." -Level Info -Component 'AcronisO365Session'
    Write-ModuleLog -Message "Sign in with your jlhosting.dk account (MFA if asked), then come back and press Enter." -Level Info -Component 'AcronisO365Session'

    $chromeCandidates = @(
        "$env:ProgramFiles\Google\Chrome\Application\chrome.exe",
        "${env:ProgramFiles(x86)}\Google\Chrome\Application\chrome.exe",
        "$env:ProgramFiles\Microsoft\Edge\Application\msedge.exe",
        "${env:ProgramFiles(x86)}\Microsoft\Edge\Application\msedge.exe"
    )
    $chrome = $chromeCandidates | Where-Object { $_ -and (Test-Path $_) } | Select-Object -First 1
    if (-not $chrome) {
        throw "Chrome/Edge not found - cannot capture the Acronis O365 session cookie."
    }

    $profileDir = Join-Path $env:TEMP 'jysk-acronis-o365-seed'
    $proc = Start-Process $chrome -ArgumentList @(
        "--remote-debugging-port=$CdpPort",
        "--user-data-dir=$profileDir",
        '--no-first-run',
        $authorizeUrl
    ) -PassThru

    # Wait for the CDP endpoint
    $targets = $null
    $deadline = (Get-Date).AddSeconds(30)
    while ((Get-Date) -lt $deadline) {
        try { $targets = Invoke-RestMethod ("http://127.0.0.1:{0}/json" -f $CdpPort) -TimeoutSec 2 -ErrorAction Stop; break }
        catch { Start-Sleep -Milliseconds 500 }
    }
    if (-not $targets) {
        try { $proc.Kill() } catch {}
        throw "Chrome debugging endpoint not reachable on port $CdpPort."
    }
    $page = $targets | Where-Object { $_.type -eq 'page' } | Select-Object -First 1
    if (-not $page -or -not $page.webSocketDebuggerUrl) {
        try { $proc.Kill() } catch {}
        throw "No debuggable Chrome page found."
    }
    $wsUrl = $page.webSocketDebuggerUrl

    $ws = [System.Net.WebSockets.ClientWebSocket]::new()
    $ws.ConnectAsync([uri]$wsUrl, [System.Threading.CancellationToken]::None).GetAwaiter().GetResult()

    $cookieHeader = $null
    $msCookieCount = 0
    try {
        Read-Host "Press Enter after signing in with jlhosting.dk"

        $msg = @{ id = 1; method = 'Network.getAllCookies'; params = @{} } | ConvertTo-Json -Compress -Depth 5
        $bytes = [Text.Encoding]::UTF8.GetBytes($msg)
        $null = $ws.SendAsync([ArraySegment[byte]]::new($bytes), [System.Net.WebSockets.WebSocketMessageType]::Text, $true, [System.Threading.CancellationToken]::None).GetAwaiter().GetResult()

        while ($true) {
            $ms = [System.IO.MemoryStream]::new()
            do {
                $buffer = New-Object byte[] 65536
                $recv = $ws.ReceiveAsync([ArraySegment[byte]]::new($buffer), [System.Threading.CancellationToken]::None).GetAwaiter().GetResult()
                $ms.Write($buffer, 0, $recv.Count)
            } while (-not $recv.EndOfMessage)
            $obj = [Text.Encoding]::UTF8.GetString($ms.ToArray()) | ConvertFrom-Json
            if ($obj.id -eq 1) {
                $cookies = @($obj.result.cookies)
                $msCookies = $cookies | Where-Object { $_.domain -like '*microsoftonline.com' }
                $msCookieCount = @($msCookies).Count
                $cookieHeader = ($msCookies | ForEach-Object { '{0}={1}' -f $_.name, $_.value }) -join '; '
                break
            }
        }
    }
    finally {
        try { $ws.Dispose() } catch {}
        try { $proc.CloseMainWindow() | Out-Null } catch {}
    }

    if (-not $cookieHeader) {
        throw "No microsoftonline.com cookies found - did you complete the sign-in?"
    }

    # Store encrypted (SecureString -> DPAPI via Export-Clixml) in the token cache
    $secureCookie = ConvertTo-SecureString -String $cookieHeader -AsPlainText -Force
    $entry = [PSCustomObject]@{
        Type               = 'AcronisO365Session'
        Cookies            = $secureCookie
        ExpirationDateTime = (Get-Date).AddDays($script:TokenCacheConfig.CookieTtlDays)
        MintedAt           = Get-Date
        MintedBy           = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
    }
    $script:TokenCache['AcronisO365Session'] = $entry
    Save-TokenCache

    Write-ModuleLog -Message ("Captured {0} microsoftonline.com cookies; session cookie valid until {1}" -f $msCookieCount, $entry.ExpirationDateTime) -Level Info -Component 'AcronisO365Session'
    return $entry
}