internal/functions/Invoke-XdrBrowserAuthentication.ps1

function Resolve-XdrBrowserPathFromCandidateSet {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [object[]]$Candidates
    )

    foreach ($candidate in $Candidates) {
        if ($candidate.CommandName) {
            $command = Get-Command $candidate.CommandName -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1
            if ($command) {
                return [pscustomobject]@{
                    Path = $command.Source
                    Name = $candidate.Name
                }
            }
        }

        if ($candidate.FilePath -and (Test-Path -LiteralPath $candidate.FilePath)) {
            return [pscustomobject]@{
                Path = $candidate.FilePath
                Name = $candidate.Name
            }
        }
    }

    return $null
}

function Resolve-XdrWindowsBrowserPath {
    [CmdletBinding()]
    param()

    $match = Resolve-XdrBrowserPathFromCandidateSet -Candidates @(
        [pscustomobject]@{ Name = 'Microsoft Edge'; CommandName = 'msedge.exe' }
        [pscustomobject]@{ Name = 'Google Chrome'; CommandName = 'chrome.exe' }
        [pscustomobject]@{ Name = 'Brave Browser'; CommandName = 'brave.exe' }
        [pscustomobject]@{ Name = 'Chromium'; CommandName = 'chromium.exe' }
        [pscustomobject]@{ Name = 'Microsoft Edge'; FilePath = 'C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe' }
        [pscustomobject]@{ Name = 'Microsoft Edge'; FilePath = 'C:\Program Files\Microsoft\Edge\Application\msedge.exe' }
        [pscustomobject]@{ Name = 'Google Chrome'; FilePath = 'C:\Program Files\Google\Chrome\Application\chrome.exe' }
        [pscustomobject]@{ Name = 'Google Chrome'; FilePath = 'C:\Program Files (x86)\Google\Chrome\Application\chrome.exe' }
        [pscustomobject]@{ Name = 'Brave Browser'; FilePath = 'C:\Program Files\BraveSoftware\Brave-Browser\Application\brave.exe' }
        [pscustomobject]@{ Name = 'Brave Browser'; FilePath = 'C:\Program Files (x86)\BraveSoftware\Brave-Browser\Application\brave.exe' }
    )

    if ($match) {
        return $match
    }

    throw 'No supported Chromium-based browser was found on Windows. Install Microsoft Edge, Google Chrome, Brave, or specify -BrowserPath.'
}

function Get-XdrMacOSBrowserCandidateSet {
    [OutputType([object[]])]
    [CmdletBinding()]
    param()

    $candidateSet = @(
        [pscustomobject]@{ Name = 'Microsoft Edge'; CommandName = 'msedge' }
        [pscustomobject]@{ Name = 'Google Chrome'; CommandName = 'google-chrome' }
        [pscustomobject]@{ Name = 'Brave Browser'; CommandName = 'brave-browser' }
        [pscustomobject]@{ Name = 'Brave Browser'; CommandName = 'brave' }
        [pscustomobject]@{ Name = 'Chromium'; CommandName = 'chromium' }
    )

    foreach ($applicationRoot in @('/Applications', (Join-Path $HOME 'Applications'))) {
        $candidateSet += @(
            [pscustomobject]@{ Name = 'Microsoft Edge'; FilePath = (Join-Path $applicationRoot 'Microsoft Edge.app/Contents/MacOS/Microsoft Edge') }
            [pscustomobject]@{ Name = 'Google Chrome'; FilePath = (Join-Path $applicationRoot 'Google Chrome.app/Contents/MacOS/Google Chrome') }
            [pscustomobject]@{ Name = 'Brave Browser'; FilePath = (Join-Path $applicationRoot 'Brave Browser.app/Contents/MacOS/Brave Browser') }
            [pscustomobject]@{ Name = 'Chromium'; FilePath = (Join-Path $applicationRoot 'Chromium.app/Contents/MacOS/Chromium') }
        )
    }

    return $candidateSet
}

function Resolve-XdrMacOSBrowserPath {
    [CmdletBinding()]
    param()

    $match = Resolve-XdrBrowserPathFromCandidateSet -Candidates (Get-XdrMacOSBrowserCandidateSet)

    if ($match) {
        return $match
    }

    throw 'No supported Chromium-based browser was found on macOS. Install Microsoft Edge, Google Chrome, Brave, Chromium, or specify -BrowserPath.'
}

