TedJ
TedJ

Reputation: 11

Using WebView2 and GetCookiesAsync with Powershell

I'm working on a script to log in to an application using SAML2 authentication in WebView2 with the aim of using the cookie for REST-API requests from PowerShell. As a first step, I'm attempting to get cookies with GetCookiesAsync.

But my script to test this method does not work. When I add GetCookiesAsync to the NavigationCompleted event, it does not complete.

Below is the script I am using to test it.

#Based on https://stackoverflow.com/questions/66106927/webview2-in-powershell-winform-gui
#----------------------------------------------
#region Import the Assemblies
#----------------------------------------------
[void][reflection.assembly]::LoadFile("$PSScriptRoot\assemblies\Microsoft.Web.WebView2.Winforms.dll")
[void][reflection.assembly]::LoadFile("$PSScriptRoot\assemblies\Microsoft.Web.WebView2.Core.dll")
[void][reflection.assembly]::Load('System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a')
[void][reflection.assembly]::Load('System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089')
#endregion Import Assemblies

#----------------------------------------------
#region Generated Form Objects
#----------------------------------------------
[System.Windows.Forms.Application]::EnableVisualStyles()
$form1 = New-Object 'System.Windows.Forms.Form'
$buttonRefresh = New-Object 'System.Windows.Forms.Button'
$buttonGo = New-Object 'System.Windows.Forms.Button'
$textbox1 = New-Object 'System.Windows.Forms.TextBox'
[Microsoft.Web.WebView2.WinForms.WebView2] $webview = New-Object 'Microsoft.Web.WebView2.WinForms.WebView2'
$webview.CreationProperties = New-Object 'Microsoft.Web.WebView2.WinForms.CoreWebView2CreationProperties'
$webview.CreationProperties.UserDataFolder = $PSScriptRoot;
$InitialFormWindowState = New-Object 'System.Windows.Forms.FormWindowState'
#endregion Generated Form Objects

#----------------------------------------------
# User Generated Script
#----------------------------------------------

$form1_Load={
    $webview.Source = ([uri]::new($textbox1.Text))
    $webview.Visible = $true
}

$buttonGo_Click={
    $webview.Source = [System.Uri] $textbox1.Text
}

$webview_SourceChanged={
    $form1.Text = $webview.Source.AbsoluteUri
}

$coreWebView2Initialized = { 
    $cookieManager = $webview.CoreWebView2.CookieManager;
    $cookie = $cookieManager.CreateCookie("name", "value", "example.com", "/")
    $cookieManager.AddOrUpdateCookie($cookie);
}

$webview_OnNavigationCompleted={
    $cookies = $webview.CoreWebView2.CookieManager.GetCookiesAsync("").GetAwaiter().GetResult();
} 

# --End User Generated Script--

#----------------------------------------------
#region Generated Events
#----------------------------------------------

$Form_StateCorrection_Load=
{
    #Correct the initial state of the form to prevent the .Net maximized form issue
    $form1.WindowState = $InitialFormWindowState
}

