Skip to content

Commit

Permalink
Browser based interactive authentication (#209)
Browse files Browse the repository at this point in the history
* Added clarification on testing the out of the box experience.

Closes #195.

* Added support for browser based interactive authentication using an embedded browser.

* Improved UX of interactive authentication.

* Ensure that the main window is available when showing a modal dialog.

* Reorganization.
  • Loading branch information
pvginkel authored Dec 28, 2023
1 parent 022ecc7 commit e78065f
Show file tree
Hide file tree
Showing 45 changed files with 627 additions and 304 deletions.
46 changes: 0 additions & 46 deletions src/Tql.Abstractions/IInteractiveAuthentication.cs

This file was deleted.

87 changes: 77 additions & 10 deletions src/Tql.Abstractions/IUI.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Windows.Forms;
using System.Collections.Specialized;
using System.Windows.Forms;

namespace Tql.Abstractions;

Expand All @@ -23,14 +24,59 @@ public interface IUI
/// Perform interactive authentication.
/// </summary>
/// <remarks>
/// If the tool you're connecting to supports interactive authentication,
/// you can integrate this into TQL using this method and the
/// <see cref="IInteractiveAuthentication"/> interface. See the documentation
/// for this interface for more information.
/// <para>
/// This method can be used to perform interactive authentication.
/// A dialog will be shown to the user telling him that he needs
/// to enter credentials. The information in <paramref name="resource"/>
/// is used to tell the user what resource is being authenticated.
/// You should include the name of a connection in this to ensure
/// the user knows what credentials he needs to enter. If the
/// user proceeds to authenticate, the <paramref name="action"/>
/// parameter is used to show UI to the user.
/// </para>
///
/// <para>
/// If you're using a library to talk to the online resource,
/// there's a good chance the interactive authentication functionality
/// takes a <see cref="IWin32Window"/> as the owner. However, if you're
/// creating your own WPF UI, you should use <see cref="System.Windows.Interop.WindowInteropHelper.Owner"/>
/// to set the owner of your window to the provided handle.
/// </para>
/// </remarks>
/// <param name="interactiveAuthentication">Pending interactive authentication.</param>
/// <param name="resource">Information about the resource being authenticated.</param>
/// <param name="action">Action called to authenticate the resource.</param>
/// <returns>Task representing the operation.</returns>
Task PerformInteractiveAuthentication(IInteractiveAuthentication interactiveAuthentication);
Task PerformInteractiveAuthentication(
InteractiveAuthenticationResource resource,
Func<IWin32Window, Task> action
);

/// <summary>
/// Perform browser based interactive authentication.
/// </summary>
/// <remarks>
/// <para>
/// This can be used to implement web based interactive authentication workflows
/// while staying in the app. The redirect URL is the URL configured with the authentication
/// provider. TQL will start a web server at that URL and wait for an incoming request.
/// The result of this method includes the parameters sent when the redirect URL
/// was called.
/// </para>
///
/// <para>
/// The OAuth2 NuGet package is a good option to implement OAuth2 in a TQL plugin. This
/// will provide you with a login URL you can pass into this method.
/// </para>
/// </remarks>
/// <param name="resource">Information about the resource being authenticated.</param>
/// <param name="loginUrl">The URL to load in the browser.</param>
/// <param name="redirectUrl">The URL that's expected to be called.</param>
/// <returns>The parameters of the redirect URI request or an exception on failure.</returns>
Task<BrowserBasedInteractiveAuthenticationResult> PerformBrowserBasedInteractiveAuthentication(
InteractiveAuthenticationResource resource,
string loginUrl,
string redirectUrl
);

/// <summary>
/// Opens a URL in the default browser.
Expand Down Expand Up @@ -100,8 +146,8 @@ DialogResult ShowException(
void ShowNotificationBar(
string key,
string message,
Action? activate = null,
Action? dismiss = null
Action<IWin32Window>? activate = null,
Action<IWin32Window>? dismiss = null
);

/// <summary>
Expand Down Expand Up @@ -131,6 +177,27 @@ void ShowNotificationBar(
/// event to force a restart of the app if the user changes
/// configuration that requires a restart of the app.
/// </remarks>
/// <param name="mode">Mode in which to shutdown the app.</param>
/// <param name="mode">Mode used when shutting down the app.</param>
void Shutdown(RestartMode mode);
}

/// <summary>
/// Describes a resource (e.g. a connection) for interactive authentication.
/// </summary>
/// <param name="PluginId">ID of the plugin that the resource belongs to.</param>
/// <param name="ResourceId">Unique ID of the resource.</param>
/// <param name="ResourceName">Name of the resource.</param>
/// <param name="ResourceIcon">Icon of the resource.</param>
public record InteractiveAuthenticationResource(
Guid PluginId,
Guid ResourceId,
string ResourceName,
ImageSource ResourceIcon
);

/// <summary>
/// Result of a browser based interactive authentication request.
/// </summary>
/// <param name="Url">The called URL.</param>
/// <param name="QueryString">The parsed parameters of the called URI.</param>
public record BrowserBasedInteractiveAuthenticationResult(Uri Url, NameValueCollection QueryString);
3 changes: 3 additions & 0 deletions src/Tql.App/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
using Tql.App.Services.Profiles;
using Tql.App.Services.Synchronization;
using Tql.App.Services.Telemetry;
using Tql.App.Services.UIService;
using Tql.App.Services.Updates;
using Tql.App.Support;
using Application = System.Windows.Application;
Expand Down Expand Up @@ -406,6 +407,8 @@ private static void ConfigureServices(IServiceCollection builder)
builder.AddSingleton<ILifecycleService, LifecycleService>();
builder.AddSingleton<MainWindow>();
builder.AddHostedService<TermsOfServiceNotificationService>();
builder.AddSingleton<InteractiveAuthenticationWindow.Factory>();
builder.AddSingleton<BrowserBasedInteractiveAuthenticationWindow.Factory>();

builder.AddSingleton<IHostedService>(p => p.GetRequiredService<SynchronizationService>());
builder.AddSingleton<IHostedService>(p => p.GetRequiredService<MainWindow>());
Expand Down
15 changes: 8 additions & 7 deletions src/Tql.App/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using Tql.App.Services;
using Tql.App.Services.Database;
using Tql.App.Services.Telemetry;
using Tql.App.Services.UIService;
using Tql.App.Support;
using Tql.Utilities;

Expand Down Expand Up @@ -354,8 +355,6 @@ private void DoShow(bool force)

RenderStack();

_ui.SetMainWindow(this);

UpdateClearVisibility();

ReloadNotifications();
Expand Down Expand Up @@ -405,7 +404,7 @@ public void SetShowInTaskbar(bool visible)
// the taskbar button re-appearing next time the app is opened.
// Maybe that's the bug they mean.
//
// The work around is that the next time we show the app, we
// The workaround is that the next time we show the app, we
// reset the ShowInTaskbar property "properly" (see the
// ResetShowInTaskbar method). This seems to work fine.

Expand Down Expand Up @@ -533,8 +532,6 @@ var window in Application.Current.Windows.OfType<Window>().Where(p => p.Owner ==
if (haveQuickStartChildWindow)
_quickStartManager.Close();

_ui.SetMainWindow(null);

Visibility = Visibility.Hidden;

DisposeSearchManager();
Expand Down Expand Up @@ -952,7 +949,9 @@ private void NotificationBarUserControl_Activated(object? sender, UINotification

ReloadNotifications();

e.Notification.Activate?.Invoke();
var handle = new WindowInteropHelper(this).Handle;

e.Notification.Activate?.Invoke(new Win32Window(handle));
}

private void NotificationBarUserControl_Dismissed(object? sender, UINotificationEventArgs e)
Expand All @@ -961,7 +960,9 @@ private void NotificationBarUserControl_Dismissed(object? sender, UINotification

ReloadNotifications();

e.Notification.Dismiss?.Invoke();
var handle = new WindowInteropHelper(this).Handle;

e.Notification.Dismiss?.Invoke(new Win32Window(handle));
}

private void _results_SelectionChanged(object sender, SelectionChangedEventArgs e)
Expand Down
1 change: 1 addition & 0 deletions src/Tql.App/QuickStart/QuickStartManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Windows.Interop;
using Tql.Abstractions;
using Tql.App.Services;
using Tql.App.Services.UIService;
using Tql.App.Support;
using Tql.Utilities;

Expand Down
Binary file removed src/Tql.App/Resources/Radar.png
Binary file not shown.
7 changes: 1 addition & 6 deletions src/Tql.App/Search/SearchManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -262,12 +262,7 @@ public async void DoSearch(bool isUpdate = false)
_ui.ShowNotificationBar(
ErrorBannerId.ToString(),
$"{Labels.SearchManager_SearchFailedWithErrorMessage} {ex.Message}",
() =>
_ui.ShowException(
((UI)_ui).MainWindow!,
Labels.SearchManager_SearchFailedWithError,
ex
)
p => _ui.ShowException(p, Labels.SearchManager_SearchFailedWithError, ex)
);
}
}
Expand Down
9 changes: 5 additions & 4 deletions src/Tql.App/Services/PluginManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Tql.Abstractions;
using Tql.App.Services.Packages;
using Tql.App.Services.Packages.PackageStore;
using Tql.App.Services.UIService;
using Tql.App.Support;
using Tql.Utilities;

Expand Down Expand Up @@ -122,10 +123,10 @@ private void ShowLoadErrors(IServiceProvider serviceProvider)
Activate
);

