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.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>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
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",
|
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user