Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change Windows implementation with our own solution #287

Merged
merged 15 commits into from
Oct 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ jobs:
nuget pack nuget/Auth0.OidcClient.AndroidX.nuspec
nuget pack nuget/Auth0.OidcClient.Core.nuspec
nuget pack nuget/Auth0.OidcClient.iOS.nuspec
nuget pack nuget/Auth0.OidcClient.MAUI.nuspec
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are moving MAUI packaging to follow the same pattern as we do with other SDKs throuhg a nuspec file to better control platform specific dependencies.

nuget pack nuget/Auth0.OidcClient.UWP.nuspec
nuget pack nuget/Auth0.OidcClient.WinForms.nuspec
nuget pack nuget/Auth0.OidcClient.WPF.nuspec
Expand Down
517 changes: 309 additions & 208 deletions Auth0.OidcClient.All.sln

Large diffs are not rendered by default.

50 changes: 50 additions & 0 deletions nuget/Auth0.OidcClient.MAUI.nuspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?xml version="1.0"?>
<package>
<metadata>
<id>Auth0.OidcClient.MAUI</id>
<version>1.0.0-beta.0</version>
<authors>Auth0</authors>
<owners>Auth0</owners>
<license type="expression">Apache-2.0</license>
<projectUrl>https://github.com/auth0/auth0-oidc-client-net</projectUrl>
<icon>Auth0Icon.png</icon>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<description>Auth0 OIDC Client for MAUI apps</description>
<releaseNotes></releaseNotes>
<copyright>Copyright 2017-2023 Auth0, Inc.</copyright>
<tags>Auth0 OIDC MAUI</tags>
<dependencies>
<group targetFramework="net6.0-android29.0">
<dependency id="Auth0.OidcClient.Core" version="3.4.1" />
<dependency id="IdentityModel.OidcClient" version="5.2.1" />
</group>
<group targetFramework="net6.0-ios13.0">
<dependency id="Auth0.OidcClient.Core" version="3.4.1" />
<dependency id="IdentityModel.OidcClient" version="5.2.1" />
<dependency id="System.Runtime.InteropServices.NFloat.Internal" version="6.0.1" />
</group>
<group targetFramework="net6.0-maccatalyst14.0">
<dependency id="Auth0.OidcClient.Core" version="3.4.1" />
<dependency id="IdentityModel.OidcClient" version="5.2.1" />
<dependency id="System.Runtime.InteropServices.NFloat.Internal" version="6.0.1" />
</group>
<group targetFramework="net6.0-windows10.0.19041">
<dependency id="Auth0.OidcClient.Core" version="3.4.1" />
<dependency id="IdentityModel.OidcClient" version="5.2.1" />
<dependency id="Microsoft.WindowsAppSDK" version="1.2.221209.1" />
</group>
</dependencies>
</metadata>
<files>
<file src="..\src\Auth0.OidcClient.MAUI\bin\Release\net6.0-android\Auth0.OidcClient.dll" target="lib\net6.0-android29.0" />
<file src="..\src\Auth0.OidcClient.MAUI\bin\Release\net6.0-android\Auth0.OidcClient.xml" target="lib\net6.0-android29.0" />
<file src="..\src\Auth0.OidcClient.MAUI\bin\Release\net6.0-ios\Auth0.OidcClient.dll" target="lib\net6.0-ios13.0" />
<file src="..\src\Auth0.OidcClient.MAUI\bin\Release\net6.0-ios\Auth0.OidcClient.xml" target="lib\net6.0-ios13.0" />
<file src="..\src\Auth0.OidcClient.MAUI\bin\Release\net6.0-maccatalyst\Auth0.OidcClient.dll" target="lib\net6.0-maccatalyst14.0" />
<file src="..\src\Auth0.OidcClient.MAUI\bin\Release\net6.0-maccatalyst\Auth0.OidcClient.xml" target="lib\net6.0-maccatalyst14.0" />
<file src="..\src\Auth0.OidcClient.MAUI\bin\Release\net6.0-windows10.0.19041.0\Auth0.OidcClient.dll" target="lib\net6.0-windows10.0.19041" />
<file src="..\src\Auth0.OidcClient.MAUI\bin\Release\net6.0-windows10.0.19041.0\Auth0.OidcClient.xml" target="lib\net6.0-windows10.0.19041" />
<file src="..\src\Auth0.OidcClient.MAUI\bin\Release\net6.0-windows10.0.19041.0\Auth0.OidcClient.MAUI.Platforms.Windows.dll" target="lib\net6.0-windows10.0.19041" />
<file src="..\build\Auth0Icon.png" />
</files>
</package>
59 changes: 59 additions & 0 deletions src/Auth0.OidcClient.MAUI.Platforms.Windows/Activator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using Microsoft.Windows.AppLifecycle;
using Windows.ApplicationModel.Activation;