function Resolve-XdrMacOSAppBundleExecutablePath {
    [OutputType([pscustomobject])]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$BundlePath
    )

    $resolvedBundlePath = (Resolve-Path -LiteralPath $BundlePath).ProviderPath
    $bundleName = [System.IO.Path]::GetFileNameWithoutExtension($resolvedBundlePath)
    $macOsPath = Join-Path $resolvedBundlePath 'Contents/MacOS'

    if (-not (Test-Path -LiteralPath $macOsPath -PathType Container)) {
        throw "Browser application bundle '$BundlePath' does not contain a Contents/MacOS executable directory."
    }

    $candidateExecutables = @(Get-ChildItem -LiteralPath $macOsPath -File -ErrorAction Stop)
    if (-not $candidateExecutables) {
        throw "Browser application bundle '$BundlePath' does not contain an executable in Contents/MacOS."
    }

    $preferredExecutable = @(
        $candidateExecutables | Where-Object { $_.Name -eq $bundleName }
        $candidateExecutables
    ) | Where-Object { $_ } | Select-Object -First 1

    return [pscustomobject]@{
        Path = $preferredExecutable.FullName
        Name = $bundleName
    }
}

function Resolve-XdrLinuxBrowserPath {
    [CmdletBinding()]
    param()

    $match = Resolve-XdrBrowserPathFromCandidateSet -Candidates @(
        [pscustomobject]@{ Name = 'Microsoft Edge'; CommandName = 'microsoft-edge' }
        [pscustomobject]@{ Name = 'Microsoft Edge'; CommandName = 'microsoft-edge-stable' }
        [pscustomobject]@{ Name = 'Google Chrome'; CommandName = 'google-chrome' }
        [pscustomobject]@{ Name = 'Google Chrome'; CommandName = 'google-chrome-stable' }
        [pscustomobject]@{ Name = 'Brave Browser'; CommandName = 'brave-browser' }
        [pscustomobject]@{ Name = 'Chromium'; CommandName = 'chromium' }
        [pscustomobject]@{ Name = 'Chromium'; CommandName = 'chromium-browser' }
        [pscustomobject]@{ Name = 'Microsoft Edge'; FilePath = '/usr/bin/microsoft-edge' }
        [pscustomobject]@{ Name = 'Microsoft Edge'; FilePath = '/usr/bin/microsoft-edge-stable' }
        [pscustomobject]@{ Name = 'Google Chrome'; FilePath = '/usr/bin/google-chrome' }
        [pscustomobject]@{ Name = 'Google Chrome'; FilePath = '/usr/bin/google-chrome-stable' }
        [pscustomobject]@{ Name = 'Brave Browser'; FilePath = '/usr/bin/brave-browser' }
        [pscustomobject]@{ Name = 'Chromium'; FilePath = '/usr/bin/chromium' }
        [pscustomobject]@{ Name = 'Chromium'; FilePath = '/usr/bin/chromium-browser' }
    )

    if ($match) {
        return $match
    }

    throw 'No supported Chromium-based browser was found on Linux. Install Microsoft Edge, Google Chrome, Brave, Chromium, or specify -BrowserPath.'
}

function Resolve-XdrBrowserPath {
    [CmdletBinding()]
    param(
        [string]$BrowserPath
    )

    if ($BrowserPath) {
        if ($IsMacOS -and $BrowserPath -like '*.app' -and (Test-Path -LiteralPath $BrowserPath -PathType Container)) {
            return Resolve-XdrMacOSAppBundleExecutablePath -BundlePath $BrowserPath
        }

        if (Test-Path -LiteralPath $BrowserPath -PathType Leaf) {
            return [pscustomobject]@{
                Path = (Resolve-Path -LiteralPath $BrowserPath).ProviderPath
                Name = [System.IO.Path]::GetFileNameWithoutExtension($BrowserPath)
            }
        }

        $command = Get-Command $BrowserPath -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1
        if ($command) {
            return [pscustomobject]@{
                Path = $command.Source
                Name = $command.Name
            }
        }

        throw "Browser executable '$BrowserPath' was not found. Specify a valid path or command name."
    }

    if ($IsWindows) {
        return Resolve-XdrWindowsBrowserPath
    }

    if ($IsMacOS) {
        return Resolve-XdrMacOSBrowserPath
    }

    if ($IsLinux) {
        return Resolve-XdrLinuxBrowserPath
    }

    throw 'Connect-XdrByBrowser is not supported on this operating system.'
}

