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!
Before we begin, make sure you have the following prerequisites on your machine:
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>
Then we need to make these values available to the code where you elect to implement your 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";
<...>
}
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;
}
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;
<...>
}
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);
<...>
}
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);
<...>
}
Great now when we click the button the browser should open and show us the login page.
But now when we login nothing happens. We need to get the code from the URL.
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:
App
.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.
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();
<...>
}
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();
}
}
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.
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);
});
}
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.
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)
{
<...>
}
}
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]));
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"];
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;
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();
}
Now you have your keys in userinfoResponseContent
and you can use it to get access to your app in the way you envisioned it.
And that's it! You have successfully implemented Google OAuth in WinUI. I hope this article was helpful to you. Happy coding!