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 } |