void Activate()
void Activate(IWin32Window owner)
{
var result = ui.ShowConfirmation(
ui.MainWindow!,
owner,
string.Format(Labels.PluginManager_PackageFailedToLoad, package.Id.Id),
string.Format(
Labels.PluginManager_PackageFailedToLoadSubtitle,
Expand All @@ -149,10 +150,10 @@ void Activate()
Activate
);

void Activate()
void Activate(IWin32Window owner)
{
var result = ui.ShowConfirmation(
ui.MainWindow!,
owner,
string.Format(
Labels.PluginManager_PluginFailedToLoad,
plugin.Plugin.Title
Expand Down
7 changes: 1 addition & 6 deletions src/Tql.App/Services/TermsOfServiceNotificationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,16 +71,11 @@ private async Task CheckTermsOfService()
ui.ShowNotificationBar(
GetType().FullName!,
Labels.TermsOfServiceNotificationService_Updated,
OpenTermsOfService
_ => ui.OpenUrl(Constants.TermsOfServiceUrl)
);
}

settings.LastTermsOfServiceHash = hash;
}
}

private void OpenTermsOfService()
{
ui.OpenUrl(Constants.TermsOfServiceUrl);
}
}
3 changes: 0 additions & 3 deletions src/Tql.App/Services/UINotification.cs

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace Tql.App.Services.UIService;

internal class BrowserBasedInteractiveAuthenticationException : Exception
{
public BrowserBasedInteractiveAuthenticationException() { }

public BrowserBasedInteractiveAuthenticationException(string? message)
: base(message) { }

public BrowserBasedInteractiveAuthenticationException(
string? message,
Exception? innerException
)
: base(message, innerException) { }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<utilities:BaseWindow
x:Class="Tql.App.Services.UIService.BrowserBasedInteractiveAuthenticationWindow"
x:ClassModifier="internal"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:app="clr-namespace:Tql.App"
xmlns:utilities="clr-namespace:Tql.Utilities;assembly=Tql.Utilities"
xmlns:wv2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
mc:Ignorable="d"
Title="{x:Static app:Labels.ApplicationTitle}"
WindowStartupLocation="CenterScreen"
ResizeMode="NoResize"
Height="800"
Width="600"
Loaded="BaseWindow_Loaded"
Unloaded="BaseWindow_Unloaded">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>

<Grid Margin="9">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>

<Image
x:Name="_resourceIcon"
Height="32"
Margin="0,0,10,0"
VerticalAlignment="Top" />

<StackPanel
Orientation="Vertical"
Grid.Column="1">
<TextBlock
Margin="0,0,0,4"
FontWeight="Bold"
Text="{x:Static app:Labels.InteractiveAuthenticationWindow_TheFollowingResourceRequiresYourCredentialsLabel}" />
<TextBlock x:Name="_resourceName">RESOURCE NAME</TextBlock>
</StackPanel>
</Grid>

<wv2:WebView2
Name="_webView"
Grid.Row="1"
Grid.ColumnSpan="2" />
</Grid>
</utilities:BaseWindow>
Loading

0 comments on commit e78065f

Please sign in to comment.