diff --git a/Directory.Packages.props b/Directory.Packages.props index 876164da2..a656e2fbb 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -363,7 +363,20 @@ - + + + + + + + + + + + + + + + net8.0 + enable + true + false + false + + + + app.manifest + + + + + + + + + + + + diff --git a/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client.Desktop/Program.cs b/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client.Desktop/Program.cs new file mode 100644 index 000000000..87c86ed4d --- /dev/null +++ b/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client.Desktop/Program.cs @@ -0,0 +1,34 @@ +using System; +using Avalonia; +using Avalonia.ReactiveUI; +using Microsoft.Extensions.DependencyInjection; + +namespace OpenIddict.Sandbox.Avalonia.Client.Desktop; + +sealed class Program +{ + // Initialization code. Don't use any Avalonia, third-party APIs or any + // SynchronizationContext-reliant code before AppMain is called: things aren't initialized + // yet and stuff might break. + [STAThread] + public static void Main(string[] args) => + BuildAvaloniaApp() + // custom: registering app url for deep links + .RegisterAppUrl() + .StartWithClassicDesktopLifetime(args); + + // Avalonia configuration, don't remove; also used by visual designer. + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure(() => + { + var services = new ServiceCollection(); + var app = new App(); + app.ConfigureServices(services); + app.Provider = services.BuildServiceProvider(); + return app; + }) + .UsePlatformDetect() + .WithInterFont() + .LogToTrace() + .UseReactiveUI(); +} diff --git a/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client.Desktop/app.manifest b/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client.Desktop/app.manifest new file mode 100644 index 000000000..202dc3aa0 --- /dev/null +++ b/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client.Desktop/app.manifest @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + diff --git a/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client.iOS/AppDelegate.cs b/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client.iOS/AppDelegate.cs new file mode 100644 index 000000000..445ff60ad --- /dev/null +++ b/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client.iOS/AppDelegate.cs @@ -0,0 +1,35 @@ +using Avalonia; +using Avalonia.iOS; +using Avalonia.ReactiveUI; +using Microsoft.Extensions.DependencyInjection; + +namespace OpenIddict.Sandbox.Avalonia.Client.iOS; + +// The UIApplicationDelegate for the application. This class is responsible for launching the +// User Interface of the application, as well as listening (and optionally responding) to +// application events from iOS. +[Register("AppDelegate")] +#pragma warning disable CA1711 // Identifiers should not have incorrect suffix +public partial class AppDelegate : AvaloniaAppDelegate +#pragma warning restore CA1711 // Identifiers should not have incorrect suffix +{ + protected override AppBuilder CreateAppBuilder() + { + return AppBuilder.Configure(() => + { + var services = new ServiceCollection(); + var app = new App(); + app.ConfigureServices(services); + app.Provider = services.BuildServiceProvider(); + return app; + }).UseiOS() + .With(new iOSPlatformOptions { RenderingMode = [iOSRenderingMode.Metal] }) + ; + } + protected override AppBuilder CustomizeAppBuilder(AppBuilder builder) + { + return base.CustomizeAppBuilder(builder) + .WithInterFont() + .UseReactiveUI(); + } +} diff --git a/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client.iOS/Entitlements.plist b/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client.iOS/Entitlements.plist new file mode 100644 index 000000000..0c67376eb --- /dev/null +++ b/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client.iOS/Entitlements.plist @@ -0,0 +1,5 @@ + + + + + diff --git a/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client.iOS/Info.plist b/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client.iOS/Info.plist new file mode 100644 index 000000000..179355bd3 --- /dev/null +++ b/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client.iOS/Info.plist @@ -0,0 +1,57 @@ + + + + + CFBundleDisplayName + OpenIddict.Sandbox.Avalonia.Client + CFBundleIdentifier + companyName.OpenIddict.Sandbox.Avalonia.Client + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1.0 + LSRequiresIPhoneOS + + MinimumOSVersion + 12.0 + UIDeviceFamily + + 1 + 2 + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + CFBundleURLTypes + + + CFBundleURLName + Type d'URL 1 + CFBundleURLSchemes + + com.openiddict.sandbox.avalonia.client + + CFBundleTypeRole + Editor + + + + diff --git a/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client.iOS/Main.cs b/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client.iOS/Main.cs new file mode 100644 index 000000000..e242dbf4e --- /dev/null +++ b/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client.iOS/Main.cs @@ -0,0 +1,14 @@ +using UIKit; + +namespace OpenIddict.Sandbox.Avalonia.Client.iOS; + +public class Application +{ + // This is the main entry point of the application. + static void Main(string[] args) + { + // if you want to use a different Application Delegate class from "AppDelegate" + // you can specify it here. + UIApplication.Main(args, null, typeof(AppDelegate)); + } +} diff --git a/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client.iOS/OpenIddict.Sandbox.Avalonia.Client.iOS.csproj b/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client.iOS/OpenIddict.Sandbox.Avalonia.Client.iOS.csproj new file mode 100644 index 000000000..80b1b13d6 --- /dev/null +++ b/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client.iOS/OpenIddict.Sandbox.Avalonia.Client.iOS.csproj @@ -0,0 +1,24 @@ + + + Exe + net8.0-ios17.5 + false + false + $(NoWarn);CA1416 + + + + + all + True + + + + + + + + + + diff --git a/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client.iOS/Resources/LaunchScreen.xib b/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client.iOS/Resources/LaunchScreen.xib new file mode 100644 index 000000000..177633b85 --- /dev/null +++ b/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client.iOS/Resources/LaunchScreen.xib @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client.sln b/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client.sln new file mode 100644 index 000000000..fd24a7618 --- /dev/null +++ b/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client.sln @@ -0,0 +1,52 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.3.32811.315 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Sandbox.Avalonia.Client", "OpenIddict.Sandbox.Avalonia.Client\OpenIddict.Sandbox.Avalonia.Client.csproj", "{EBFA8512-1EA5-4D8C-B4AC-AB5B48A6D568}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Sandbox.Avalonia.Client.Desktop", "OpenIddict.Sandbox.Avalonia.Client.Desktop\OpenIddict.Sandbox.Avalonia.Client.Desktop.csproj", "{ABC31E74-02FF-46EB-B3B2-4E6AE43B456C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Sandbox.Avalonia.Client.iOS", "OpenIddict.Sandbox.Avalonia.Client.iOS\OpenIddict.Sandbox.Avalonia.Client.iOS.csproj", "{EBD9022F-BC83-4846-9A11-6F7F3772DC64}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Sandbox.Avalonia.Client.Android", "OpenIddict.Sandbox.Avalonia.Client.Android\OpenIddict.Sandbox.Avalonia.Client.Android.csproj", "{7AD1DAC8-7FBE-49D5-8614-7321233DB82E}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{3DA99C4E-89E3-4049-9C22-0A7EC60D83D8}" + ProjectSection(SolutionItems) = preProject + Directory.Build.props = Directory.Build.props + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {EBFA8512-1EA5-4D8C-B4AC-AB5B48A6D568}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EBFA8512-1EA5-4D8C-B4AC-AB5B48A6D568}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EBFA8512-1EA5-4D8C-B4AC-AB5B48A6D568}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EBFA8512-1EA5-4D8C-B4AC-AB5B48A6D568}.Release|Any CPU.Build.0 = Release|Any CPU + {ABC31E74-02FF-46EB-B3B2-4E6AE43B456C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ABC31E74-02FF-46EB-B3B2-4E6AE43B456C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ABC31E74-02FF-46EB-B3B2-4E6AE43B456C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ABC31E74-02FF-46EB-B3B2-4E6AE43B456C}.Release|Any CPU.Build.0 = Release|Any CPU + {1C1A049E-235C-4CD0-B6FA-D53AC418F4DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1C1A049E-235C-4CD0-B6FA-D53AC418F4DA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1C1A049E-235C-4CD0-B6FA-D53AC418F4DA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1C1A049E-235C-4CD0-B6FA-D53AC418F4DA}.Release|Any CPU.Build.0 = Release|Any CPU + {EBD9022F-BC83-4846-9A11-6F7F3772DC64}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EBD9022F-BC83-4846-9A11-6F7F3772DC64}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EBD9022F-BC83-4846-9A11-6F7F3772DC64}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EBD9022F-BC83-4846-9A11-6F7F3772DC64}.Release|Any CPU.Build.0 = Release|Any CPU + {7AD1DAC8-7FBE-49D5-8614-7321233DB82E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7AD1DAC8-7FBE-49D5-8614-7321233DB82E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7AD1DAC8-7FBE-49D5-8614-7321233DB82E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7AD1DAC8-7FBE-49D5-8614-7321233DB82E}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {83CB65B8-011F-4ED7-BCD3-A6CFA935EF7E} + EndGlobalSection +EndGlobal diff --git a/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client/App.axaml b/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client/App.axaml new file mode 100644 index 000000000..c8bfd8b9f --- /dev/null +++ b/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client/App.axaml @@ -0,0 +1,15 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client/App.axaml.cs b/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client/App.axaml.cs new file mode 100644 index 000000000..64998181c --- /dev/null +++ b/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client/App.axaml.cs @@ -0,0 +1,64 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; +using Microsoft.Extensions.DependencyInjection; +using OpenIddict.Sandbox.Avalonia.Client.OpenId; +using OpenIddict.Sandbox.Avalonia.Client.ViewModels; +using OpenIddict.Sandbox.Avalonia.Client.Views; + +namespace OpenIddict.Sandbox.Avalonia.Client; + +public partial class App : Application +{ + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public IServiceProvider? Provider { get; set; } + + public void ConfigureServices(IServiceCollection services) + { + services.AddAuth(); + + + services.AddTransient(); + } + + public override void OnFrameworkInitializationCompleted() + { + var provider = Provider; + + if(provider is null) + throw new InvalidOperationException("DI initialization failed - provider is null"); + + using var s = provider.CreateScope(); + + // emulate maui behavior: + provider.InitializeMauiInitializeServices(); + + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + var window = new MainWindow(); + window.DataContext = provider.GetRequiredService(); + desktop.MainWindow = window; + + // emulate MAUI behavior + provider.InitializeMauiInitializeScopedService(); + } + else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform) + { + var window = new MainView(); + window.DataContext = provider.GetRequiredService(); + singleViewPlatform.MainView = window; + + // emulate MAUI behavior + provider.InitializeMauiInitializeScopedService(); + } + + + + base.OnFrameworkInitializationCompleted(); + } + +} diff --git a/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client/Assets/avalonia-logo.ico b/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client/Assets/avalonia-logo.ico new file mode 100644 index 000000000..da8d49ff9 Binary files /dev/null and b/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client/Assets/avalonia-logo.ico differ diff --git a/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client/OpenId/AvaloniaSetup.cs b/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client/OpenId/AvaloniaSetup.cs new file mode 100644 index 000000000..71968981a --- /dev/null +++ b/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client/OpenId/AvaloniaSetup.cs @@ -0,0 +1,158 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting.Internal; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Debug; +using Microsoft.IdentityModel.Tokens; +using Microsoft.Maui.Hosting; +using OpenIddict.Client; +using OpenIddict.Client.SystemIntegration; +using OpenIddict.Sandbox.Maui.Client; +using System.Security.Cryptography; +using static OpenIddict.Abstractions.OpenIddictConstants; + +namespace OpenIddict.Sandbox.Avalonia.Client.OpenId; + +internal static class AvaloniaSetup +{ + public static IServiceCollection AddAuth(this IServiceCollection services) + { + // add logging (copied from MAUI source) + services.AddLogging(); + services.TryAddEnumerable(ServiceDescriptor.Singleton()); + + services.AddDbContext(options => + { + options.UseSqlite($"Filename={Path.Combine(Path.GetTempPath(), "openiddict-sandbox-avalonia-client.sqlite3")}"); + options.UseOpenIddict(); + }); + + services.AddOpenIddict() + + // Register the OpenIddict core components. + .AddCore(options => + { + // Configure OpenIddict to use the Entity Framework Core stores and models. + // Note: call ReplaceDefaultEntities() to replace the default OpenIddict entities. + options.UseEntityFrameworkCore() + .UseDbContext(); + }) + + // Register the OpenIddict client components. + .AddClient(options => + { + // Note: this sample uses the authorization code and refresh token + // flows, but you can enable the other flows if necessary. + options.AllowAuthorizationCodeFlow() + .AllowRefreshTokenFlow(); + + options.AddCertificatesForMobileApps(); + + // Register the operating system integration. + options.UseSystemIntegration(); + + // Register the System.Net.Http integration and use the identity of the current + // assembly as a more specific user agent, which can be useful when dealing with + // providers that use the user agent as a way to throttle requests (e.g Reddit). + options.UseSystemNetHttp() + .SetProductInformation(typeof(App).Assembly) +#if IOS + // Warning: server certificate validation is disabled to simplify testing the MAUI + // application with the iOS simulator: in production, it SHOULD NEVER be disabled. + .ConfigureHttpClientHandler("Local", handler => handler.ServerCertificateCustomValidationCallback = + HttpClientHandler.DangerousAcceptAnyServerCertificateValidator) +#endif + ; + + // Add a client registration matching the client application definition in the server project. + options.AddRegistration(new OpenIddictClientRegistration + { + Issuer = new Uri("https://vsr1d2md-44395.euw.devtunnels.ms/", UriKind.Absolute), + ProviderName = "Local", + + ClientId = "avalonia", + + // This sample uses protocol activations with a custom URI scheme to handle callbacks. + // + // For more information on how to construct private-use URI schemes, + // read https://www.rfc-editor.org/rfc/rfc8252#section-7.1 and + // https://www.rfc-editor.org/rfc/rfc7595#section-3.8. + PostLogoutRedirectUri = new Uri("com.openiddict.sandbox.avalonia.client:/callback/logout/local", UriKind.Absolute), + RedirectUri = new Uri("com.openiddict.sandbox.avalonia.client:/callback/login/local", UriKind.Absolute), + + Scopes = { Scopes.Email, Scopes.Profile, Scopes.OfflineAccess, "demo_api" } + }); + + // Register the Web providers integrations. + // + // Note: to mitigate mix-up attacks, it's recommended to use a unique redirection endpoint + // address per provider, unless all the registered providers support returning an "iss" + // parameter containing their URL as part of authorization responses. For more information, + // see https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.4. + options.UseWebProviders() + .AddTwitter(options => + { + options.SetClientId("bXgwc0U3N3A3YWNuaWVsdlRmRWE6MTpjaQ") + // Note: Twitter doesn't support the recommended ":/" syntax and requires using "://". + .SetRedirectUri("com.openiddict.sandbox.avalonia.client://callback/login/twitter"); + }); + }); + + // Note: MAUI is not built on top of the .NET Generic Host and doesn't register any of + // the services typically found in applications using the .NET Generic Host. Since these + // services are required by the OpenIddict client system integration to handle callbacks + // and redirect protocol activations to the correct instance, custom implementations are + // registered here. For more information, see https://github.com/dotnet/maui/issues/2244. + + services.AddSingleton(new HostingEnvironment + { + ApplicationName = typeof(App).Assembly.GetName().Name! + }); + + services.AddSingleton(); + + services.AddSingleton(static provider => new MauiHostedServiceAdapter( + ActivatorUtilities.CreateInstance(provider))); + + services.AddSingleton(static provider => new MauiHostedServiceAdapter( + ActivatorUtilities.CreateInstance(provider))); + + services.AddSingleton(static provider => new MauiHostedServiceAdapter( + ActivatorUtilities.CreateInstance(provider))); + + + // Register the initialization service responsible for creating the SQLite database. + services.AddScoped(); + + return services; + } + + + /// + /// Note: Do not use the following keys in production! + /// there just added so we can authenticate from Android (Maui/Avalonia), iOS (Maui/Avalonia) and Macos, since on these platforms the following does not work + /// + /// options.AddDevelopmentEncryptionCertificate() + /// .AddDevelopmentSigningCertificate(); + /// + /// this is because + /// 1. Adding an X509 certificate to the system store is not supported (throws on iOS and Android) + /// 2. and even if so, it would be a different machine - therefore different store - and client and server would not use the same certificate! + /// + /// + /// + private static OpenIddictClientBuilder AddCertificatesForMobileApps(this OpenIddictClientBuilder options) + { + var privateKeyXml = "uSQBwbidg8/lAw3N3xeWmc9uYQPMHH5fODGmER6uXRzzJaL8upFWXanwts7ILNFOFAWogxQuWaTqu4dUFDVuXhJsdxpT4YZy0+k8QEMyBi6VIenQtKhYgiCgx9RK6cAuXRN1X6iQ2F+3MaenUGxztEOSQ1iJarV7E5od0o0doDl0TcW/wVqnwpAc5j8K/06kICuy1Pb1glHZsF8vzCgTPwdBTAYLGbzJWWxpLNiEFDuvJR6lopSSxKpurvzYXgpZHMZuOUlmQM/XGXjCYctHldAmr+gp8/xtufx3w2/V3gApLS6kWdkA9xazLOt7Xqb2QBGNGbunVzhtGg2rBYdBXQ==AQAB

wiiY1qCfHaiO+FoVpB3OocUYtqI9WvXUV2tk/JIOVuBth5oRg01GMN1cMA085YcwlV1d2RQVqGXdhAKHUwyi73luFQ/yt5ehemPUQPau03Pv8GkySLSGsbwuK+FKpDQ9kdupG1eW6dBt91um4Q1Gtu+GAJ2LkucYRHA2yx6osIs=

9BwZ5gtnMw70n/h8NvULco5RxxpfoQ++2D7iQ6rc7i27/k53E0is2L03PP/LR8bV/14z+ixMW6rH7G2d475NIzFTrR4HjZdf+i05Fq7N/xvNCLrUvAd0CWqxYrume0t9zfw62JQtp5IYQ3g9K7DxUwfY9qVwYlZByLkgrUz26rc=m2n5pVte4lOpVXxudDbzzqPA+3f0WtoKBYvOgym6VqpAolmeCRcSx0x5XXFLPIMxTW42D+w2xdv8K44GmmC0D7KIfk2MwI6cUCaWoQWUvWfBORRLjs0KQDzcTH2CzNuQKS/GNj+vaitPyr9PXjfNUeN6xQVW0tkuoKGeCorZBq8=HOd26ZZQEeuja42wp5E8WcQgSsMEr719i31mrTx+DHW93M7NqqrgTImbEM348/bHQAWXgffc0r3WDlisaVsPJyugDM+RdWKHKshQCi+IlLxl+rKknd8EDlljx50QiWjW7J0BGsPw4/aYiOSj2ZiJ+prjRdExDXPJNks1Y0/JrOE=g+JNJBZbKFIY5jWZxCeX7TW25KpR498x+0yGJlzCwy23JbBGDupt2tsBnhXr8KuTxSfMOGWtazQeipI//XyLCvV7BohkL6PhzMKKHwAoM/0xNaqA0d5t9Q32OqEn6I+deu4SF4OwMXkQ96xGp0zLlsWnw3HdG2rVtx5KYARMmGA=YA+CqdT0RXQUyyTacKp4hY3PI58oxI/9L9by52cX6VAgCKMsplDKkwad0vwveLGQ5WqaKIjME88xy+NHiMTAYycECDgs1ZNA+RrHHEDBL9vznQkINPQ0GDB9u7E2vVnttHVoLR31KY9gKe9nLJ9Y2WtF9JN3mVpYZa9NUfXOLVc+zs6ChwqfryfrkgQGHZXNFtwYhG4KuOLkrQy2S4etJEWn+NMbJVYEmy1Sg99BZs4eyi0666B30ofUsx6GwyCa9IXgDm4cJnUDQu0ZEGNU7LX+p9lFym13DkWt4z9TuE3QeOSr7jHEQz1CdE8a4zsqdf3TKP2Fl05+URL35kr/MQ==
"; + var rsa = RSA.Create(2048); + rsa.FromXmlString(privateKeyXml); + + options.AddEncryptionKey(new SymmetricSecurityKey(Convert.FromBase64String("DRjd/GnduI3Efzen9V9BvbNUfc/VKgXltV7Kbk9sMkY="))); + options.AddSigningKey(new RsaSecurityKey(rsa)); + + return options; + } + +} \ No newline at end of file diff --git a/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client/OpenId/MauiHostApplicationLifetime.cs b/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client/OpenId/MauiHostApplicationLifetime.cs new file mode 100644 index 000000000..8e6624aad --- /dev/null +++ b/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client/OpenId/MauiHostApplicationLifetime.cs @@ -0,0 +1,24 @@ +//#if IOS || MACCATALYST || WINDOWS +using Microsoft.Extensions.Hosting; +using System; +using System.Threading; + +namespace OpenIddict.Sandbox.Maui.Client; + +public class MauiHostApplicationLifetime : IHostApplicationLifetime +{ + private readonly CancellationTokenSource _source = new(); + + public CancellationToken ApplicationStarted => new(canceled: true); + + public CancellationToken ApplicationStopping => _source.Token; + + public CancellationToken ApplicationStopped => _source.Token; + + public void StopApplication() + { + _source.Cancel(throwOnFirstException: false); + Environment.Exit(0); + } +} +//#endif diff --git a/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client/OpenId/MauiHostedServiceAdapter.cs b/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client/OpenId/MauiHostedServiceAdapter.cs new file mode 100644 index 000000000..66697f41c --- /dev/null +++ b/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client/OpenId/MauiHostedServiceAdapter.cs @@ -0,0 +1,20 @@ +//#if IOS || MACCATALYST || WINDOWS +using Microsoft.Extensions.Hosting; +using Microsoft.Maui.Hosting; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace OpenIddict.Sandbox.Maui.Client; + +public class MauiHostedServiceAdapter : IMauiInitializeService +{ + private readonly IHostedService _service; + + public MauiHostedServiceAdapter(IHostedService service) + => _service = service ?? throw new ArgumentNullException(nameof(service)); + + public void Initialize(IServiceProvider services) + => Task.Run(() => _service.StartAsync(CancellationToken.None)).GetAwaiter().GetResult(); +} +//#endif diff --git a/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client/OpenId/MauiHosting.cs b/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client/OpenId/MauiHosting.cs new file mode 100644 index 000000000..c2c68670c --- /dev/null +++ b/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client/OpenId/MauiHosting.cs @@ -0,0 +1,27 @@ +using System; + +namespace Microsoft.Maui.Hosting +{ + /// + /// Represents a service that is initialized during the application construction. + /// + /// + /// This service is initialized during the MauiAppBuilder.Build() method. It is + /// executed once per application using the root service provider. + /// + public interface IMauiInitializeService + { + void Initialize(IServiceProvider services); + } + /// + /// Represents a service that is initialized during the window construction. + /// + /// + /// This service is initialized during the creation of a window. It is + /// executed once per window using the window-scoped service provider. + /// + public interface IMauiInitializeScopedService + { + void Initialize(IServiceProvider services); + } +} diff --git a/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client/OpenId/ServiceProviderExtensions.cs b/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client/OpenId/ServiceProviderExtensions.cs new file mode 100644 index 000000000..6f18368ff --- /dev/null +++ b/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client/OpenId/ServiceProviderExtensions.cs @@ -0,0 +1,30 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Maui.Hosting; +using System; + +namespace OpenIddict.Sandbox.Avalonia.Client.OpenId +{ + public static class ServiceProviderExtensions + { + public static void InitializeMauiInitializeServices(this IServiceProvider provider) + { + var initServices = provider.GetServices(); + foreach (var service in initServices) + { + service.Initialize(provider); + } + } + + public static void InitializeMauiInitializeScopedService(this IServiceProvider provider) + { + // emulate maui behavior: + using var scope = provider.CreateScope(); + + var initServices = scope.ServiceProvider.GetServices(); + foreach (var service in initServices) + { + service.Initialize(scope.ServiceProvider); + } + } + } +} diff --git a/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client/OpenId/Worker.cs b/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client/OpenId/Worker.cs new file mode 100644 index 000000000..1f8622e11 --- /dev/null +++ b/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client/OpenId/Worker.cs @@ -0,0 +1,15 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Maui.Hosting; +using System; + +namespace OpenIddict.Sandbox.Maui.Client; + +public class Worker : IMauiInitializeScopedService +{ + public void Initialize(IServiceProvider services) + { + var context = services.GetRequiredService(); + context.Database.EnsureCreated(); + } +} diff --git a/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client.csproj b/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client.csproj new file mode 100644 index 000000000..2f7db736d --- /dev/null +++ b/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client.csproj @@ -0,0 +1,34 @@ + + + net8.0 + enable + latest + false + false + false + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client/ViewLocator.cs b/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client/ViewLocator.cs new file mode 100644 index 000000000..23cf8b3ea --- /dev/null +++ b/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client/ViewLocator.cs @@ -0,0 +1,30 @@ +using System; +using Avalonia.Controls; +using Avalonia.Controls.Templates; +using OpenIddict.Sandbox.Avalonia.Client.ViewModels; + +namespace OpenIddict.Sandbox.Avalonia.Client; + +public class ViewLocator : IDataTemplate +{ + public Control? Build(object? data) + { + if (data is null) + return null; + + var name = data.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal); + var type = Type.GetType(name); + + if (type != null) + { + return (Control)Activator.CreateInstance(type)!; + } + + return new TextBlock { Text = "Not Found: " + name }; + } + + public bool Match(object? data) + { + return data is ViewModelBase; + } +} \ No newline at end of file diff --git a/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client/ViewModels/BoolToOpacityConverter.cs b/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client/ViewModels/BoolToOpacityConverter.cs new file mode 100644 index 000000000..43246df08 --- /dev/null +++ b/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client/ViewModels/BoolToOpacityConverter.cs @@ -0,0 +1,22 @@ +using Avalonia.Data.Converters; +using System; +using System.Globalization; + +namespace OpenIddict.Sandbox.Avalonia.Client.ViewModels; + +public class BoolToOpacityConverter : IValueConverter +{ + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if(value is bool b && b) + { + return 1d; + } + return 0.4d; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} diff --git a/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client/ViewModels/MainViewModel.cs b/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client/ViewModels/MainViewModel.cs new file mode 100644 index 000000000..8db96086a --- /dev/null +++ b/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client/ViewModels/MainViewModel.cs @@ -0,0 +1,189 @@ +using Avalonia.Controls; +using OpenIddict.Abstractions; +using OpenIddict.Client; +using ReactiveUI; +using System; +using System.Collections.Generic; +using System.Reactive; +using System.Reactive.Linq; +using System.Threading; +using System.Threading.Tasks; +using static OpenIddict.Abstractions.OpenIddictConstants; +using static OpenIddict.Abstractions.OpenIddictExceptions; + +namespace OpenIddict.Sandbox.Avalonia.Client.ViewModels; + +public class MainViewModel : ViewModelBase +{ + + private OpenIddictClientService _service; + private string _message = string.Empty; + private bool _isEnabled = true; + CancellationTokenSource? _source; + + public MainViewModel(OpenIddictClientService service) + { + _service = service; + + var canExecute = this.WhenAnyValue(v => v.IsEnabled); + + LoginCommand = ReactiveCommand.CreateFromTask(LoginAsync, canExecute); + LoginWithGithubCommand = ReactiveCommand.CreateFromTask(LoginWithGithubAsync, canExecute); + LogoutCommand = ReactiveCommand.CreateFromTask(LogoutAsync, canExecute); + CancelCommand = ReactiveCommand.CreateFromTask(CancelAsync, canExecute.Select(v => !v)); + + Message = "Welcome to Avalonia UI"; + } + + public ReactiveCommand LoginCommand { get; } + public ReactiveCommand LoginWithGithubCommand { get; } + public ReactiveCommand LogoutCommand { get; } + public ReactiveCommand CancelCommand { get; } + + public string Message + { + get { return _message; } + set { this.RaiseAndSetIfChanged(ref _message, value); } + } + + public bool IsEnabled + { + get { return _isEnabled; } + set { this.RaiseAndSetIfChanged(ref _isEnabled, value); } + } + + private async Task LoginAsync() + { + await LogInAsync("Local"); + } + + private async Task LoginWithGithubAsync() + { + await LogInAsync("Local", new() + { + [Parameters.IdentityProvider] = "GitHub" + }); + } + + private async Task LogoutAsync() + { + await LogOutAsync("Local"); + } + + private async Task LogInAsync(string provider, Dictionary? parameters = null) + { + // Disable the buttons to prevent concurrent operations. + IsEnabled = false; + + try + { + _source = new CancellationTokenSource(delay: TimeSpan.FromSeconds(90)); + + try + { + // Ask OpenIddict to initiate the authentication flow (typically, by starting the system browser). + var result = await _service.ChallengeInteractivelyAsync(new() + { + AdditionalAuthorizationRequestParameters = parameters, + CancellationToken = _source.Token, + ProviderName = provider + }); + + // Wait for the user to complete the authorization process. + var principal = (await _service.AuthenticateInteractivelyAsync(new() + { + CancellationToken = _source.Token, + Nonce = result.Nonce + })).Principal; + + Message = $"Welcome, {principal.FindFirst(Claims.Name)!.Value}."; + } + + catch (OperationCanceledException) + { + Message = "The authentication process was aborted."; + } + + catch (ProtocolException exception) when (exception.Error is Errors.AccessDenied) + { + Message = "The authorization was denied by the end user."; + } + + catch + { + Message = "An error occurred while trying to authenticate the user."; + } + } + + finally + { + _source?.Dispose(); + // Re-enable the buttons to allow starting a new operation. + IsEnabled = true; + } + } + + private async Task LogOutAsync(string provider, Dictionary? parameters = null) + { + // Disable the buttons to prevent concurrent operations. + IsEnabled = false; + + try + { + using var source = new CancellationTokenSource(delay: TimeSpan.FromSeconds(90)); + + try + { + // Ask OpenIddict to initiate the logout flow (typically, by starting the system browser). + var result = await _service.SignOutInteractivelyAsync(new() + { + AdditionalLogoutRequestParameters = parameters, + CancellationToken = source.Token, + ProviderName = provider + }); + + // Wait for the user to complete the logout process and authenticate the callback request. + // + // Note: in this case, only the claims contained in the state token can be resolved since + // the authorization server doesn't return any other user identity during a logout dance. + await _service.AuthenticateInteractivelyAsync(new() + { + CancellationToken = source.Token, + Nonce = result.Nonce + }); + + Message = "The user was successfully logged out from the local server."; + } + + catch (OperationCanceledException) + { + Message = "The logout process was aborted."; + } + + catch + { + Message = "An error occurred while trying to log the user out."; + } + } + + finally + { + // Re-enable the buttons to allow starting a new operation. + IsEnabled = true; + } + } + + private Task CancelAsync() + { + + if (IsEnabled) + return Task.CompletedTask; + + if (_source is null) + return Task.CompletedTask; + + _source.Cancel(); + + return Task.CompletedTask; + } +} diff --git a/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client/ViewModels/ViewModelBase.cs b/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client/ViewModels/ViewModelBase.cs new file mode 100644 index 000000000..3db6c9785 --- /dev/null +++ b/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client/ViewModels/ViewModelBase.cs @@ -0,0 +1,7 @@ +using ReactiveUI; + +namespace OpenIddict.Sandbox.Avalonia.Client.ViewModels; + +public class ViewModelBase : ReactiveObject +{ +} diff --git a/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client/Views/MainView.axaml b/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client/Views/MainView.axaml new file mode 100644 index 000000000..e371ee28c --- /dev/null +++ b/sandbox/OpenIddict.Sandbox.Avalonia.Client/OpenIddict.Sandbox.Avalonia.Client/Views/MainView.axaml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + +