Replace IE WebBrowser with WebView2 for eBay OAuth login

- Add Microsoft.Web.WebView2 package
- New EbayLoginWindow using Edge (WebView2) instead of IE — handles modern eBay login pages
- Intercept ThirdPartyAuthSucessFailure redirect to extract auth code
- Remove invalid sell.listing scope that caused immediate invalid_scope error
- Add detailed browser navigation logging for diagnostics

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Peter Foster
2026-04-14 17:06:39 +01:00
parent 1ff9d3d78b
commit da0efc1374
5 changed files with 167 additions and 34 deletions

View File

@@ -24,6 +24,7 @@
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.2651.64" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -1,8 +1,8 @@
using System.Diagnostics;
using System.Net;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Text; using System.Text;
using System.Windows;
using EbayListingTool.Models; using EbayListingTool.Models;
using EbayListingTool.Views;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
@@ -27,7 +27,6 @@ public class EbayAuthService
[ [
"https://api.ebay.com/oauth/api_scope", "https://api.ebay.com/oauth/api_scope",
"https://api.ebay.com/oauth/api_scope/sell.inventory", "https://api.ebay.com/oauth/api_scope/sell.inventory",
"https://api.ebay.com/oauth/api_scope/sell.listing",
"https://api.ebay.com/oauth/api_scope/sell.fulfillment", "https://api.ebay.com/oauth/api_scope/sell.fulfillment",
"https://api.ebay.com/oauth/api_scope/sell.account" "https://api.ebay.com/oauth/api_scope/sell.account"
]; ];
@@ -69,7 +68,6 @@ public class EbayAuthService
public async Task<string> LoginAsync() public async Task<string> LoginAsync()
{ {
var redirectUri = $"http://localhost:{_settings.RedirectPort}/";
var scopeString = Uri.EscapeDataString(string.Join(" ", Scopes)); var scopeString = Uri.EscapeDataString(string.Join(" ", Scopes));
var authBase = _settings.Sandbox var authBase = _settings.Sandbox
? "https://auth.sandbox.ebay.com/oauth2/authorize" ? "https://auth.sandbox.ebay.com/oauth2/authorize"
@@ -79,39 +77,39 @@ public class EbayAuthService
$"&redirect_uri={Uri.EscapeDataString(_settings.RuName)}" + $"&redirect_uri={Uri.EscapeDataString(_settings.RuName)}" +
$"&response_type=code&scope={scopeString}"; $"&response_type=code&scope={scopeString}";
// Start local listener before opening browser Log($"LoginAsync start — RuName={_settings.RuName}, ClientId={_settings.ClientId}");
using var listener = new HttpListener();
listener.Prefixes.Add(redirectUri);
listener.Start();
// Open browser string? code = null;
Process.Start(new ProcessStartInfo(authUrl) { UseShellExecute = true });
// Wait for redirect with code (60s timeout) // Open embedded browser dialog on UI thread and block until it closes
var contextTask = listener.GetContextAsync(); await Application.Current.Dispatcher.InvokeAsync(() =>
if (await Task.WhenAny(contextTask, Task.Delay(TimeSpan.FromSeconds(60))) != contextTask)
{ {
listener.Stop(); var win = new EbayLoginWindow(authUrl);
throw new TimeoutException("eBay login timed out. Please try again."); var result = win.ShowDialog();
if (result == true)
code = win.AuthCode;
});
if (string.IsNullOrEmpty(code))
{
Log("User cancelled login or no code returned");
throw new InvalidOperationException("eBay login was cancelled or did not return an authorisation code.");
} }
var context = await contextTask; Log($"Auth code received (length={code.Length})");
var code = context.Request.QueryString["code"]
?? throw new InvalidOperationException("No authorisation code received from eBay.");
// Send OK page to browser
var responseHtml = "<html><body><h2>Connected! You can close this tab.</h2></body></html>";
var responseBytes = Encoding.UTF8.GetBytes(responseHtml);
context.Response.ContentType = "text/html";
context.Response.ContentLength64 = responseBytes.Length;
await context.Response.OutputStream.WriteAsync(responseBytes);
context.Response.Close();
listener.Stop();
await ExchangeCodeForTokenAsync(code); await ExchangeCodeForTokenAsync(code);
return _token!.EbayUsername; return _token!.EbayUsername;
} }
private static readonly string LogFile = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"EbayListingTool", "auth_log.txt");
private static void Log(string msg)
{
try { File.AppendAllText(LogFile, $"{DateTime.Now:HH:mm:ss} {msg}\n"); } catch { }
}
private async Task ExchangeCodeForTokenAsync(string code) private async Task ExchangeCodeForTokenAsync(string code)
{ {
var tokenUrl = _settings.Sandbox var tokenUrl = _settings.Sandbox
@@ -130,20 +128,33 @@ public class EbayAuthService
["redirect_uri"] = _settings.RuName ["redirect_uri"] = _settings.RuName
}); });
Log($"Token exchange → POST {tokenUrl}");
var response = await _http.SendAsync(codeRequest); var response = await _http.SendAsync(codeRequest);
var json = await response.Content.ReadAsStringAsync(); var json = await response.Content.ReadAsStringAsync();
Log($"Token exchange ← {(int)response.StatusCode}: {json}");
System.Diagnostics.Debug.WriteLine($"[eBay token exchange] {(int)response.StatusCode}: {json}");
if (!response.IsSuccessStatusCode) if (!response.IsSuccessStatusCode)
throw new HttpRequestException($"Token exchange failed: {json}"); throw new HttpRequestException($"Token exchange failed ({(int)response.StatusCode}): {json}");
var obj = JObject.Parse(json); var obj = JObject.Parse(json);
var accessToken = obj["access_token"]?.ToString()
?? throw new InvalidOperationException($"No access_token in response: {json}");
var expiresIn = obj["expires_in"]?.Value<int>() ?? 7200;
var refreshToken = obj["refresh_token"]?.ToString() ?? "";
var refreshExpiresIn = obj["refresh_token_expires_in"]?.Value<int>() ?? 0;
_token = new EbayToken _token = new EbayToken
{ {
AccessToken = obj["access_token"]!.ToString(), AccessToken = accessToken,
RefreshToken = obj["refresh_token"]!.ToString(), RefreshToken = refreshToken,
AccessTokenExpiry = DateTime.UtcNow.AddSeconds(obj["expires_in"]!.Value<int>()), AccessTokenExpiry = DateTime.UtcNow.AddSeconds(expiresIn),
RefreshTokenExpiry = DateTime.UtcNow.AddSeconds(obj["refresh_token_expires_in"]!.Value<int>()), RefreshTokenExpiry = refreshExpiresIn > 0
? DateTime.UtcNow.AddSeconds(refreshExpiresIn)
: DateTime.UtcNow.AddDays(18 * 30), // eBay default: 18 months
}; };
Log($"Token set — AccessToken length={accessToken.Length}, Expiry={_token.AccessTokenExpiry:HH:mm:ss}, IsValid={_token.IsAccessTokenValid}");
// Fetch username // Fetch username
_token.EbayUsername = await FetchUsernameAsync(_token.AccessToken); _token.EbayUsername = await FetchUsernameAsync(_token.AccessToken);

