Skip to main content

Authentication Guide

To support authentication workflows in your Capacitor Windows app, such as when using Ionic Auth Connect, there are a few changes that need to be made to your project.

First, the app must support single instancing so authentication redirects back to the app do not open multiple windows. Then, some additional code must be added to pass the authentication callback data to the web view to be processed.

Packaged Apps using Windows App SDK:#

First, we will need to add a custom Main method to the app, which requires disabling the automatic Main that Visual Studio generates in our app.

Open your App.csproj in an editor and add the <PropertyGroup> entries for DISABLE_XAML_GENERATED_MAIN as shown in the example below:

App.csproj
<Project Sdk="Microsoft.NET.Sdk">    <PropertyGroup>        <OutputType>WinExe</OutputType>        <TargetFramework>net5.0-windows10.0.19041.0</TargetFramework>        <TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>        <RootNamespace>App</RootNamespace>        <ApplicationManifest>app.manifest</ApplicationManifest>        <Platforms>x86;x64;arm64</Platforms>        <RuntimeIdentifiers>win10-x86;win10-x64;win10-arm64</RuntimeIdentifiers>        <UseWinUI>true</UseWinUI>    </PropertyGroup>
    <!-- START: ADD THESE -->    <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x86'">        <DefineConstants>DISABLE_XAML_GENERATED_MAIN</DefineConstants>    </PropertyGroup>    <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x86'">        <DefineConstants>DISABLE_XAML_GENERATED_MAIN</DefineConstants>    </PropertyGroup>    <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">        <DefineConstants>DISABLE_XAML_GENERATED_MAIN</DefineConstants>    </PropertyGroup>    <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">        <DefineConstants>DISABLE_XAML_GENERATED_MAIN</DefineConstants>    </PropertyGroup>    <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|arm64'">        <DefineConstants>DISABLE_XAML_GENERATED_MAIN</DefineConstants>    </PropertyGroup>    <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|arm64'">        <DefineConstants>DISABLE_XAML_GENERATED_MAIN</DefineConstants>    </PropertyGroup>    <!-- END -->
    <ItemGroup>        <PackageReference Include="Microsoft.WindowsAppSDK" Version="1.0.2" />        <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />        <PackageReference Include="Capacitor" Version="1.*" />        <Manifest Include="$(ApplicationManifest)" />    </ItemGroup></Project>

Next, create a new .cs file named Program.cs inside of the App project, with the following contents. This will be the new Main entry point for the app and will manage app instancing and sending through Protocol events that will happen when our app is opened from an authentication redirect:

Program.cs
using Microsoft.UI.Dispatching;using Microsoft.Windows.AppLifecycle;using System;using System.Threading;using System.Threading.Tasks;using Windows.ApplicationModel.Activation;
namespace App {    // NOTE: We need to check for redirection as early as possible, and    // before creating any windows. To do this, we must define this symbol    // in the project build properties:    // DISABLE_XAML_GENERATED_MAIN    // ...and define a custom Program class with a Main method 
    public static class Program {        private static string INSTANCE_KEY = "myApp";
        // Replaces the standard App.g.i.cs.        // Note: We can't declare Main to be async because in a WinUI app        // this prevents Narrator from reading XAML elements.        [STAThread]        static void Main(string[] args) {            WinRT.ComWrappersSupport.InitializeComWrappers();
            bool isRedirect = DecideRedirection();            if (!isRedirect) {                Microsoft.UI.Xaml.Application.Start((p) => {                    var context = new DispatcherQueueSynchronizationContext(                        DispatcherQueue.GetForCurrentThread());                    SynchronizationContext.SetSynchronizationContext(context);                    new App();                });            }        }
        #region Report helpers
        private static void OnActivated(object sender, AppActivationArguments args) {            ExtendedActivationKind kind = args.Kind;            if (kind == ExtendedActivationKind.Protocol) {                if (args.Data is IProtocolActivatedEventArgs protoArgs) {                    if (App.Current is App thisApp && thisApp.AppWindow != null &&                        thisApp.AppWindow is MainWindow thisWindow) {                        thisWindow.HandleRedirect(protoArgs.Uri);                    }
                }            }        }
        #endregion