function Get-XdrBrowserDefaultProfilePath {
    [CmdletBinding()]
    param()

    if ($IsWindows) {
        return Join-Path $env:LOCALAPPDATA 'XdrInternals\BrowserProfile'
    }

    if ($IsMacOS) {
        return Join-Path $HOME 'Library/Application Support/XdrInternals/BrowserProfile'
    }

    if ($IsLinux) {
        return Join-Path $HOME '.config/XdrInternals/browser-profile'
    }

    throw 'Connect-XdrByBrowser is not supported on this operating system.'
}

function Initialize-XdrBrowserProfile {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Private helper that prepares the dedicated browser profile.')]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$ProfilePath
    )

    if (-not (Test-Path -LiteralPath $ProfilePath)) {
        $null = New-Item -ItemType Directory -Path $ProfilePath -Force
    }

    if (-not $IsWindows) {
        return
    }

    $defaultProfilePath = Join-Path $ProfilePath 'Default'
    if (-not (Test-Path -LiteralPath $defaultProfilePath)) {
        $null = New-Item -ItemType Directory -Path $defaultProfilePath -Force
    }

    $preferencesPath = Join-Path $defaultProfilePath 'Preferences'
    if (Test-Path -LiteralPath $preferencesPath) {
        return
    }

    @{
        sync    = @{ requested = $false }
        signin  = @{ allowed = $true }
        browser = @{ has_seen_welcome_page = $true }
    } | ConvertTo-Json -Depth 5 | Set-Content -Path $preferencesPath -Encoding UTF8
}

function Resolve-XdrBrowserProfileConfiguration {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Private helper that prepares browser profile state.')]
    [CmdletBinding()]
    param(
        [string]$ProfilePath,

        [switch]$ResetProfile,

        [switch]$PrivateSession
    )

    if ($PrivateSession -and $ProfilePath) {
        throw 'Do not combine -PrivateSession with -ProfilePath. Private session uses a temporary profile automatically.'
    }

    if ($PrivateSession) {
        $temporaryProfilePath = Join-Path ([System.IO.Path]::GetTempPath()) ('xdr-browser-signin-' + [guid]::NewGuid().ToString('N'))
        $null = New-Item -ItemType Directory -Path $temporaryProfilePath -Force

        return [pscustomobject]@{
            ProfilePath          = $temporaryProfilePath
            UsePrivateSession    = $true
            CleanupProfileOnExit = $true
        }
    }

    $resolvedProfilePath = if ($ProfilePath) { $ProfilePath } else { Get-XdrBrowserDefaultProfilePath }
    if ($ResetProfile -and (Test-Path -LiteralPath $resolvedProfilePath)) {
        Remove-Item -Path $resolvedProfilePath -Recurse -Force -ErrorAction Stop
    }

    Initialize-XdrBrowserProfile -ProfilePath $resolvedProfilePath

    return [pscustomobject]@{
        ProfilePath          = $resolvedProfilePath
        UsePrivateSession    = $false
        CleanupProfileOnExit = $false
    }
}

function Get-XdrBrowserInteractiveStartUrl {
    [OutputType([string])]
    [CmdletBinding()]
    param(
        [string]$Username,

        [string]$TenantId
    )

    $tenantSegment = if ($TenantId) { $TenantId } else { 'organizations' }
    $clientId = '80ccca67-54bd-44ab-8625-4b79c4dc7775'
    $redirectUri = [uri]::EscapeDataString('https://security.microsoft.com/')
    $nonce = [guid]::NewGuid().ToString()
    $prompt = if ($Username) { 'login' } else { 'select_account' }

    $startUrl = "https://login.microsoftonline.com/$tenantSegment/oauth2/v2.0/authorize?" +
    "client_id=$clientId" +
    "&response_type=id_token" +
    "&redirect_uri=$redirectUri" +
    "&scope=openid%20profile" +
    "&response_mode=fragment" +
    "&prompt=$prompt" +
    "&nonce=$nonce"

    if ($Username) {
        $startUrl += "&login_hint=$([uri]::EscapeDataString($Username))"
    }

    return $startUrl
}

function Get-XdrBrowserPrivateModeArgument {
    [OutputType([string])]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        $Browser
    )

    $browserName = [string]$Browser.Name
    $browserPath = [string]$Browser.Path

    if ($browserName -like '*Edge*' -or $browserPath -match '(?i)msedge') {
        return '--inprivate'
    }

    return '--incognito'
}