View File

@@ -0,0 +1,25 @@
<Window x:Class="EbayListingTool.Views.EbayLoginWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:wv2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
Title="Connect to eBay" Width="960" Height="700"
WindowStartupLocation="CenterScreen"
ResizeMode="CanResizeWithGrip">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Border Grid.Row="0" Padding="12,8"
Background="{DynamicResource MahApps.Brushes.Gray9}"
BorderThickness="0,0,0,1"
BorderBrush="{DynamicResource MahApps.Brushes.Gray8}">
<TextBlock Text="Sign in to your eBay account to connect"
FontSize="13" VerticalAlignment="Center"
Foreground="{DynamicResource MahApps.Brushes.Gray2}"/>
</Border>
<wv2:WebView2 Grid.Row="1" x:Name="Browser"/>
</Grid>
</Window>

View File

@@ -0,0 +1,92 @@
using System.IO;
using System.Web;
using System.Windows;
using Microsoft.Web.WebView2.Core;
namespace EbayListingTool.Views;
public partial class EbayLoginWindow : Window
{
public string? AuthCode { get; private set; }
private readonly string _authUrl;
private static readonly string LogFile = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"EbayListingTool", "auth_log.txt");
private static void Log(string msg)
{
try { File.AppendAllText(LogFile, $"{DateTime.Now:HH:mm:ss} [Browser] {msg}\n"); } catch { }
}
public EbayLoginWindow(string authUrl)
{
InitializeComponent();
_authUrl = authUrl;
Loaded += OnLoaded;
}
private async void OnLoaded(object sender, RoutedEventArgs e)
{
Log($"WebView2 initialising...");
try
{
await Browser.EnsureCoreWebView2Async();
Browser.CoreWebView2.NavigationStarting += CoreWebView2_NavigationStarting;
Log($"Navigating to auth URL");
Browser.CoreWebView2.Navigate(_authUrl);
}
catch (Exception ex)
{
Log($"WebView2 init failed: {ex.Message}");
MessageBox.Show($"Browser could not initialise: {ex.Message}\n\nEnsure Microsoft Edge WebView2 Runtime is installed.",
"Browser Error", MessageBoxButton.OK, MessageBoxImage.Error);
DialogResult = false;
Close();
}
}
private void CoreWebView2_NavigationStarting(object? sender, CoreWebView2NavigationStartingEventArgs e)
{
var url = e.Uri ?? "";
Log($"NavigationStarting → {url}");
if (!url.Contains("ThirdPartyAuth", StringComparison.OrdinalIgnoreCase))
return;
e.Cancel = true;
Log($"ThirdPartyAuth intercepted: {url}");
try
{
var uri = new Uri(url);
var qs = HttpUtility.ParseQueryString(uri.Query);
var code = qs["code"];
Log($"Query params: {string.Join(", ", qs.AllKeys.Select(k => $"{k}={qs[k]?.Substring(0, Math.Min(qs[k]?.Length ?? 0, 30))}"))}");
if (!string.IsNullOrEmpty(code))
{
AuthCode = code;
DialogResult = true;
}
else
{
var error = qs["error"] ?? qs["error_id"] ?? "unknown";
var desc = qs["error_description"] ?? qs["error_message"] ?? "";
Log($"No code — error={error}, desc={desc}");
MessageBox.Show($"eBay login error: {error}\n{desc}", "Login Failed",
MessageBoxButton.OK, MessageBoxImage.Warning);
DialogResult = false;
}
}
catch (Exception ex)
{
Log($"Exception parsing redirect: {ex.Message}");
DialogResult = false;
}
Dispatcher.Invoke(Close);
}
}

View File

@@ -63,7 +63,11 @@ public partial class MainWindow : MetroWindow
MessageBox.Show(ex.Message, "eBay Login Failed", MessageBox.Show(ex.Message, "eBay Login Failed",
MessageBoxButton.OK, MessageBoxImage.Warning); MessageBoxButton.OK, MessageBoxImage.Warning);
} }
finally { ConnectBtn.IsEnabled = true; } finally
{
ConnectBtn.IsEnabled = true;
UpdateConnectionState(); // always sync UI to actual auth state
}
} }
private void DisconnectBtn_Click(object sender, RoutedEventArgs e) private void DisconnectBtn_Click(object sender, RoutedEventArgs e)