namespace Auth0.OidcClient.Platforms.Windows
{
public interface IActivator
{
bool RedirectActivationChecked { get; }
bool CheckRedirectionActivation();
}

/// <summary>
/// Activator class used to enable protocol activation check and redirects activation to the correct application instance
/// </summary>
public sealed class Activator : IActivator
{
private readonly IAppInstanceProxy _appInstanceProxy;

public static readonly Activator Default = new Activator(new AppInstanceProxy());

internal Activator(IAppInstanceProxy appInstanceProxy)
{
_appInstanceProxy = appInstanceProxy;
}

/// <summary>
/// Boolean indication the redirect activation was checked
/// </summary>
public bool RedirectActivationChecked { get; internal set; }

/// <summary>
/// Performs a protocol activation check and redirects activation to the correct application instance.
/// </summary>
public bool CheckRedirectionActivation()
{
var activatedEventArgs = _appInstanceProxy.GetCurrentActivatedEventArgs();

RedirectActivationChecked = true;

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

var ctx = RedirectionContextManager.GetRedirectionContext(protocolArgs);

if (ctx is not null && ctx.AppInstanceKey is not null && ctx.TaskId is not null)
{
return _appInstanceProxy.RedirectActivationToAsync(ctx.AppInstanceKey, activatedEventArgs);
}
else
{
_appInstanceProxy.FindOrRegisterForKey();
}
return false;
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Microsoft.Windows.AppLifecycle;

namespace Auth0.OidcClient.Platforms.Windows;

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; }
}
100 changes: 100 additions & 0 deletions src/Auth0.OidcClient.MAUI.Platforms.Windows/AppInstanceProxy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.Windows.AppLifecycle;

namespace Auth0.OidcClient.Platforms.Windows;

internal interface IAppInstanceProxy
{
event EventHandler<IAppActivationArguments> Activated;
string GetCurrentAppKey();
Microsoft.Windows.AppLifecycle.AppActivationArguments GetCurrentActivatedEventArgs();

bool RedirectActivationToAsync(string key,
Microsoft.Windows.AppLifecycle.AppActivationArguments activatedEventArgs);

void FindOrRegisterForKey();
}

/// <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)

Check warning on line 31 in src/Auth0.OidcClient.MAUI.Platforms.Windows/AppInstanceProxy.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
{
Activated?.Invoke(this, new AppActivationArguments
{
Kind = e.Kind,
Data = e.Data
});
}

/// <summary>
/// Get the current application key.
/// </summary>
/// <remarks>
/// Proxy call to AppInstance.GetCurrent().Key.
/// Used because AppInstance is complicated to use in tests.
/// </remarks>
/// <returns>The key for the current application.</returns>
public virtual string GetCurrentAppKey()
{
return AppInstance.GetCurrent().Key;
}

/// <summary>
/// Get the current application <see cref="AppActivationArguments"/>
/// </summary>
/// <remarks>
/// Proxy call to AppInstance.GetCurrent().GetActivatedEventArgs().
/// Used because AppInstance is complicated to use in tests.
/// </remarks>
/// <returns>Null if no current application instance is found, or the corresponding <see cref="AppActivationArguments"/>.</returns>
public virtual Microsoft.Windows.AppLifecycle.AppActivationArguments GetCurrentActivatedEventArgs()
{
return AppInstance.GetCurrent()?.GetActivatedEventArgs();
}

/// <summary>
/// Redirect the activation to the correct application instance and kill the current process.
/// </summary>
/// <param name="key">Key of the application to activated</param>
/// <param name="activatedEventArgs"><see cref="AppActivationArguments"/> to pass to the application.</param>
/// <returns>Boolean indicating an application instance was activated.</returns>
public virtual bool RedirectActivationToAsync(string key, Microsoft.Windows.AppLifecycle.AppActivationArguments activatedEventArgs)
{
var instance = AppInstance.GetInstances().FirstOrDefault(i => i.Key == key);

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

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

return true;
}

return false;
}

/// <summary>
/// Registers the current application using a new key.
/// </summary>
public virtual void FindOrRegisterForKey()
{
var instance = AppInstance.GetCurrent();

if (string.IsNullOrEmpty(instance.Key))
{
AppInstance.FindOrRegisterForKey(Guid.NewGuid().ToString());
}
}
}
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>
101 changes: 101 additions & 0 deletions src/Auth0.OidcClient.MAUI.Platforms.Windows/Helpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
using System.Runtime.InteropServices;
using System.Text;
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);

/// <summary>
/// Helper property to verify the application is packaged.
/// </summary>
/// <remarks>
/// Original source: https://github.com/dotMorten/WinUIEx
/// </remarks>
/// <returns>A boolean indicate whether or not the app is packaged.</returns>
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 StringBuilder(0);
int result = GetCurrentPackageFullName(ref length, sb);
return result != AppModelErrorNoPackage;
}
catch
{
return false;
}
}
}

/// <summary>
/// Helper method to verify the scheme is defined as a protocol in the AppxManifest.xml files
/// </summary>
/// <remarks>
/// Original source: https://github.com/dotMorten/WinUIEx
/// </remarks>
/// <param name="scheme">The scheme expected to be declared.</param>
/// <returns>A boolean indicate whether or not the scheme is declared as an Uri protocol.</returns>
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();
}

/// <summary>
/// Helper method to open the browser through the url.dll.
/// </summary>
/// <param name="uri">The Uri to open</param>
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();
}
Comment on lines +80 to +87
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before going to GA, we will want to have a decent security review on this. It works as expected, and is also used in other packages, but does look a bit funky.

If this turns out to be an issue before we go GA, we can swap for an embedded webview implementation.


public static string Encode(string value)
{
var bytes = Encoding.UTF8.GetBytes(value);
return Convert.ToBase64String(bytes);
}

public static string Decode(string value)
{
var bytes = Convert.FromBase64String(value);
return Encoding.UTF8.GetString(bytes);
}
}
}
Loading
Loading