$Form_Cleanup_FormClosed=
{
    #Remove all event handlers from the controls
    try
    {
        $buttonGo.remove_Click($buttonGo_Click)
        $webview.remove_SourceChanged($webview_SourceChanged)
        $form1.remove_Load($form1_Load)
        $form1.remove_Load($Form_StateCorrection_Load)
        $form1.remove_FormClosed($Form_Cleanup_FormClosed)
    }
    catch { Out-Null <# Prevent PSScriptAnalyzer warning #> }
}
#endregion Generated Events

#----------------------------------------------
#region Generated Form Code
#----------------------------------------------
$form1.SuspendLayout()
$form1.Controls.Add($buttonRefresh)
$form1.Controls.Add($buttonGo)
$form1.Controls.Add($textbox1)
$form1.Controls.Add($webview)
$form1.AutoScaleDimensions = New-Object System.Drawing.SizeF(6, 13)
$form1.AutoScaleMode = 'Font'
$form1.ClientSize = New-Object System.Drawing.Size(619, 413)
$form1.Name = 'form1'
$form1.Text = 'Form'
$form1.add_Load($form1_Load)
#
# buttonRefresh
#
$buttonRefresh.Location = New-Object System.Drawing.Point(13, 13)
$buttonRefresh.Name = 'buttonRefresh'
$buttonRefresh.Size = New-Object System.Drawing.Size(75, 23)
$buttonRefresh.TabIndex = 3
$buttonRefresh.Text = 'Refresh'
$buttonRefresh.UseVisualStyleBackColor = $True
#
# buttonGo
#
$buttonGo.Location = New-Object System.Drawing.Point(538, 9)
$buttonGo.Name = 'buttonGo'
$buttonGo.Size = New-Object System.Drawing.Size(75, 23)
$buttonGo.TabIndex = 2
$buttonGo.Text = 'Go'
$buttonGo.UseVisualStyleBackColor = $True
$buttonGo.add_Click($buttonGo_Click)
#
# textbox1
#
$textbox1.Location = New-Object System.Drawing.Point(96, 13)
$textbox1.Name = 'textbox1'
$textbox1.Size = New-Object System.Drawing.Size(435, 20)
$textbox1.TabIndex = 1
$textbox1.Text = 'https://www.bing.com'
#
# webview
#
$webview.Location = New-Object System.Drawing.Point(0, 49)
$webview.Name = 'webview'
$webview.Size = New-Object System.Drawing.Size(619, 364)
$webview.TabIndex = 0
$webview.ZoomFactor = 1
$webview.add_SourceChanged($webview_SourceChanged)

#Actions with cookies
$webview.add_NavigationCompleted($webview_OnNavigationCompleted)
$webview.add_CoreWebView2InitializationCompleted($coreWebView2Initialized)

$form1.ResumeLayout()
#endregion Generated Form Code

#----------------------------------------------

#Save the initial state of the form
$InitialFormWindowState = $form1.WindowState
#Init the OnLoad event to correct the initial state of the form
$form1.add_Load($Form_StateCorrection_Load)
#Clean up the control events
$form1.add_FormClosed($Form_Cleanup_FormClosed)
#Show the Form
$form1.ShowDialog()
$cookies | Out-Host
Read-Host

The WebView2 browser freezes up. How do I use GetCookiesAsync in powershell?

Upvotes: 1

Views: 140

Answers (2)

Chris Amendola
Chris Amendola

Reputation: 1

I had the same issue in PowerShell.

Full disclosure: I didn't have any luck finding the "proper" way to do this in PowerShell, so I had to hack something out...Consider this an Alpha. This is what I have so far. I wouldn't consider this to be the "proper" way, it's just a way that is actually working for me. I borrowed snippets from various examples to kludge this together.

$Width = "600"
$Height = "800"
$Purge_cache = $false
$debug=$true

$web = New-Object 'Microsoft.Web.WebView2.WinForms.WebView2'
$WebView2Options = [Microsoft.Web.WebView2.Core.CoreWebView2EnvironmentOptions]::New()
$WebView2Env = [Microsoft.Web.WebView2.Core.CoreWebView2Environment]::CreateAsync(
            [String]::Empty, [IO.Path]::Combine( [String[]]([IO.Path]::GetTempPath(), 'MyTempWebview2CacheDir') ), $WebView2Options
        )

        $WebView2Env.GetAwaiter().OnCompleted(
            [Action]{
                $web.EnsureCoreWebView2Async( $WebView2Env.Result )
                $web.Source = [Uri]::New( $uri )
            }
        )    

    $web.Dock = "Fill"
    # CookieManager isn't available until Initialization is completed.  So I get it in the Initialization Completed event.
            $coreWebView2Initialized = {
                $script:cookieManager = $web.CoreWebView2.CookieManager;
                $script:cookies = $script:cookieManager.GetCookiesAsync("");
                $script:coreweb2pid =  $web.CoreWebView2.BrowserProcessId;    #this I use later to find out if the webview2 process was closed so I could delete the cache.
            }
            $web.add_CoreWebView2InitializationCompleted($coreWebView2Initialized);


# Once the initial naviation is completed I hook to the GetCookiesAsync method.
# With my particular situation, I wanted to deal with MFA/JWT authentication with a 3rd party vendor talking to our MFA provider.  The vendor uses javascript to change pages which dosen't trigger a webview2 event.  I added a javascript observer that watched for the documentElement.innerText for the "You can close" text that the 3rd party provider would return indicating it's ok to close the browser.  Once this text came through I used the webview.postMessage('Close!') to send a message back to my script so it could close the form and cleanup everything.
#  The specific part of this that addressed the getting async cookies part is adding the GetCookiesAsync hookup once the initial page is loaded.  For me, the cookies I wanted were HTTP Only cookies so I had to do it this way to get at them.

   $web_NavigationCompleted = {
        $script:cookies = $script:cookieManager.GetCookiesAsync("");
        $web.CoreWebView2.ExecuteScriptAsync("
            //Setup an observer to watch for time to close the window
            function observerCallback(mutations) {
                if ( (document.documentElement.textContent || document.documentElement.innerText).indexOf('You can close') > -1 ) {
                    //send a Close! message back to webview2 so it can close the window and complete.
                    window.chrome.webview.postMessage('Close!');
                }    
            }    
            const observer = new MutationObserver(observerCallback);
            const targetNode = document.documentElement || document.body;
            const observerconf = { attributes: true, childList: true, subtree: true, characterData: true };
            observer.observe(targetNode, observerconf);
        ");
    }
        $web.add_NavigationCompleted($web_NavigationCompleted)
$form = New-Object System.Windows.Forms.Form -Property @{Width=$Width;Height=$Height;Text=$title} -ErrorAction Stop
#   Once the form "Close!" message is generated, the cookie I want should be there.  This is ignoring any of the misc innerText change events that happen and just waiting for the "Close!".
#   I grab the specific HTTP Only cookie and return the value.

   $web.add_WebMessageReceived({
        param($WebView2, $message)
 
        if ($message.TryGetWebMessageAsString() -eq 'Close!') {
            $result = ($cookies.Result | Where-Object {$_.name -eq "The_Name_of_the_HTTP_ONLY_Cookie_I_Wanted"}).value
            $web.Dispose()
            # Cleanup cache dir if desired - wait for the webview2 process to close after the dispose(), then you can delete the cache dir.
            if ($Purge_cache) {
                if ($debug) {write-host "form closing webview2 pid "$script:coreweb2pid -ForegroundColor blue} 
                    $timeout = 0
                    try
                    {
                        while ($null -ne [System.Diagnostics.Process]::GetProcessById($script:coreweb2pid)  -and $timeout -le 2000)
                        {
                            if ($debug) {write-host "Waiting for close pid "$script:coreweb2pid -ForegroundColor blue} 
                            Start-Sleep -seconds 1                      
                            $timeout += 10;
                        }
                    }
                    catch { }
                    if ($debug) {write-host "cleaning up old temp folder" -ForegroundColor blue} 
                    
                    $OriginalPref = $ProgressPreference 
                    $ProgressPreference = "SilentlyContinue"
                    $null = remove-item  "$([IO.Path]::Combine( [String[]]([IO.Path]::GetTempPath(), 'MyTempWebview2CacheDir')) )" -Recurse -Force 
                    $ProgressPreference = $originalpref            
            }
            $form.Close()
                
            return $result.tostring()
        }
    })
$form.Controls.Add($web)
$form.Add_Shown( { $form.Activate() } )
$form.ShowDialog() | Out-Null

There's probably a cleaner way to do this. For now, it works. It drove me crazy because if you dig for anything about doing MFA authentication with PowerShell you end up with O365 examples...Like the only thing we'd use PowerShell for is O365? If/when I get my authentication module polished enough to post, I'll add it to GitHub and update this post. I spent a lot of time running around in circles trying to do this in PowerShell, hopefully this removes that barrier for folks.

Note: I've also tried setting incognito mode for the Webview2 browser (many ways). That doesn't work, not so far anyway. I don't like any of this authentication data being written to disk in cache or any other way, I want it to be momentary authorization for the use of the script and then gone...I am continuing to work on making this not cache things, but for now at least I have a path to delete the browser environment cache.

Cheers.

Upvotes: 0

Woazim
Woazim

Reputation: 1

I had exactly the same problem, and reproduced it in a pure .Net app (without PowerShell).

I think it's a threading issue. I made a lot of experiments to walk around this issue. It seems that we cannot call GetCookiesAsync("").GetAwaiter().GetResult() in response of an event. GetCookiesAsync must be called in an async function (potentially with an 'await' call). Then event handler can then be async, but it is not possible (or not found how) to do it in PowerShell. (I tried with Powershell Jobs and ThreadJobs, without success).

So I wrote a PowerShell binary module to wrap this. It consists of a hidden WPF application that run in the background and with a CmdLet written in C# that communicates with it.

You can find it here: https://github.com/Woazim/PSWebView2

This is a work in progress and suggestions are welcome as I'm not an experimented C# .Net developer. However, it can be a base to your use case.

Upvotes: 0

Related Questions