IdentityModel / IdentityModel.OidcClient.Samples

Apache License 2.0
291 stars 162 forks source link

IBrowser implementation in WPF using a background thread #149

Closed Yabberfax closed 4 months ago

Yabberfax commented 5 months ago

Hi,

I am not that skilled in WPF and I would like to use the OIDCClient in a background service that interacts with an API. Everything works fine when I launch the code from the UI, i.e. when starting my application. I can then store my token and use it, in my case for 5 mins, as it then expires. The API sends me a message that the token has expired, so I want to refresh it. Here is the problem. I dont have a UI thread there and whatever I try to do with treads, task, dispatchers I have the issue that the IBrowser implementation needs to be directly called from the UI or STAThread as it seems.

Is there a solution for this issue?

josephdecock commented 4 months ago

A background service sounds like machine to machine interaction, which would make OIDC the wrong protocol to use. Do you have an interactive user?

Yabberfax commented 4 months ago

Maybe service is not the correct word, it is some class that does not have access to the UI thread and this is exactly where i need to refresh the token in case of its expiration, 5 mins in my case.. However I managed to solve it in the meantime by creating a view that contains the webview2 and instead of creating the window on each login/refresh in the IBrowser implementation I assign the existing view.

So in any initialization where you have UI access in WPF you can create the window and instead of closing it at the end you just collapse it.

Maybe there are better approaches, but as WPF is not my topic thats how it implemented it.

So the XAML is basically this

<window ....
        Height="1000"
        Title="KeyCloak Login"
        Width="600">
    <Grid>
        <wpf:WebView2 Name="WebView" Margin="0"/>
    </Grid>
</Window>

The code-behind looks like this

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using IdentityModel.OidcClient.Browser;
using Microsoft.Web.WebView2.Core;

namespace XXX.Shell.UserInterface.Views
{
    public partial class KeyCloakWindow : Window, IDisposable
    {
        private SemaphoreSlim semaphoreSlim;
        private BrowserOptions browserOptions;
        private bool disposed;
        private bool webViewInitialized = false;

        public KeyCloakWindow()
        {
            this.InitializeComponent();

            this.WebView.NavigationStarting += (s, e) =>
            {
                if (this.IsBrowserNavigatingToRedirectUri(new Uri(e.Uri)))
                {
                    e.Cancel = true;

                    this.BrowserResult = new BrowserResult()
                    {
                        ResultType = BrowserResultType.Success,
                        Response = new Uri(e.Uri).AbsoluteUri
                    };

                    // maybe enough in dispose
                    this.semaphoreSlim.Release();

                    this.Visibility = Visibility.Collapsed;
                }
            };
        }

        public BrowserResult BrowserResult { get; set; }

        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }

        public async Task<BrowserResult> Login(BrowserOptions options)
        {
            // Navigate
            this.browserOptions = options;

            if (this.browserOptions.StartUrl.Contains("kc_idp_hint=kc-oidc-ad-emp-krb"))
            {
                this.ShowInTaskbar = false;
                this.Height = 0;
                this.Width = 0;
            }
            else
            {
                this.ShowInTaskbar = true;
                this.Height = 1000;
                this.Width = 600;
            }

            this.WindowStartupLocation = WindowStartupLocation.CenterScreen;

            this.semaphoreSlim = new SemaphoreSlim(0, 1);
            this.BrowserResult = new BrowserResult()
            {
                ResultType = BrowserResultType.UserCancel
            };

            this.Show();
            this.Visibility = Visibility.Visible;

            await this.InitializeAsync();

            this.WebView.CoreWebView2.Navigate(this.browserOptions.StartUrl);

            await this.semaphoreSlim.WaitAsync();

            return this.BrowserResult;
        }

        protected virtual void Dispose(bool disposing)
        {
            if (!this.disposed)
            {
                if (disposing)
                {
                    this.semaphoreSlim.Dispose();
                }
            }

            this.disposed = true;
        }

        private bool IsBrowserNavigatingToRedirectUri(Uri uri)
        {
            return uri.AbsoluteUri.StartsWith(this.browserOptions.EndUrl, System.StringComparison.CurrentCulture);
        }

        private async Task<bool> InitializeAsync()
        {
            // makes sure, that the WebView is only initalized once, after the window is shown
            if (!this.webViewInitialized == true)
            {
                var localData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + "\\KeyCloakWebView";
                var environment = await CoreWebView2Environment.CreateAsync(null, localData);
                await this.WebView.EnsureCoreWebView2Async(environment);

                // Delete existing Cookies so previous logins won't remembered
                this.WebView.CoreWebView2.CookieManager.DeleteAllCookies();

                this.webViewInitialized = true;
            }
            else
            {
                // Delete existing Cookies so previous logins won't be remembered, in case of non-SSO, to allow impersonation
                if (!this.browserOptions.StartUrl.Contains("kc_idp_hint=kc-oidc-ad-emp-krb"))
                {
                    this.WebView.CoreWebView2.CookieManager.DeleteAllCookies();
                }
            }

            return true;
        }
    }
}

Then this is my IBrowswer implementation which needs the window from above in the constructor

namespace xxx.Shell.UserInterface
{

    public class KeyCloakWpfEmbeddedBrowserWindow : IBrowser
    {
        private KeyCloakWindow keyCloakWindow;

        public KeyCloakWpfEmbeddedBrowserWindow(KeyCloakWindow kcWindow)
        {
            this.keyCloakWindow = kcWindow;
        }

        public async Task<BrowserResult> InvokeAsync(BrowserOptions options, CancellationToken cancellationToken = default)
        {
            return await this.keyCloakWindow.Login(options);
        }
    }
}