        #region Redirection
        // Decide if we want to redirect the incoming activation to another instance.        private static bool DecideRedirection() {            bool isRedirect = false;
            // Find out what kind of activation this is.            AppActivationArguments args = AppInstance.GetCurrent().GetActivatedEventArgs();            ExtendedActivationKind kind = args.Kind;            if (kind == ExtendedActivationKind.Launch) {                // This is a launch activation.                AppInstance keyInstance = AppInstance.FindOrRegisterForKey(INSTANCE_KEY);
                // If we successfully registered the file name, we must be the                // only instance running that was activated for this file.                if (keyInstance.IsCurrent) {                    // Hook up the Activated event, to allow for this instance of the app                    // getting reactivated as a result of multi-instance redirection.                    keyInstance.Activated += OnActivated;                }            } else if (kind == ExtendedActivationKind.Protocol) {
                try {                    // This is a file activation: here we'll get the file information,                    // and register the file name as our instance key.                    if (args.Data is IProtocolActivatedEventArgs protoArgs) {                        var uri = protoArgs.Uri;                        AppInstance keyInstance = AppInstance.FindOrRegisterForKey(INSTANCE_KEY);
                        // If we successfully registered the file name, we must be the                        // only instance running that was activated for this file.                        if (keyInstance.IsCurrent) {                            // Report successful file name key registration.
                            // Hook up the Activated event, to allow for this instance of the app                            // getting reactivated as a result of multi-instance redirection.                            keyInstance.Activated += OnActivated;                        } else {                            isRedirect = true;                            RedirectActivationTo(args, keyInstance);                        }                    }                } catch (Exception ex) {                    throw;                }            }
            return isRedirect;        }
        private static IntPtr redirectEventHandle = IntPtr.Zero;
        // Do the redirection on another thread, and use a non-blocking        // wait method to wait for the redirection to complete.        public static void RedirectActivationTo(            AppActivationArguments args, AppInstance keyInstance) {            var redirectSemaphore = new Semaphore(0, 1);            Task.Run(() => {                keyInstance.RedirectActivationToAsync(args).AsTask().Wait();                redirectSemaphore.Release();            });            redirectSemaphore.WaitOne();        }
        #endregion
    }}

Add the following code to App.xaml.cs right inside of the App class to expose our app's Window instance:

App.xaml.cs
namespace App {    /// <summary>    /// Provides application-specific behavior to supplement the default Application class.    /// </summary>    public partial class App : Application {
      // ADD THIS:      public Window AppWindow {          get { return m_window; }          private set { }      }

Passing Auth Data#

To make sure any Protocol events that opened our app end up passing the correct authentication URL redirect back to the web view, add this method inside of the MainWindow class in your MainWindow.xaml.cs file. Note: this code is for Ionic Auth Connect and may need to be modified depending on your authentication library JS API:

MainWindow.xaml.cs
namespace App {    /// <summary>    /// An empty window that can be used on its own or navigated to within a Frame.    /// </summary>    public sealed partial class MainWindow : Window {        /* ... */
        // ADD THIS:        public async void HandleRedirect(Uri uri) {            var js = $@"var u = new URL('{uri.ToString()}');if (u.searchParams.get('code')) {{    window.IonicAuth.handleLoginCallback('{uri.ToString()}');}} else {{    window.IonicAuth.handleLogoutCallback();}}            ";
            this.CapacitorWebView.DispatcherQueue.TryEnqueue(() => {                _ = this.CapacitorWebView.ExecuteScriptAsync(js);            });        }

Register Protocol Handler#

Finally, our app must register to handle certain custom URL protocols in order to launch and handle auth redirects. Open Package.appxmanifest in the App (Package) project, then navigate to the Declarations tab. Select Protocol in the Available Declarations dropdown, then choose a Logo, Display name, and Name for the protocol. The Name field is the most important: this is the protocol or custom URL scheme you will use to redirect back to the app. For example, we chose ionic.cs.appsummit for our demo app because the redirect url will be of the form ionic.cs.appsummit:// which will trigger our app to load and handle the auth redirect.

Expose Ionic Auth#

If using Auth Connect, the main IonicAuth subclass must be assigned on window for the native side to access it.

In your main authentication service, add this code in the constructor or immediately after initializing your authentication service:

(window as any).IonicAuth = this;

Use Custom Protocol for Redirect#

Make sure to change the Redirect URL in your authentication service (on the backend) to use the custom protocol handler. For example, we use ionic.cs.appsummit://login in the settings for our cloud-based authentication service when redirecting back to the native windows app.