Google OAuth in WinUI? Here is how to do it!

30.1.2024

Introduction

In the world of application development, implementing authentication mechanisms is a crucial task. One such mechanism is Google OAuth, a popular choice due to its wide acceptance and ease of use. However, when it comes to integrating Google OAuth in WinUI, developers often find themselves in uncharted waters due to the scarcity of official documentation and tutorials from Google or any other source. This article aims to bridge this gap by providing a step-by-step guide on how to successfully implement Google OAuth in WinUI. Let's embark on this journey together!

Prerequisites

Before we begin, make sure you have the following prerequisites on your machine:

  • Visual Studio 2022
  • .NET 6.0 SDK
  • Installed WinUI 3 Environment in Visual Studio 2022
  • Template Studio for WinUI extension in Visual Studio
  • A WinUI project created using the Template Studio for WinUI extension
  • OAuth 2.0 Credentials from Google

Step 1: Add the Google OAuth Credentials

If you know what you're doing, you can basically save these credentials in whatever way you prefer. I personally elected to put them in the App.xaml file. To do this, add the following code to the App.xaml file:

<Application.Resources>
    <ResourceDictionary>
        <...>
        <x:String x:Key="GoogleClientId">Your Client Id</x:String>
        <x:String x:Key="GoogleAuthUri">Your Auth Uri (probably google)</x:String>
        <x:String x:Key="GoogleRedirectUri">Your Redirect URI (we'll get to this later)</x:String>
    </ResourceDictionary>
</Application.Resources>
xml
Copy

Then we need to make these values available to the code where you elect to implement your login button.

Step 2: Create the Login Button

The first step is to create a button that will be used to initiate the Google OAuth process. To do this create a Button in your desired location and create a Click event handler for it. In the event handler, we start with the following code:

private void GoogleLogin_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
    string state = randomDataBase64url(32);
    string code_verifier = randomDataBase64url(32);
    string code_challenge = base64urlencodeNoPadding(sha256(code_verifier));
    const string code_challenge_method = "S256";

    <...>
}
cs
Copy

As you see we generate these values to login. These functions are exactly the same as the ones in the WPF example project from Google. You can just copy the functions to your project.

public static string randomDataBase64url(uint length)
{
    IBuffer buffer = CryptographicBuffer.GenerateRandom(length);
    return base64urlencodeNoPadding(buffer);
}

public static IBuffer sha256(string inputString)
{
    HashAlgorithmProvider sha = HashAlgorithmProvider.OpenAlgorithm(HashAlgorithmNames.Sha256);
    IBuffer buff = CryptographicBuffer.ConvertStringToBinary(inputString, BinaryStringEncoding.Utf8);
    return sha.HashData(buff);
}

public static string base64urlencodeNoPadding(IBuffer buffer)
{
    string base64 = CryptographicBuffer.EncodeToBase64String(buffer);

    // Converts base64 to base64url.
    base64 = base64.Replace("+", "-");
    base64 = base64.Replace("/", "_");
    // Strips padding.
    base64 = base64.Replace("=", "");

    return base64;
}
cs
Copy

Now we save two of these values in the local settings of the app. We need them later to verify the login. And the variables would be lost if we don't save them.

private void GoogleLogin_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
    <...>

    ApplicationDataContainer localSettings = ApplicationData.Current.LocalSettings;
    localSettings.Values["state"] = state;
    localSettings.Values["code_verifier"] = code_verifier;

    <...>
}
cs
Copy

Now that we have all of these things we can generate the login URL. This is done with the following code:

private void GoogleLogin_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
    <...>

    string authorizationRequest = string.Format("{0}?response_type=code&scope=openid%20email%20profile&redirect_uri={1}&client_id={2}&state={3}&code_challenge={4}&code_challenge_method={5}",
        OAuthAuthUri,
        System.Uri.EscapeDataString(OAuthRedirectUri),
        OAuthClientId,
        state,
        code_challenge,
        code_challenge_method);

    <...>
}
cs
Copy

Now we have the URL and we can open it in the browser. To do this we use the following code:

private void GoogleLogin_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
    <...>

    var uri = new Uri(authorizationRequest);
    var success = Windows.System.Launcher.LaunchUriAsync(uri);

    <...>
}
cs
Copy

Great now when we click the button the browser should open and show us the login page.

Login Page Logo

But now when we login nothing happens. We need to get the code from the URL.

Part 3: The Redirect URI

To get the code from the URL we need to create Protocol Declaration. This is an identifier that browsers can use to open the app. This is similar to the way that mailto: works. To do this we need to add the declaration in the Package.appxmanifest file. To do this open the file and go to the Declarations tab. Then click on Protocol and add the following values:

Protocol Declaration Logo

  • A display name for the protocol. This can be anything you want. I chose App.
  • A name for the protocol. This is the identifier that the browser will use to open the app. I chose app.org.windows.

This same redirect URI needs to be added in the ´App.xaml´ file. This is the same URI that we used in the login URL.

Now that we have done this we see that the browser opens the app when we login. But what seems a bit odd is that the browser opens a second process of the app.

Part 4: Getting back to the original process

When the second process opens the OnLaunched event is called. This is the same event that is called when the app is opened normally. So we need to check if the app was opened normally or if it was opened by the browser. To do this we use the following code in App.xaml.cs:

protected async override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
{
    var myInstance = AppInstance.GetCurrent();
    var keyInstance = AppInstance.FindOrRegisterForKey("linkwave-windows");
    var activatedArgs = myInstance.GetActivatedEventArgs();

    <...>
}
cs
Copy

Using this we now have two processes. If this is the first time the app was opened the Process registered with the key linkwave-windows will be the same as the current process. If this is the second process the process registered with the key linkwave-windows will be different from the current process. So we can use this to check if the app was opened normally or by the browser.