function Get-XdrBrowserLaunchArgumentList {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        $Browser,

        [Parameter(Mandatory)]
        [bool]$UsePrivateSession,

        [Parameter(Mandatory)]
        [int]$DebugPort,

        [Parameter(Mandatory)]
        [string]$ProfileDirectory,

        [Parameter(Mandatory)]
        [string]$StartUrl,

        [string]$UserAgent
    )

    $arguments = @(
        "--remote-debugging-port=$DebugPort",
        "--user-data-dir=$ProfileDirectory",
        '--new-window',
        '--no-first-run',
        '--no-default-browser-check',
        '--disable-default-apps',
        $StartUrl
    )

    # Investigate the brief post-auth Edge account picker flash later. The WebToBrowserSignIn disable-features experiment did not provide a reliable improvement, so it is not enabled by default.

    if ($UsePrivateSession) {
        $arguments = @((Get-XdrBrowserPrivateModeArgument -Browser $Browser)) + $arguments
    }

    if ($UserAgent) {
        $arguments = @("--user-agent=$UserAgent") + $arguments
    }

    return $arguments
}

function Format-XdrProcessArgumentList {
    [OutputType([string[]])]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string[]]$ArgumentList
    )

    $formattedArguments = foreach ($argument in $ArgumentList) {
        if ($argument -match '^(--[^=]+=)(.*)$') {
            $argumentPrefix = $Matches[1]
            $argumentValue = $Matches[2]

            if ($argumentValue -match '[\s"]') {
                $escapedValue = $argumentValue.Replace('"', '\"')
                $argumentPrefix + '"' + $escapedValue + '"'
                continue
            }

            $argument
            continue
        }

        if ($argument -match '[\s"]') {
            $escapedArgument = $argument.Replace('"', '\"')
            '"' + $escapedArgument + '"'
            continue
        }

        $argument
    }

    return $formattedArguments
}

function Start-XdrBrowserProcess {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Private helper that launches the browser process for authentication.')]
    [OutputType([System.Diagnostics.Process])]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$BrowserPath,

        [Parameter(Mandatory)]
        [string[]]$ArgumentList,

        [switch]$SuppressBrowserOutput
    )

    $formattedArgumentList = Format-XdrProcessArgumentList -ArgumentList $ArgumentList

    if ($SuppressBrowserOutput -and -not $IsWindows) {
        $redirectConfiguration = New-XdrBrowserProcessRedirectConfiguration
        $process = Start-Process -FilePath $BrowserPath -ArgumentList $formattedArgumentList -PassThru -RedirectStandardOutput $redirectConfiguration.StandardOutputPath -RedirectStandardError $redirectConfiguration.StandardErrorPath
        $null = $process | Add-Member -NotePropertyName StandardOutputPath -NotePropertyValue $redirectConfiguration.StandardOutputPath -PassThru
        $null = $process | Add-Member -NotePropertyName StandardErrorPath -NotePropertyValue $redirectConfiguration.StandardErrorPath -PassThru
        return $process
    }

    return Start-Process -FilePath $BrowserPath -ArgumentList $formattedArgumentList -PassThru
}

function New-XdrBrowserProcessRedirectConfiguration {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Private helper that allocates temporary redirect file paths for browser process output.')]
    [OutputType([pscustomobject])]
    [CmdletBinding()]
    param()

    $temporaryPath = [System.IO.Path]::GetTempPath()

    return [pscustomobject]@{
        StandardOutputPath = [System.IO.Path]::Combine($temporaryPath, ('xdr-browser-stdout-' + [guid]::NewGuid().ToString('N') + '.log'))
        StandardErrorPath  = [System.IO.Path]::Combine($temporaryPath, ('xdr-browser-stderr-' + [guid]::NewGuid().ToString('N') + '.log'))
    }
}

function Remove-XdrBrowserProcessRedirectFiles {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Private helper that cleans up temporary redirect files created for browser process output.')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = 'Private helper operates on the redirect file set attached to a process object.')]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [object]$Process
    )

    $redirectPaths = @()

    if ($Process.PSObject.Properties['StandardOutputPath']) {
        $redirectPaths += [string]$Process.StandardOutputPath
    }

    if ($Process.PSObject.Properties['StandardErrorPath']) {
        $redirectPaths += [string]$Process.StandardErrorPath
    }

    foreach ($redirectPath in ($redirectPaths | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique)) {
        Remove-Item -LiteralPath $redirectPath -Force -ErrorAction SilentlyContinue
    }
}

