Google Oauth in WinUI3

I was creating an app using WinUI3, the newest app framework Microsoft materialised out of UWP. I wanted to add 'Sign in with Google' to the app, however Google only gave an example with with WPF and UWP, so time to figure how to do it in WinUI3!

If you know anything about Microsoft, they created many frameworks that are essentially incompatible with each other, each with varying feature sets, that they soon slowly start to abandon without actually deprecating. It's likely that Google didn't come around to adding an example for WinUI3, probably since people just use Electron these days, and most serious Windows apps don't use WinUI3. Even CefSharp, Chromium Embedded Framework for C#, only supports WinForms from 2001 and WPF from 2006.

<Extensions>
  <uap:Extension Category="windows.protocol">
    <uap:Protocol Name="appname.oauth2"/>
  </uap:Extension>
</Extensions>

I first created a OAuth client ID so that I can create the authorisation request for my app. Google cloud console had UWP, Android, iOS, etc. but no WinUI3 template, so I had to use iOS to set I up. I set the bundle ID as appname.oauth2. I also changed the Package.appxmanifest to declare the protocol I just defined.

The URL generated from the browser after the authentication request would be in the form appname.oauth2:/oauth2redirect?..., I just need to figure out how to receive this. I created a Program.cs file, the entry point of an app, that redirects the ActivatedEventArgs to the primary app instance, so when the URL redirects, the Uri is received by the correct window.

class program {

    [STAThread]
    static int Main(string[] args) {

        WinRT.ComWrappersSupport.InitializeComWrappers();

        if (DecideRedirection().Result) { return 0; }
        
        Microsoft.UI.Xaml.Application.Start((p) => {
            SynchronizationContext.SetSynchronizationContext(
                new DispatcherQueueSynchronizationContext(DispatcherQueue.GetForCurrentThread()));
                
            new App();
        });

        return 0;
    }

    private async static Task DecideRedirection() {

        AppInstance keyInstance = AppInstance.FindOrRegisterForKey("main");
        if (keyInstance.IsCurrent) { return false; }
        await keyInstance.RedirectActivationToAsync(AppInstance.GetCurrent().GetActivatedEventArgs());

        return true;
    }
}

Unlike UWP where they had proper activation and lifecycle management, WinUI3 is more of a manual approach.

MainInstanceActivated is invoked when a redirection request is received, however compared to OnLaunched(), this isn't called on the UI thread, therefore to navigate back to the login page we need to use the DispatcherQueue. We also unpack the response Uri from the args and send it in the navigate request.

public App() {
    
    this.InitializeComponent();

    mainInstance = Microsoft.Windows.AppLifecycle.AppInstance.FindOrRegisterForKey("main");
    mainInstance.Activated += MainInstanceActivated;
}

private void MainInstanceActivated(object? sender, AppActivationArguments e) {

    this.mainWindow.DispatcherQueue.TryEnqueue(() => {
        RootFrame.Navigate(typeof(LoginPage), ((ProtocolActivatedEventArgs)e.Data).Uri, 
            new SlideNavigationTransitionInfo() { Effect = SlideNavigationTransitionEffect.FromRight });
    });

    WindowHelper.ShowWindow(mainWindow);
}
WinUI3 AppLoginPage.xaml.csProgram.csApp.xaml.csBrowserLaunchUri for authorizationRequestInvoked by declaredURL protocolRedirectActivation to primary AppInstanceEnqueue Navigate requestin DispatcherQueue

Once we navigate to the LoginPage the OnNavigatedTo function would be triggered in LoginPage.xaml.cs with the Uri as the parameter. At that point the login page can take over and extract, verify and process the Uri. Finally!

Probably there's an easier WPF style way to doing this. This is more UWP style.

private static class WindowHelper {
    [DllImport("user32.dll")]
    private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);

    [DllImport("user32.dll")]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool SetForegroundWindow(IntPtr hWnd);

    public static void ShowWindow(Window window) {
        var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(window);
        ShowWindow(hwnd, 0x00000009);
        SetForegroundWindow(hwnd);
    }
}

Side note, you might have noticed that I explicitly call WindowHelper.ShowWindow(). That's since Microsoft still hasn't patched the regression of bringing the window to foreground when mainInstance.Activated is called.

I just ended up adding a separate class directly calling user32.dll, the core API for user interfaces, to bring the window to the foreground. The 0x00000009 is SW_RESTORE that activates and displays the window.

The code for creating the Google Oauth request itself from the client ID, and processing it once the response is received in the login page, is easily available in Google's own documentation. Hv Fun!!