Skip to content

Commit

Permalink
Fix logout by patching WinUIEx
Browse files Browse the repository at this point in the history
  • Loading branch information
frederikprijck committed Oct 25, 2023
1 parent 47ef0f9 commit 6f15b4c
Show file tree
Hide file tree
Showing 20 changed files with 1,773 additions and 211 deletions.
517 changes: 309 additions & 208 deletions Auth0.OidcClient.All.sln

Large diffs are not rendered by default.

52 changes: 52 additions & 0 deletions Auth0.OidcClient.MAUI.Platforms.Windows/Activator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using Microsoft.Windows.AppLifecycle;
using Windows.ApplicationModel.Activation;

namespace Auth0.OidcClient.Platforms.Windows
{
public sealed class Activator
{
internal static bool RedirectActivationCheck;

/// <summary>
/// Performs an protocol activation check and redirects activation to the correct application instance.
/// </summary>
public static bool CheckRedirectionActivation()
{
var activatedEventArgs = AppInstance.GetCurrent()?.GetActivatedEventArgs();

RedirectActivationCheck = true;

if (activatedEventArgs is null || activatedEventArgs.Kind != ExtendedActivationKind.Protocol || activatedEventArgs.Data is not IProtocolActivatedEventArgs protocolArgs)
{
return false;
}

var ctx = RedirectionContextManager.GetRedirectionContext(activatedEventArgs.Data as IProtocolActivatedEventArgs);

if (ctx is not null && ctx.AppInstanceKey is not null && ctx.TaskId is not null)
{
var instance = AppInstance.GetInstances().FirstOrDefault(i => i.Key == ctx.AppInstanceKey);

if (instance is not null && !instance.IsCurrent)
{
instance.RedirectActivationToAsync(activatedEventArgs).AsTask().Wait();

System.Diagnostics.Process.GetCurrentProcess().Kill();

return true;
}
}
else
{
var instance = AppInstance.GetCurrent();

if (string.IsNullOrEmpty(instance.Key))
{
AppInstance.FindOrRegisterForKey(Guid.NewGuid().ToString());
}
}
return false;
}

}
}
50 changes: 50 additions & 0 deletions Auth0.OidcClient.MAUI.Platforms.Windows/AppInstanceProxy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.Windows.AppLifecycle;

namespace Auth0.OidcClient.Platforms.Windows;

internal interface IAppInstanceProxy
{
event EventHandler<IAppActivationArguments> Activated;
string GetCurrentAppKey();
}

internal interface IAppActivationArguments
{
ExtendedActivationKind Kind { get; set; }
object Data { get; set; }
}

internal class AppActivationArguments : IAppActivationArguments
{
public ExtendedActivationKind Kind { get; set; }
public object Data { get; set; }
}