function Test-XdrBrowserProcessOutputSuppression {
    [OutputType([bool])]
    [CmdletBinding()]
    param()

    return (-not $IsWindows) -and ($VerbosePreference -ne [System.Management.Automation.ActionPreference]::Continue)
}

function Test-XdrBrowserAuthenticationCompletion {
    [OutputType([bool])]
    [CmdletBinding()]
    param(
        [string]$SccAuthCookieValue,

        [object]$EstsCookie,

        [Nullable[datetime]]$FirstEstsCookieObservedAt,

        [datetime]$Deadline,

        [int]$PortalCookieGracePeriodSeconds = 45
    )

    if ($SccAuthCookieValue) {
        return $true
    }

    if (-not $EstsCookie) {
        return $false
    }

    if (-not $FirstEstsCookieObservedAt) {
        return $false
    }

    $portalCookieGraceDeadline = ([datetime]$FirstEstsCookieObservedAt).AddSeconds($PortalCookieGracePeriodSeconds)
    if ($portalCookieGraceDeadline -gt $Deadline) {
        $portalCookieGraceDeadline = $Deadline
    }

    return (Get-Date) -ge $portalCookieGraceDeadline
}

function Get-XdrBrowserFreeTcpPort {
    [CmdletBinding()]
    param()

    $listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, 0)
    try {
        $listener.Start()
        return ([System.Net.IPEndPoint]$listener.LocalEndpoint).Port
    } finally {
        $listener.Stop()
    }
}

function Get-XdrBrowserCdpVersion {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [int]$Port,

        [int]$TimeoutSeconds = 20
    )

    $deadline = (Get-Date).AddSeconds($TimeoutSeconds)
    $versionUri = "http://127.0.0.1:$Port/json/version"

    do {
        try {
            return Invoke-RestMethod -Uri $versionUri -Method Get -ErrorAction Stop
        } catch {
            Start-Sleep -Milliseconds 500
        }
    } while ((Get-Date) -lt $deadline)

    throw "Timed out waiting for the browser DevTools endpoint on port $Port."
}

function Get-XdrBrowserTargetList {
    [OutputType([object[]])]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [int]$Port
    )

    $targetUri = "http://127.0.0.1:$Port/json/list"
    $targets = Invoke-RestMethod -Uri $targetUri -Method Get -ErrorAction Stop
    return @($targets | Where-Object { $_ })
}

function Get-XdrBrowserPreferredWebSocketUrl {
    [OutputType([string])]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [int]$Port,

        [string]$FallbackWebSocketUrl
    )

    try {
        $targets = @(Get-XdrBrowserTargetList -Port $Port)
    } catch {
        return $FallbackWebSocketUrl
    }

    $preferredTarget = @(
        $targets | Where-Object { $_.type -eq 'page' -and $_.url -like 'https://security.microsoft.com/*' -and $_.webSocketDebuggerUrl }
        $targets | Where-Object { $_.type -eq 'page' -and $_.url -like 'https://login.microsoftonline.com/*' -and $_.webSocketDebuggerUrl }
        $targets | Where-Object { $_.type -eq 'page' -and $_.webSocketDebuggerUrl }
    ) | Where-Object { $_ } | Select-Object -First 1

    if ($preferredTarget) {
        return [string]$preferredTarget.webSocketDebuggerUrl
    }

    return $FallbackWebSocketUrl
}

function Get-XdrBrowserPreferredTargetContext {
    [OutputType([pscustomobject])]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [int]$Port,

        [string]$FallbackWebSocketUrl
    )

    try {
        $targets = @(Get-XdrBrowserTargetList -Port $Port)
    } catch {
        return [pscustomobject]@{
            Url          = $null
            Title        = $null
            Type         = $null
            WebSocketUrl = $FallbackWebSocketUrl
        }
    }

    $preferredTarget = @(
        $targets | Where-Object { $_.type -eq 'page' -and $_.url -like 'https://security.microsoft.com/*' -and $_.webSocketDebuggerUrl }
        $targets | Where-Object { $_.type -eq 'page' -and $_.url -like 'https://login.microsoftonline.com/*' -and $_.webSocketDebuggerUrl }
        $targets | Where-Object { $_.type -eq 'page' -and $_.webSocketDebuggerUrl }
    ) | Where-Object { $_ } | Select-Object -First 1

    if (-not $preferredTarget) {
        return [pscustomobject]@{
            Url          = $null
            Title        = $null
            Type         = $null
            WebSocketUrl = $FallbackWebSocketUrl
        }
    }

    return [pscustomobject]@{
        Url          = [string]$preferredTarget.url
        Title        = [string]$preferredTarget.title
        Type         = [string]$preferredTarget.type
        WebSocketUrl = [string]$preferredTarget.webSocketDebuggerUrl
    }
}

