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:
@@ -24,6 +24,7 @@
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.2651.64" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using System.Diagnostics;
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
using EbayListingTool.Models;
|
||||
using EbayListingTool.Views;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Newtonsoft.Json;
|
||||
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/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.account"
|
||||
];
|
||||
@@ -69,7 +68,6 @@ public class EbayAuthService
|
||||
|
||||
public async Task<string> LoginAsync()
|
||||
{
|
||||
var redirectUri = $"http://localhost:{_settings.RedirectPort}/";
|
||||
var scopeString = Uri.EscapeDataString(string.Join(" ", Scopes));
|
||||
var authBase = _settings.Sandbox
|
||||
? "https://auth.sandbox.ebay.com/oauth2/authorize"
|
||||
@@ -79,39 +77,39 @@ public class EbayAuthService
|
||||
$"&redirect_uri={Uri.EscapeDataString(_settings.RuName)}" +
|
||||
$"&response_type=code&scope={scopeString}";
|
||||
|
||||
// Start local listener before opening browser
|
||||
using var listener = new HttpListener();
|
||||
listener.Prefixes.Add(redirectUri);
|
||||
listener.Start();
|
||||
Log($"LoginAsync start — RuName={_settings.RuName}, ClientId={_settings.ClientId}");
|
||||
|
||||
// Open browser
|
||||
Process.Start(new ProcessStartInfo(authUrl) { UseShellExecute = true });
|
||||
string? code = null;
|
||||
|
||||
// Wait for redirect with code (60s timeout)
|
||||
var contextTask = listener.GetContextAsync();
|
||||
if (await Task.WhenAny(contextTask, Task.Delay(TimeSpan.FromSeconds(60))) != contextTask)
|
||||
// Open embedded browser dialog on UI thread and block until it closes
|
||||
await Application.Current.Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
listener.Stop();
|
||||
throw new TimeoutException("eBay login timed out. Please try again.");
|
||||
var win = new EbayLoginWindow(authUrl);
|
||||
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;
|
||||
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();
|
||||
|
||||
Log($"Auth code received (length={code.Length})");
|
||||
await ExchangeCodeForTokenAsync(code);
|
||||
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)
|
||||
{
|
||||
var tokenUrl = _settings.Sandbox
|
||||
@@ -130,20 +128,33 @@ public class EbayAuthService
|
||||
["redirect_uri"] = _settings.RuName
|
||||
});
|
||||
|
||||
Log($"Token exchange → POST {tokenUrl}");
|
||||
var response = await _http.SendAsync(codeRequest);
|
||||
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)
|
||||
throw new HttpRequestException($"Token exchange failed: {json}");
|
||||
throw new HttpRequestException($"Token exchange failed ({(int)response.StatusCode}): {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
|
||||
{
|
||||
AccessToken = obj["access_token"]!.ToString(),
|
||||
RefreshToken = obj["refresh_token"]!.ToString(),
|
||||
AccessTokenExpiry = DateTime.UtcNow.AddSeconds(obj["expires_in"]!.Value<int>()),
|
||||
RefreshTokenExpiry = DateTime.UtcNow.AddSeconds(obj["refresh_token_expires_in"]!.Value<int>()),
|
||||
AccessToken = accessToken,
|
||||
RefreshToken = refreshToken,
|
||||
AccessTokenExpiry = DateTime.UtcNow.AddSeconds(expiresIn),
|
||||
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
|
||||
_token.EbayUsername = await FetchUsernameAsync(_token.AccessToken);
|
||||
|
||||
25
EbayListingTool/Views/EbayLoginWindow.xaml
Normal file
25
EbayListingTool/Views/EbayLoginWindow.xaml
Normal 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>
|
||||
92
EbayListingTool/Views/EbayLoginWindow.xaml.cs
Normal file
92
EbayListingTool/Views/EbayLoginWindow.xaml.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -63,7 +63,11 @@ public partial class MainWindow : MetroWindow
|
||||
MessageBox.Show(ex.Message, "eBay Login Failed",
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user