protected async override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
{
    <...>

    if (keyInstance.IsCurrent)
    {
        keyInstance.Activated += OnActivated;
        base.OnLaunched(args);
        await App.GetService<IActivationService>().ActivateAsync(args);
    }
    else if (activatedArgs.Kind == ExtendedActivationKind.Protocol)
    {
        await keyInstance.RedirectActivationToAsync(activatedArgs);
        Process currentProcess = Process.GetCurrentProcess();
        currentProcess.Kill();
    }
    else
    {
        var keyProcess = Process.GetProcessesByName("LinkWaveUI").FirstOrDefault(i => i.Id == keyInstance.ProcessId);
        if (keyProcess != null)
        {
            SetForegroundWindow(keyProcess.MainWindowHandle);
        }

        Process currentProcess = Process.GetCurrentProcess();
        currentProcess.Kill();
    }
}
cs
Copy

First we check if the app was opened normally. If it was simply activate the App event. But before we do that we need to make sure that there is a way to go back to the main process. To do this we register the function OnActivated (below) to the Activated event. Then we check if the app was opened by the browser. If it was we call the RedirectActivationToAsync function. This is a function that can simulate the OnActivated event the main process just subscribed to. Then we kill the current process. This is because we don't need it anymore. The third check is just so that we can move the main process to the front. This is just in case the app is opened a second time the normal non browser way.

And yes this means that we effectively Single-Instanced the app. For most applications this should be enough and if we do not need multiple instances of the app this is the best way to do it as doing OAuth safely and reliably is way easier without multiple instances.

Part 5: Back in the main process

Now we need to implement the OnActivated function. This is the function that is called when the main process is called from the browser process. To do this we use the following code:

private static void OnActivated(object sender, AppActivationArguments args)
{
    ExtendedActivationKind kind = args.Kind;
    if (kind != ExtendedActivationKind.Protocol) return;

    var protocolArgs = args.Data as IProtocolActivatedEventArgs;
    if (protocolArgs == null) return;

    var uri = protocolArgs.Uri;
    App.MainWindow.DispatcherQueue.TryEnqueue(() =>
    {
        App.MainWindow.BringToFront();
        App.GetService<INavigationService>().NavigateTo(typeof(AccountLoginViewModel).FullName!, uri);
    });
}
cs
Copy

First we check if the event was called by the browser. If it was we get the URI from the event arguments. Then we use the INavigationService to navigate to the AccountLoginViewModel. This is the ViewModel that Corresponds with the page where we came from in this case (it really doesn't matter which page you go to or if you even want to navigate to a page). Then we pass the URI to the ViewModel. This is because we need the URI to get the code from the URL.

Part 6: Getting the code from the URL

Now the page we came from with the button receives an OnNavigatedTo event. This is the event that is called when the page is navigated to. We can simply create a function that receives the URI and gets the code from it. To do this we use the following code:

protected override async void OnNavigatedTo(NavigationEventArgs e)
{
    if (e.Parameter is Uri)
    {
      <...>
    }
}
cs
Copy

If the parameter is a URI it means that the page was navigated to from the browser. So we can get the code from the URI. To do this we use the following code:

Uri authorizationResponse = (Uri)e.Parameter;
string queryString = authorizationResponse.Query;

Dictionary<string, string> queryStringParams =
    queryString.Substring(1).Split('&')
    .ToDictionary(c => c.Split('=')[0],
    c => Uri.UnescapeDataString(c.Split('=')[1]));
cs
Copy

Then we need to check if the required values are in the URL. To do this we use the following code:

if (queryStringParams.ContainsKey("error"))
{
    return;
}

if (!queryStringParams.ContainsKey("code")
    || !queryStringParams.ContainsKey("state"))
{
    return;
}
string code = queryStringParams["code"];
string incoming_state = queryStringParams["state"];
cs
Copy

Now we need to verify wether the response is legitimate. We saved the state and the code verifier in the local settings. So we can use them to verify the response. To do this we use the following code:

ApplicationDataContainer localSettings = ApplicationData.Current.LocalSettings;
string expected_state = (String)localSettings.Values["state"];

if (incoming_state != expected_state)
{
    return;
}

localSettings.Values["state"] = null;
cs
Copy

Now we need to get the code verifier from the local settings. To do this we use the following code (This function is exactly the same as the one in the WPF example project from Google):

async void performCodeExchangeAsync(string code, string code_verifier)
{
    string tokenRequestBody = string.Format("code={0}&redirect_uri={1}&client_id={2}&code_verifier={3}&scope=&grant_type=authorization_code",
        code,
        System.Uri.EscapeDataString(redirectURI),
        clientID,
        code_verifier
        );
    StringContent content = new StringContent(tokenRequestBody, Encoding.UTF8, "application/x-www-form-urlencoded");

    HttpClientHandler handler = new HttpClientHandler();
    handler.AllowAutoRedirect = true;
    HttpClient client = new HttpClient(handler);

    HttpResponseMessage response = await client.PostAsync(tokenEndpoint, content);
    string responseString = await response.Content.ReadAsStringAsync();

    if (!response.IsSuccessStatusCode)
    {
        return;
    }

    JsonObject tokens = JsonObject.Parse(responseString);
    string accessToken = tokens.GetNamedString("access_token");
    client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken);

    HttpResponseMessage userinfoResponse = client.GetAsync(userInfoEndpoint).Result;
    string userinfoResponseContent = await userinfoResponse.Content.ReadAsStringAsync();
}
cs
Copy

Now you have your keys in userinfoResponseContent and you can use it to get access to your app in the way you envisioned it.

Conclusion

And that's it! You have successfully implemented Google OAuth in WinUI. I hope this article was helpful to you. Happy coding!