function Format-XdrBrowserTargetDescription {
    [OutputType([string])]
    [CmdletBinding()]
    param(
        [string]$Url,

        [string]$Title
    )

    if ([string]::IsNullOrWhiteSpace($Url)) {
        return $null
    }

    if ([string]::IsNullOrWhiteSpace($Title)) {
        return $Url
    }

    return "$Title [$Url]"
}

function Invoke-XdrBrowserCdpCommand {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$WebSocketUrl,

        [Parameter(Mandatory)]
        [string]$Method,

        [hashtable]$Params
    )

    $webSocket = [System.Net.WebSockets.ClientWebSocket]::new()
    $cancellation = [System.Threading.CancellationTokenSource]::new()

    try {
        $webSocket.ConnectAsync($WebSocketUrl, $cancellation.Token).GetAwaiter().GetResult()

        $requestId = [System.Math]::Abs([guid]::NewGuid().GetHashCode())
        $payload = @{ id = $requestId; method = $Method }
        if ($Params) {
            $payload.params = $Params
        }

        $message = $payload | ConvertTo-Json -Compress -Depth 10
        $sendBuffer = [System.Text.Encoding]::UTF8.GetBytes($message)
        $webSocket.SendAsync(
            [System.ArraySegment[byte]]::new($sendBuffer),
            [System.Net.WebSockets.WebSocketMessageType]::Text,
            $true,
            $cancellation.Token
        ).GetAwaiter().GetResult()

        $receiveBuffer = [byte[]]::new(65536)

        while ($true) {
            $builder = [System.Text.StringBuilder]::new()
            do {
                $result = $webSocket.ReceiveAsync(
                    [System.ArraySegment[byte]]::new($receiveBuffer),
                    $cancellation.Token
                ).GetAwaiter().GetResult()

                if ($result.MessageType -eq [System.Net.WebSockets.WebSocketMessageType]::Close) {
                    throw 'The browser DevTools endpoint closed the WebSocket connection unexpectedly.'
                }

                $null = $builder.Append([System.Text.Encoding]::UTF8.GetString($receiveBuffer, 0, $result.Count))
            } while (-not $result.EndOfMessage)

            $response = $builder.ToString() | ConvertFrom-Json -Depth 20
            if ($response.id -ne $requestId) {
                continue
            }

            if ($null -ne $response.error) {
                throw "Browser DevTools command '$Method' failed: $($response.error.message)"
            }

            return $response.result
        }
    } finally {
        $webSocket.Dispose()
        $cancellation.Dispose()
    }
}

function Get-XdrBrowserCookieJar {
    [OutputType([object[]])]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$WebSocketUrl
    )

    $cookieResult = $null

    foreach ($method in @('Network.getAllCookies', 'Network.getCookies', 'Storage.getCookies')) {
        try {
            $result = Invoke-XdrBrowserCdpCommand -WebSocketUrl $WebSocketUrl -Method $method
        } catch {
            continue
        }

        $cookieResult = @(
            @($result) | Where-Object {
                $_ -and $_.PSObject.Properties['cookies']
            }
        ) | Select-Object -Last 1

        if ($cookieResult) {
            break
        }
    }

    if ($null -eq $cookieResult -or $null -eq $cookieResult.cookies) {
        return @()
    }

    return @($cookieResult.cookies)
}