/// <summary>
/// Excludes from Code Coverage because of the integration with AppInstance.GetCurrent()
/// </summary>
[ExcludeFromCodeCoverage]
internal class AppInstanceProxy : IAppInstanceProxy
{
public AppInstanceProxy()
{
AppInstance.GetCurrent().Activated += OnActivated;
}

public event EventHandler<IAppActivationArguments> Activated;

protected virtual void OnActivated(object? sender, Microsoft.Windows.AppLifecycle.AppActivationArguments e)
{
Activated?.Invoke(this, new AppActivationArguments
{
Kind = e.Kind,
Data = e.Data
});
}

public virtual string GetCurrentAppKey()
{
return AppInstance.GetCurrent().Key;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net6.0-windows10.0.19041.0</TargetFrameworks>
<!-- Uncomment to also build the tizen app. You will need to install tizen by following this: https://github.com/Samsung/Tizen.NET -->
<!-- <TargetFrameworks>$(TargetFrameworks);net7.0-tizen</TargetFrameworks> -->
<UseMaui>true</UseMaui>
<SingleProject>true</SingleProject>
<ImplicitUsings>enable</ImplicitUsings>

<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</SupportedOSPlatformVersion>
<TargetPlatformMinVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</TargetPlatformMinVersion>
<AssemblyOriginatorKeyFile>..\build\Auth0OidcClientStrongName.snk</AssemblyOriginatorKeyFile>
</PropertyGroup>

</Project>
68 changes: 68 additions & 0 deletions Auth0.OidcClient.MAUI.Platforms.Windows/Helpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using System.Runtime.InteropServices;
using System.Xml;
using System.Xml.Linq;
using System.Xml.XPath;

namespace Auth0.OidcClient.Platforms.Windows
{
internal interface IHelpers
{
bool IsAppPackaged { get; }
bool IsUriProtocolDeclared(string scheme);
void OpenBrowser(Uri uri);
}
internal class Helpers : IHelpers
{
#pragma warning disable SA1203 // Constants should appear before fields
private const long AppModelErrorNoPackage = 15700L;
#pragma warning restore SA1203 // Constants should appear before fields

[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern int GetCurrentPackageFullName(ref int packageFullNameLength, System.Text.StringBuilder packageFullName);

public bool IsAppPackaged
{
get
{
try
{
// Application is MSIX packaged if it has an identity: https://learn.microsoft.com/en-us/windows/msix/detect-package-identity
int length = 0;
var sb = new System.Text.StringBuilder(0);
int result = GetCurrentPackageFullName(ref length, sb);
return result != AppModelErrorNoPackage;
}
catch
{
return false;
}
}
}

public bool IsUriProtocolDeclared(string scheme)
{
if (global::Windows.ApplicationModel.Package.Current is null)
return false;
var docPath = Path.Combine(global::Windows.ApplicationModel.Package.Current.InstalledLocation.Path, "AppxManifest.xml");
var doc = XDocument.Load(docPath, LoadOptions.None);
var reader = doc.CreateReader();
var namespaceManager = new XmlNamespaceManager(reader.NameTable);
namespaceManager.AddNamespace("x", "http://schemas.microsoft.com/appx/manifest/foundation/windows10");
namespaceManager.AddNamespace("uap", "http://schemas.microsoft.com/appx/manifest/uap/windows10");

// Check if the protocol was declared
var decl = doc.Root?.XPathSelectElements($"//uap:Extension[@Category='windows.protocol']/uap:Protocol[@Name='{scheme}']", namespaceManager);

return decl != null && decl.Any();
}

public void OpenBrowser(Uri uri)
{
var process = new System.Diagnostics.Process();
process.StartInfo.FileName = "rundll32.exe";
process.StartInfo.Arguments = $"url.dll,FileProtocolHandler \"{uri.ToString().Replace("\"", "%22")}\"";
process.StartInfo.UseShellExecute = true;
process.Start();
}
}
}
36 changes: 36 additions & 0 deletions Auth0.OidcClient.MAUI.Platforms.Windows/RedirectionContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System.Collections.Specialized;
using System.Text.Json.Nodes;

namespace Auth0.OidcClient.Platforms.Windows
{
internal class RedirectionContext
{
internal string TaskId { get; set; }
internal string AppInstanceKey { get; set; }

internal static RedirectionContext New(IAppInstanceProxy appInstanceProxy)
{
return new RedirectionContext
{
TaskId = Guid.NewGuid().ToString(),
AppInstanceKey = appInstanceProxy.GetCurrentAppKey()
};
}

internal JsonObject ToJsonObject(NameValueCollection query)
{
var jsonObject = new JsonObject
{
{ "appInstanceKey", AppInstanceKey },
{ "taskId", TaskId }
};

if (query["state"] is string oldState && !string.IsNullOrEmpty(oldState))
{
jsonObject["state"] = oldState;
}

return jsonObject;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using System.Runtime.CompilerServices;
using System.Text.Json.Nodes;
using Windows.ApplicationModel.Activation;

[assembly: InternalsVisibleTo("Auth0.OidcClient.MAUI.Platforms.Windows.UnitTests")]
namespace Auth0.OidcClient.Platforms.Windows
{
internal class RedirectionContextManager
{
internal static RedirectionContext? GetRedirectionContext(IProtocolActivatedEventArgs protocolArgs)
{
var vals = System.Web.HttpUtility.ParseQueryString(protocolArgs.Uri.Query);
var state = vals["state"];
JsonObject jsonObject = null;

if (!string.IsNullOrEmpty(state))
{

try
{
jsonObject = JsonNode.Parse(state) as JsonObject;
}
catch
{
jsonObject = JsonNode.Parse(Uri.UnescapeDataString(state)) as JsonObject;
}
}

if (jsonObject is not null)
{
return new RedirectionContext
{
AppInstanceKey = TryGetJsonValue(jsonObject, "appInstanceKey"),
TaskId = TryGetJsonValue(jsonObject, "taskId")
};
}
else
{
return null;
}
}

private static string? TryGetJsonValue(JsonObject jsonObject, string key)
{
if (jsonObject.ContainsKey(key) && jsonObject[key] is JsonValue jValue && jValue.TryGetValue(out string value))
{
return value;
}

return null;
}
}
}
72 changes: 72 additions & 0 deletions Auth0.OidcClient.MAUI.Platforms.Windows/StateModifier.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using System.Text.Json.Nodes;

namespace Auth0.OidcClient.Platforms.Windows;

internal class StateModifier
{
internal static Uri MoveStateToReturnTo(Uri uri)
{
var query = System.Web.HttpUtility.ParseQueryString(uri.Query);
// The state QueryString as generated by WinUIEx
var state = query["state"];
// The original returnTo as configured externally
var returnTo = query["returnTo"];


UriBuilder returnToBuilder = new UriBuilder(returnTo);

// Get the original returnTo querystring params, so we can append state to it
var returnToQuery = System.Web.HttpUtility.ParseQueryString(new Uri(returnTo).Query);
// Append state as a querystring parameter to returnTo
// We need to escape it for it to be accepted
returnToQuery["state"] = Uri.EscapeDataString(state);
// Set the query again on the returnTo url
returnToBuilder.Query = returnToQuery.ToString();

// Update returnTo in the original query so that it now includes state
query["returnTo"] = returnToBuilder.Uri.ToString();

UriBuilder logoutUrlBuilder = new UriBuilder(uri);
// Set the query again on the logout url
logoutUrlBuilder.Query = query.ToString();

// Return the Uri so it can be used internally by WinUIEx to start the process and open the browser
return logoutUrlBuilder.Uri;
}

internal static Uri ResetRawState(Uri uri)
{
var query = System.Web.HttpUtility.ParseQueryString(uri.Query);

var state = query["state"];

JsonObject jsonObject;
try
{
jsonObject = JsonNode.Parse(state ?? "{}") as JsonObject;

}
catch (Exception ex)
{

jsonObject = JsonNode.Parse(Uri.UnescapeDataString(state)) as JsonObject;
}

var originalState = jsonObject["state"];

if (originalState is not null)
{
query["state"] = originalState.ToString();
}
else
{
query.Remove("state");
}


UriBuilder uriBuilder = new UriBuilder(uri);
uriBuilder.Query = query.ToString();
return uriBuilder.Uri;

}
}
Loading

0 comments on commit 6f15b4c

Please sign in to comment.