From ef7da0cc215a16b76053d216eac4eb0d22d2983d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Fri, 12 Jan 2024 10:05:56 +0100 Subject: [PATCH] Add new Windows Forms/Windows Presentation Foundation demos using GitHub authentication --- Directory.Build.props | 2 + Directory.Packages.props | 6 +- OpenIddict.Samples.sln | 19 +++- README.md | 1 + .../Matty/Matty.Client/InteractiveService.cs | 13 ++- .../MainForm.Designer.cs | 62 ++++++++++++ .../Sorgan/Sorgan.WinForms.Client/MainForm.cs | 95 +++++++++++++++++++ .../Sorgan/Sorgan.WinForms.Client/Program.cs | 85 +++++++++++++++++ .../Sorgan.WinForms.Client.csproj | 20 ++++ .../Sorgan/Sorgan.WinForms.Client/Worker.cs | 42 ++++++++ samples/Sorgan/Sorgan.Wpf.Client/App.xaml | 7 ++ samples/Sorgan/Sorgan.Wpf.Client/App.xaml.cs | 7 ++ .../Sorgan/Sorgan.Wpf.Client/MainWindow.xaml | 11 +++ .../Sorgan.Wpf.Client/MainWindow.xaml.cs | 77 +++++++++++++++ samples/Sorgan/Sorgan.Wpf.Client/Program.cs | 89 +++++++++++++++++ .../Sorgan.Wpf.Client.csproj | 21 ++++ samples/Sorgan/Sorgan.Wpf.Client/Worker.cs | 42 ++++++++ 17 files changed, 593 insertions(+), 6 deletions(-) create mode 100644 samples/Sorgan/Sorgan.WinForms.Client/MainForm.Designer.cs create mode 100644 samples/Sorgan/Sorgan.WinForms.Client/MainForm.cs create mode 100644 samples/Sorgan/Sorgan.WinForms.Client/Program.cs create mode 100644 samples/Sorgan/Sorgan.WinForms.Client/Sorgan.WinForms.Client.csproj create mode 100644 samples/Sorgan/Sorgan.WinForms.Client/Worker.cs create mode 100644 samples/Sorgan/Sorgan.Wpf.Client/App.xaml create mode 100644 samples/Sorgan/Sorgan.Wpf.Client/App.xaml.cs create mode 100644 samples/Sorgan/Sorgan.Wpf.Client/MainWindow.xaml create mode 100644 samples/Sorgan/Sorgan.Wpf.Client/MainWindow.xaml.cs create mode 100644 samples/Sorgan/Sorgan.Wpf.Client/Program.cs create mode 100644 samples/Sorgan/Sorgan.Wpf.Client/Sorgan.Wpf.Client.csproj create mode 100644 samples/Sorgan/Sorgan.Wpf.Client/Worker.cs diff --git a/Directory.Build.props b/Directory.Build.props index b42982c06..292d40e3b 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -8,8 +8,10 @@ $(NoWarn);CS1591;NU5128 NU1901;NU1902;NU1903;NU1904 true + false true false + true diff --git a/Directory.Packages.props b/Directory.Packages.props index 7a2157c85..e1527dbb5 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -64,6 +64,9 @@ + + + @@ -77,13 +80,14 @@ - + + diff --git a/OpenIddict.Samples.sln b/OpenIddict.Samples.sln index e59449705..1143fee76 100644 --- a/OpenIddict.Samples.sln +++ b/OpenIddict.Samples.sln @@ -126,7 +126,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mortis.Server", "samples\Mo EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mortis.Client", "samples\Mortis\Mortis.Client\Mortis.Client.csproj", "{561DF817-8F4F-477A-AD66-DA971E99A666}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Zirku.Client2", "samples\Zirku\Zirku.Client2\Zirku.Client2.csproj", "{25D6FF23-937F-4E73-8237-4E805ACDB6C9}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Zirku.Client2", "samples\Zirku\Zirku.Client2\Zirku.Client2.csproj", "{25D6FF23-937F-4E73-8237-4E805ACDB6C9}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Sorgan", "Sorgan", "{F2076FDE-06F9-441B-938E-97953A3C0906}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sorgan.WinForms.Client", "samples\Sorgan\Sorgan.WinForms.Client\Sorgan.WinForms.Client.csproj", "{6E1B3224-B529-4B45-AD66-969BBBA08F63}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sorgan.Wpf.Client", "samples\Sorgan\Sorgan.Wpf.Client\Sorgan.Wpf.Client.csproj", "{5132ABBD-6FC5-4232-B9E1-7F53EC52C826}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -270,6 +276,14 @@ Global {25D6FF23-937F-4E73-8237-4E805ACDB6C9}.Debug|Any CPU.Build.0 = Debug|Any CPU {25D6FF23-937F-4E73-8237-4E805ACDB6C9}.Release|Any CPU.ActiveCfg = Release|Any CPU {25D6FF23-937F-4E73-8237-4E805ACDB6C9}.Release|Any CPU.Build.0 = Release|Any CPU + {6E1B3224-B529-4B45-AD66-969BBBA08F63}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6E1B3224-B529-4B45-AD66-969BBBA08F63}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6E1B3224-B529-4B45-AD66-969BBBA08F63}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6E1B3224-B529-4B45-AD66-969BBBA08F63}.Release|Any CPU.Build.0 = Release|Any CPU + {5132ABBD-6FC5-4232-B9E1-7F53EC52C826}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5132ABBD-6FC5-4232-B9E1-7F53EC52C826}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5132ABBD-6FC5-4232-B9E1-7F53EC52C826}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5132ABBD-6FC5-4232-B9E1-7F53EC52C826}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -323,6 +337,9 @@ Global {AEE318EE-4D49-4262-9CBE-6CC36051668E} = {036564F9-3EDF-41A3-B2A0-E6BC959E65BA} {561DF817-8F4F-477A-AD66-DA971E99A666} = {036564F9-3EDF-41A3-B2A0-E6BC959E65BA} {25D6FF23-937F-4E73-8237-4E805ACDB6C9} = {E0ADFFCA-A604-42D1-8F6D-DE888E061188} + {F2076FDE-06F9-441B-938E-97953A3C0906} = {8B467944-153B-4C90-BAB1-8F1B34C3075A} + {6E1B3224-B529-4B45-AD66-969BBBA08F63} = {F2076FDE-06F9-441B-938E-97953A3C0906} + {5132ABBD-6FC5-4232-B9E1-7F53EC52C826} = {F2076FDE-06F9-441B-938E-97953A3C0906} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {F3ECDD26-F40D-4AB4-BC48-8DF143F98FAE} diff --git a/README.md b/README.md index 80ee52c28..fc79fff27 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ This repository contains samples demonstrating **how to use [OpenIddict](https:/ - [Imynusoph](samples/Imynusoph): refresh token grant demo, with a .NET console acting as the client. - [Matty](samples/Matty): device authorization flow demo, with a .NET console acting as the client. - [Mimban](samples/Mimban): authorization code flow demo using minimal APIs and GitHub delegation for user authentication, with a .NET console acting as the client. + - [Sorgan](samples/Sorgan): Windows Forms and Windows Presentation Foundation clients using GitHub for user authentication. - [Velusia](samples/Velusia): authorization code flow demo, with an ASP.NET Core application acting as the client. - [Weytta](samples/Weytta): authorization code flow with Integrated Windows Authentication support and a .NET console acting as the client. - [Zirku](samples/Zirku): authorization code flow demo using minimal APIs with 2 hard-coded user identities, a .NET console and a SPA acting as the clients and two API projects using introspection (Api1) and local validation (Api2). diff --git a/samples/Matty/Matty.Client/InteractiveService.cs b/samples/Matty/Matty.Client/InteractiveService.cs index 46a53f0f4..1b0aa39d6 100644 --- a/samples/Matty/Matty.Client/InteractiveService.cs +++ b/samples/Matty/Matty.Client/InteractiveService.cs @@ -42,19 +42,24 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) if (result.VerificationUriComplete is not null) { - AnsiConsole.MarkupLineInterpolated( - $"[yellow]Please visit [link]{result.VerificationUriComplete}[/] and confirm the displayed code is '{result.UserCode}' to complete the authentication demand.[/]"); + AnsiConsole.MarkupLineInterpolated($""" + [yellow]Please visit [link]{result.VerificationUriComplete}[/] and confirm the + displayed code is '{result.UserCode}' to complete the authentication demand.[/] + """); } else { - AnsiConsole.MarkupLineInterpolated( - $"[yellow]Please visit [link]{result.VerificationUri}[/] and enter '{result.UserCode}' to complete the authentication demand.[/]"); + AnsiConsole.MarkupLineInterpolated($""" + [yellow]Please visit [link]{result.VerificationUri}[/] and enter + '{result.UserCode}' to complete the authentication demand.[/] + """); } // Wait for the user to complete the demand on the other device. var principal = (await _service.AuthenticateWithDeviceAsync(new() { + CancellationToken = stoppingToken, DeviceCode = result.DeviceCode, Interval = result.Interval, Timeout = result.ExpiresIn < TimeSpan.FromMinutes(5) ? result.ExpiresIn : TimeSpan.FromMinutes(5) diff --git a/samples/Sorgan/Sorgan.WinForms.Client/MainForm.Designer.cs b/samples/Sorgan/Sorgan.WinForms.Client/MainForm.Designer.cs new file mode 100644 index 000000000..6b6c189b1 --- /dev/null +++ b/samples/Sorgan/Sorgan.WinForms.Client/MainForm.Designer.cs @@ -0,0 +1,62 @@ +using System.Windows.Forms; + +namespace Sorgan.WinForms.Client; + +partial class MainForm +{ + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + LoginButton = new Button(); + SuspendLayout(); + // + // LoginButton + // + LoginButton.Location = new System.Drawing.Point(243, 191); + LoginButton.Margin = new Padding(4, 3, 4, 3); + LoginButton.Name = "LoginButton"; + LoginButton.Size = new System.Drawing.Size(456, 96); + LoginButton.TabIndex = 1; + LoginButton.Text = "Log in using GitHub"; + LoginButton.UseVisualStyleBackColor = true; + LoginButton.Click += LoginButton_Click; + // + // MainForm + // + AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); + AutoScaleMode = AutoScaleMode.Font; + ClientSize = new System.Drawing.Size(933, 519); + Controls.Add(LoginButton); + Margin = new Padding(4, 3, 4, 3); + Name = "MainForm"; + Text = "OpenIddict Sorgan WinForms client"; + ResumeLayout(false); + } + + #endregion + + private Button LoginButton; +} \ No newline at end of file diff --git a/samples/Sorgan/Sorgan.WinForms.Client/MainForm.cs b/samples/Sorgan/Sorgan.WinForms.Client/MainForm.cs new file mode 100644 index 000000000..e6ef91b70 --- /dev/null +++ b/samples/Sorgan/Sorgan.WinForms.Client/MainForm.cs @@ -0,0 +1,95 @@ +using System; +using System.Threading; +using System.Windows.Forms; +using Dapplo.Microsoft.Extensions.Hosting.WinForms; +using OpenIddict.Client; +using static OpenIddict.Abstractions.OpenIddictConstants; +using static OpenIddict.Abstractions.OpenIddictExceptions; + +namespace Sorgan.WinForms.Client; + +public partial class MainForm : Form, IWinFormsShell +{ + private readonly OpenIddictClientService _service; + + public MainForm(OpenIddictClientService service) + { + _service = service ?? throw new ArgumentNullException(nameof(service)); + + InitializeComponent(); + } + + private async void LoginButton_Click(object sender, EventArgs e) + { + // Disable the login button to prevent concurrent authentication operations. + LoginButton.Enabled = false; + + try + { + using var 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() + { + CancellationToken = source.Token + }); + + // Wait for the user to complete the authorization process. + var principal = (await _service.AuthenticateInteractivelyAsync(new() + { + CancellationToken = source.Token, + Nonce = result.Nonce + })).Principal; + + TaskDialog.ShowDialog(new TaskDialogPage + { + Caption = "Authentication successful", + Heading = "Authentication successful", + Icon = TaskDialogIcon.ShieldSuccessGreenBar, + Text = $"Welcome, {principal.FindFirst(Claims.Name)!.Value}." + }); + } + + catch (OperationCanceledException) + { + TaskDialog.ShowDialog(new TaskDialogPage + { + Caption = "Authentication timed out", + Heading = "Authentication timed out", + Icon = TaskDialogIcon.Warning, + Text = "The authentication process was aborted." + }); + } + + catch (ProtocolException exception) when (exception.Error is Errors.AccessDenied) + { + TaskDialog.ShowDialog(new TaskDialogPage + { + Caption = "Authorization denied", + Heading = "Authorization denied", + Icon = TaskDialogIcon.Warning, + Text = "The authorization was denied by the end user." + }); + } + + catch + { + TaskDialog.ShowDialog(new TaskDialogPage + { + Caption = "Authentication failed", + Heading = "Authentication failed", + Icon = TaskDialogIcon.Error, + Text = "An error occurred while trying to authenticate the user." + }); + } + } + + finally + { + // Re-enable the login button to allow starting a new authentication operation. + LoginButton.Enabled = true; + } + } +} \ No newline at end of file diff --git a/samples/Sorgan/Sorgan.WinForms.Client/Program.cs b/samples/Sorgan/Sorgan.WinForms.Client/Program.cs new file mode 100644 index 000000000..15c32e71b --- /dev/null +++ b/samples/Sorgan/Sorgan.WinForms.Client/Program.cs @@ -0,0 +1,85 @@ +using System.IO; +using Dapplo.Microsoft.Extensions.Hosting.WinForms; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Sorgan.WinForms.Client; + +var host = new HostBuilder() + // Note: applications for which a single instance is preferred can reference + // the Dapplo.Microsoft.Extensions.Hosting.AppServices package and call this + // method to automatically close extra instances based on the specified identifier: + // + // .ConfigureSingleInstance(options => options.MutexId = "{7113F751-8CD1-42D8-B294-E5F360497577}") + // + .ConfigureLogging(options => options.AddDebug()) + .ConfigureServices(services => + { + services.AddDbContext(options => + { + options.UseSqlite($"Filename={Path.Combine(Path.GetTempPath(), "openiddict-sorgan-winforms-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(); + + // Register the signing and encryption credentials used to protect + // sensitive data like the state tokens produced by OpenIddict. + options.AddDevelopmentEncryptionCertificate() + .AddDevelopmentSigningCertificate(); + + // Add 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(Program).Assembly); + + // 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() + .AddGitHub(options => + { + options.SetClientId("fa9321227d63cda3f341") + // Note: GitHub doesn't allow creating public clients and requires using a client secret. + .SetClientSecret("d904b9b9ededc39da499b2ea4c13df5c7e35ddbe") + // Note: GitHub doesn't support the recommended ":/" syntax and requires using "://". + .SetRedirectUri("com.openiddict.sorgan.winforms.client://callback/login/github"); + }); + }); + + // Register the worker responsible for creating the database used to store tokens + // and adding the registry entries required to register the custom URI scheme. + // + // Note: in a real world application, this step should be part of a setup script. + services.AddHostedService(); + }) + .ConfigureWinForms() + .UseWinFormsLifetime() + .Build(); + +await host.RunAsync(); \ No newline at end of file diff --git a/samples/Sorgan/Sorgan.WinForms.Client/Sorgan.WinForms.Client.csproj b/samples/Sorgan/Sorgan.WinForms.Client/Sorgan.WinForms.Client.csproj new file mode 100644 index 000000000..42ca575eb --- /dev/null +++ b/samples/Sorgan/Sorgan.WinForms.Client/Sorgan.WinForms.Client.csproj @@ -0,0 +1,20 @@ + + + + WinExe + net8.0-windows7.0 + true + true + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/Sorgan/Sorgan.WinForms.Client/Worker.cs b/samples/Sorgan/Sorgan.WinForms.Client/Worker.cs new file mode 100644 index 000000000..99e4c055d --- /dev/null +++ b/samples/Sorgan/Sorgan.WinForms.Client/Worker.cs @@ -0,0 +1,42 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Win32; + +namespace Sorgan.WinForms.Client; + +public class Worker : IHostedService +{ + private readonly IServiceProvider _provider; + + public Worker(IServiceProvider provider) + => _provider = provider; + + public async Task StartAsync(CancellationToken cancellationToken) + { + using var scope = _provider.CreateScope(); + + var context = scope.ServiceProvider.GetRequiredService(); + await context.Database.EnsureCreatedAsync(cancellationToken); + + // Create the registry entries necessary to handle URI protocol activations. + // + // Note: this sample creates the entry under the current user account (as it doesn't + // require administrator rights), but the registration can also be added globally + // in HKEY_CLASSES_ROOT (in this case, it should be added by a dedicated installer). + // + // Alternatively, the application can be packaged and use windows.protocol to + // register the protocol handler/custom URI scheme with the operation system. + using var root = Registry.CurrentUser.CreateSubKey("SOFTWARE\\Classes\\com.openiddict.sorgan.winforms.client"); + root.SetValue(string.Empty, "URL:com.openiddict.sorgan.winforms.client"); + root.SetValue("URL Protocol", string.Empty); + + using var command = root.CreateSubKey("shell\\open\\command"); + command.SetValue(string.Empty, string.Format("\"{0}\" \"%1\"", Environment.ProcessPath)); + } + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; +} diff --git a/samples/Sorgan/Sorgan.Wpf.Client/App.xaml b/samples/Sorgan/Sorgan.Wpf.Client/App.xaml new file mode 100644 index 000000000..c9d81efc5 --- /dev/null +++ b/samples/Sorgan/Sorgan.Wpf.Client/App.xaml @@ -0,0 +1,7 @@ + + + + + diff --git a/samples/Sorgan/Sorgan.Wpf.Client/App.xaml.cs b/samples/Sorgan/Sorgan.Wpf.Client/App.xaml.cs new file mode 100644 index 000000000..ca4cd10c9 --- /dev/null +++ b/samples/Sorgan/Sorgan.Wpf.Client/App.xaml.cs @@ -0,0 +1,7 @@ +using System.Windows; + +namespace Sorgan.Wpf.Client; + +public partial class App : Application +{ +} diff --git a/samples/Sorgan/Sorgan.Wpf.Client/MainWindow.xaml b/samples/Sorgan/Sorgan.Wpf.Client/MainWindow.xaml new file mode 100644 index 000000000..9fdd52bcf --- /dev/null +++ b/samples/Sorgan/Sorgan.Wpf.Client/MainWindow.xaml @@ -0,0 +1,11 @@ + + +