function Get-XdrBestBrowserEstsCookie {
    [CmdletBinding()]
    param(
        [object[]]$Cookies
    )

    if ($null -eq $Cookies -or $Cookies.Count -eq 0) {
        return $null
    }

    $estsCookies = @($Cookies | Where-Object { $_.name -like 'ESTS*' -and $_.value })
    if (-not $estsCookies) {
        return $null
    }

    $preferenceRank = @{
        ESTSAUTH           = 0
        ESTSAUTHPERSISTENT = 1
        ESTSAUTHLIGHT      = 2
    }

    return $estsCookies |
        Sort-Object -Property @(
            @{ Expression = { if ($preferenceRank.ContainsKey([string]$_.name)) { $preferenceRank[[string]$_.name] } else { 99 } } },
            @{ Expression = { $_.value.Length }; Descending = $true }
        ) |
        Select-Object -First 1
}

function Get-XdrBrowserCookieValue {
    [CmdletBinding()]
    param(
        [object[]]$Cookies,

        [Parameter(Mandatory)]
        [string]$Name,

        [string]$DomainLike
    )

    if ($null -eq $Cookies -or $Cookies.Count -eq 0) {
        return $null
    }

    $cookieMatches = @(
        $Cookies | Where-Object {
            $_.name -eq $Name -and
            $_.value -and
            (-not $DomainLike -or [string]$_.domain -like $DomainLike)
        }
    )

    if (-not $cookieMatches) {
        return $null
    }

    return ($cookieMatches | Select-Object -First 1).value
}

