Search code examples
wpfasp.net-corejwt

ASP.NET Core Identity login scenario with WPF desktop app


I have implemented a standard out of the box ASP.NET Core Identity authentification solution for my web page login where user credentials are stored in a SQL Server database on the cloud. This works fine, no questions there.

However, I also have a desktop WPF app, that should use the same authentication infrastructure as the web app, as they will both be used by the same users and should be able to use the same credentials for both. Thus, the idea is that when a user is trying to login into the WPF desktop app, they are redirected to a simple login webpage in the browser, enter their credentials, and on successful login receives the JWT access token that could then be used by the desktop app for further actions.

I have seen such a flow used in multiple modern day Windows desktop applications (at least, I suspect they use such a flow), however I am struggling to understand how this accuired access token could be properly transfered from the login web app to my desktop application. All I can do at the moment is direct user to login page:

Process.Start(new ProcessStartInfo { FileName = @"https://www.myapplogin.com/", UseShellExecute = true });

but from here I have no reasonable idea how to let my desktop app understand that the login has been successful and proceed.

Can someone describe a potential workflow here for accomplishing this (preferably with code examples)?


Solution

  • Coincidentally I just completed something exactly like this with my WPF application.

    My solution was to use CEFSharp (Chromium Embedded Framework) to open a window to the web-based login page, then include JavaScript code in the login page that - if it's the desktop app logging in - invokes .NET code through interop to provide the JWT.

    Here is a step by step breakdown of what I do (of course this is not the only way):

    WPF Project

    1. Include CEFSharp for WPF in your project. (You could use WebView2 as well, but this can make your app more complicated to distribute potentially).

    2. To login a user, open a modal window that includes the ChromiumWebBrowser control. Set the URL to the same login page your web app would use. For styling this is what I use (make sure to include your own cancel/close button though):

       WindowStyle="None"        
       ResizeMode="NoResize"
       Height="650"
       Width="500"
       WindowStartupLocation="CenterScreen"
      
    3. You also need to provide a .NET class that your login page's JavaScript code will be able to access (more on that below). Let's call this LoginInterop. At some point (for example when the modal opens), you need to register an instance of this class with:

      _browser.JavascriptObjectRepository.Register("loginInterop", new LoginInterop());

    4. In LoginInterop include a method such as ProvideJwt(string jwt). Your login page's Javascript (when running in CEF) will be able to invoke this method and provide the JWT after login. (More on this below). This method should also close the dialog.

    Web Project

    I suggest in your main login page route you include a query option such as desktop=true - then set this on the WPF side when setting ChromiumWebBrowser.Address. Your client-side script will use this to determine that it's inside a CEF browser and not normal Chrome.

    At this point things become very much dependent on your specifics, but I'm assuming at some point your web app has access to the JWT client-side. (This means it can't merely be an HttpOnly session cookie). If it only uses cookie auth, you need to make an API GET endpoint that exchanges the session cookie for the JWT (beware of cross-origin vulnerabilities).

    Once login is complete and your client has access to the JWT - and assuming you're inside CEF based on the aforementioned query flag - then you can invoke your .NET code like so (FYI this is Typescript; plain JS will be a bit simpler):

       let cef = (window as any).CefSharp;
       await cef.BindObjectAsync("loginInterop");
       (window as any).loginInterop.provideJwt(jwt);
    

    (Note CEF camelcases method names when binding).

    One other caveat re: CEFSharp for WPF

    Literally yesterday we discovered that one of our users had a problem displaying the CEF window, apparently due to their graphics hardware and some conflict with WPF. The solution was to add:

        protected override void OnSourceInitialized(EventArgs e)
        {
            base.OnSourceInitialized(e);
            HwndSource source = PresentationSource.FromVisual(this) as HwndSource;
            if (source != null)
                source.CompositionTarget.RenderMode = RenderMode.SoftwareOnly;
        }
    

    Somewhere in the modal window class. I don't know if WebView2 has the same issue.

    Alternative Requiring no Embedded Browser

    I recently came across another way this can be done, and without needing to embed a browser in your application (which has its drawbacks). If you've ever signed into a streaming app like Disney+ on your phone using a code from the television you'll immediately get the gist of this -

    1. To sign in, do two things: (1) have your WPF app open a new browser process going to your login URL, providing it a random unique one time code (or nonce) such as a Guid. That will need to be provided as a URL query parameter for your login page. You'll also need the aformentioned flag that it's a desktop app login.
    2. During your server login process, generate the JWT as normal, and store it somewhere keyed to the nonce.
    3. Expose a new unauthenticated server API endpoint that accepts the nonce and returns the JWT, if the login has finished, immediately deleting it from its own storage. (Also the record should be shortlived if this API is never called - no more than five minutes say - and any lefotvers should all be purged whenever the server spins up).
    4. While the browser is open, your WPF app should be polling the aforementioned endpoint every few seconds with the nonce. Once the endpoint returns successfully and provides the JWT, the WPF app can kill the browser process.

    So as you can see there are many ways to approach this problem. I'm sure there are plenty of libraries that take care of most of these details for you too. I for one like to at least understand the fundamentals even if I am going to use a library, if not implement it from scratch.