function Invoke-XdrBrowserAuthentication {
    <#
    .SYNOPSIS
        Launches a browser-driven sign-in flow and returns captured authentication artifacts.

    .DESCRIPTION
        This helper launches a dedicated Chromium-based browser profile, waits for the user to
        complete the sign-in, and reads the resulting cookies through the local DevTools protocol.

        When the Defender portal session cookies are already present, those are returned so the
        caller can connect directly with Set-XdrConnectionSettings. ESTS cookies are also captured
        when available as a fallback bootstrap path.

        This is an internal function used by Connect-XdrByBrowser.

    .PARAMETER Username
        Optional username to display to the user while they complete the sign-in.

    .PARAMETER TenantId
        Optional tenant identifier to scope the Entra authorize prompt.

    .PARAMETER TimeoutSeconds
        Maximum time to wait for the browser sign-in to complete.

    .PARAMETER BrowserPath
        Optional browser executable path or command name.

    .PARAMETER ProfilePath
        Optional dedicated browser profile path.

    .PARAMETER ResetProfile
        Clears the dedicated browser profile before launching the sign-in flow.

    .PARAMETER PrivateSession
        Uses a temporary private/incognito browser session instead of the default dedicated profile.

    .PARAMETER UserAgent
        Optional User-Agent override for the launched browser.

    .OUTPUTS
        PSCustomObject containing browser authentication artifacts.

    .EXAMPLE
        $cookie = Invoke-XdrBrowserAuthentication -Username 'admin@contoso.com'

        Launches a supported browser, waits for sign-in to complete, and returns the captured browser authentication artifacts.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')]
    [CmdletBinding()]
    param(
        [string]$Username,

        [string]$TenantId,

        [ValidateRange(30, 1800)]
        [int]$TimeoutSeconds = 300,

        [string]$BrowserPath,

        [string]$ProfilePath,

        [switch]$ResetProfile,

        [switch]$PrivateSession,

        [string]$UserAgent
    )

    $browser = Resolve-XdrBrowserPath -BrowserPath $BrowserPath
    $debugPort = Get-XdrBrowserFreeTcpPort
    $profileConfiguration = Resolve-XdrBrowserProfileConfiguration -ProfilePath $ProfilePath -ResetProfile:$ResetProfile -PrivateSession:$PrivateSession
    $profileDirectory = $profileConfiguration.ProfilePath

    $browserProcess = $null

    try {
        $startUrl = Get-XdrBrowserInteractiveStartUrl -Username $Username -TenantId $TenantId
        $arguments = Get-XdrBrowserLaunchArgumentList -Browser $browser -UsePrivateSession:$profileConfiguration.UsePrivateSession -DebugPort $debugPort -ProfileDirectory $profileDirectory -StartUrl $startUrl -UserAgent $UserAgent

        Write-Host "Launching $($browser.Name) for browser sign-in..."
        if ($profileConfiguration.UsePrivateSession) {
            Write-Host 'Using a temporary private browser session.'
        } else {
            Write-Host "Using dedicated browser profile: $profileDirectory"
        }
        if ($Username) {
            Write-Host "Complete the sign-in in the browser with account: $Username"
        } else {
            Write-Host 'Complete the sign-in in the browser with the target account.'
        }

        $browserProcess = Start-XdrBrowserProcess -BrowserPath $browser.Path -ArgumentList $arguments -SuppressBrowserOutput:(Test-XdrBrowserProcessOutputSuppression)
        $versionInfo = Get-XdrBrowserCdpVersion -Port $debugPort -TimeoutSeconds 20

        $deadline = (Get-Date).AddSeconds($TimeoutSeconds)
        $selectedEstsCookie = $null
        $selectedSccAuth = $null
        $selectedXsrfToken = $null
        $firstEstsCookieObservedAt = $null
        $lastObservedTargetDescription = $null

        do {
            Start-Sleep -Seconds 2

            if ($browserProcess) {
                $browserProcess.Refresh()
                if ($browserProcess.HasExited) {
                    $message = 'The browser window was closed before the browser sign-in completed.'
                    if ($lastObservedTargetDescription) {
                        $message += " Last observed browser page: $lastObservedTargetDescription"
                    }

                    throw $message
                }
            }

            try {
                $targetContext = Get-XdrBrowserPreferredTargetContext -Port $debugPort -FallbackWebSocketUrl $versionInfo.webSocketDebuggerUrl
                $currentTargetDescription = Format-XdrBrowserTargetDescription -Url $targetContext.Url -Title $targetContext.Title
                if ($currentTargetDescription -and $currentTargetDescription -ne $lastObservedTargetDescription) {
                    $lastObservedTargetDescription = $currentTargetDescription
                    Write-Verbose "Observed browser page: $currentTargetDescription"
                }

                $cookies = @(Get-XdrBrowserCookieJar -WebSocketUrl $targetContext.WebSocketUrl)
            } catch {
                Write-Verbose "Cookie polling failed: $($_.Exception.Message)"
                continue
            }

            $selectedEstsCookie = Get-XdrBestBrowserEstsCookie -Cookies $cookies
            $selectedSccAuth = Get-XdrBrowserCookieValue -Cookies $cookies -Name 'sccauth' -DomainLike 'security.microsoft.com'
            $selectedXsrfToken = Get-XdrBrowserCookieValue -Cookies $cookies -Name 'XSRF-TOKEN' -DomainLike 'security.microsoft.com'

            if ($selectedEstsCookie -and -not $firstEstsCookieObservedAt) {
                $firstEstsCookieObservedAt = Get-Date
                Write-Verbose 'Captured ESTS authentication cookie. Waiting for Defender portal cookies to appear before falling back to ESTS bootstrap.'
            }

            if (Test-XdrBrowserAuthenticationCompletion -SccAuthCookieValue $selectedSccAuth -EstsCookie $selectedEstsCookie -FirstEstsCookieObservedAt $firstEstsCookieObservedAt -Deadline $deadline) {
                break
            }
        } while ((Get-Date) -lt $deadline)

        if (-not $selectedSccAuth -and -not $selectedEstsCookie) {
            $message = 'Browser sign-in did not produce Defender portal or ESTS authentication cookies before the timeout expired.'
            if ($lastObservedTargetDescription) {
                $message += " Last observed browser page: $lastObservedTargetDescription"
            }

            throw $message
        }

        if ($selectedSccAuth) {
            Write-Verbose 'Captured Defender portal session cookies from the signed-in browser session.'
        } elseif ($selectedEstsCookie) {
            Write-Verbose 'Captured ESTS authentication cookie before the Defender XDR portal cookie appeared. Continuing with ESTS cookie bootstrap.'
        }

        return [pscustomobject]@{
            EstsAuthCookieValue = if ($selectedEstsCookie) { $selectedEstsCookie.value } else { $null }
            SccAuthCookieValue  = $selectedSccAuth
            XsrfToken           = $selectedXsrfToken
        }
    } finally {
        if ($browserProcess) {
            $browserProcess.Refresh()
            if (-not $browserProcess.HasExited) {
                Stop-Process -Id $browserProcess.Id -Force -ErrorAction SilentlyContinue
                $browserProcess.WaitForExit(1000)
            }

            Remove-XdrBrowserProcessRedirectFiles -Process $browserProcess
        }

        if ($profileConfiguration.CleanupProfileOnExit) {
            Start-Sleep -Milliseconds 500
            Remove-Item -Path $profileDirectory -Recurse -Force -ErrorAction SilentlyContinue
        }
    }
}