diff --git a/Directory.Packages.props b/Directory.Packages.props index e1527dbb..3e21ad6e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -73,6 +73,7 @@ + diff --git a/OpenIddict.Samples.sln b/OpenIddict.Samples.sln index 1143fee7..5f941c0c 100644 --- a/OpenIddict.Samples.sln +++ b/OpenIddict.Samples.sln @@ -134,6 +134,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sorgan.WinForms.Client", "s 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 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sorgan.BlazorHybrid.Client", "samples\Sorgan\Sorgan.BlazorHybrid.Client\Sorgan.BlazorHybrid.Client.csproj", "{C392496F-B3E4-4B7C-97F3-66EB13206985}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -284,6 +286,10 @@ Global {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 + {C392496F-B3E4-4B7C-97F3-66EB13206985}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C392496F-B3E4-4B7C-97F3-66EB13206985}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C392496F-B3E4-4B7C-97F3-66EB13206985}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C392496F-B3E4-4B7C-97F3-66EB13206985}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -340,6 +346,7 @@ Global {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} + {C392496F-B3E4-4B7C-97F3-66EB13206985} = {F2076FDE-06F9-441B-938E-97953A3C0906} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {F3ECDD26-F40D-4AB4-BC48-8DF143F98FAE} diff --git a/README.md b/README.md index fc79fff2..991221c8 100644 --- a/README.md +++ b/README.md @@ -12,7 +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. + - [Sorgan](samples/Sorgan): Windows Forms, Windows Presentation Foundation and Blazor Hybrid 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/Sorgan/Sorgan.BlazorHybrid.Client/App.xaml b/samples/Sorgan/Sorgan.BlazorHybrid.Client/App.xaml new file mode 100644 index 00000000..2e1bf00d --- /dev/null +++ b/samples/Sorgan/Sorgan.BlazorHybrid.Client/App.xaml @@ -0,0 +1,6 @@ + + diff --git a/samples/Sorgan/Sorgan.BlazorHybrid.Client/App.xaml.cs b/samples/Sorgan/Sorgan.BlazorHybrid.Client/App.xaml.cs new file mode 100644 index 00000000..6af804ec --- /dev/null +++ b/samples/Sorgan/Sorgan.BlazorHybrid.Client/App.xaml.cs @@ -0,0 +1,7 @@ +using System.Windows; + +namespace Sorgan.BlazorHybrid.Client; + +public partial class App : Application +{ +} diff --git a/samples/Sorgan/Sorgan.BlazorHybrid.Client/Login.razor b/samples/Sorgan/Sorgan.BlazorHybrid.Client/Login.razor new file mode 100644 index 00000000..51644aaf --- /dev/null +++ b/samples/Sorgan/Sorgan.BlazorHybrid.Client/Login.razor @@ -0,0 +1,70 @@ +@using OpenIddict.Client; +@using System.Security.Claims; +@using System.Threading +@using System.Windows +@using static OpenIddict.Abstractions.OpenIddictExceptions +@using static OpenIddict.Abstractions.OpenIddictConstants +@inject OpenIddictClientService service; + +
+ +
+ +@code +{ + private bool IsButtonDisabled; + + public async Task LoginAsync() + { + // Disable the login button to prevent concurrent authentication operations. + IsButtonDisabled = true; + + 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; + + MessageBox.Show($"Welcome, {principal.FindFirst(ClaimTypes.Name)!.Value}.", + "Authentication successful", MessageBoxButton.OK, MessageBoxImage.Information); + } + + catch (OperationCanceledException) + { + MessageBox.Show("The authentication process was aborted.", + "Authentication timed out", MessageBoxButton.OK, MessageBoxImage.Warning); + } + + catch (ProtocolException exception) when (exception.Error is Errors.AccessDenied) + { + MessageBox.Show("The authorization was denied by the end user.", + "Authorization denied", MessageBoxButton.OK, MessageBoxImage.Warning); + } + + catch + { + MessageBox.Show("An error occurred while trying to authenticate the user.", + "Authentication failed", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + + finally + { + // Re-enable the login button to allow starting a new authentication operation. + IsButtonDisabled = false; + } + } +} \ No newline at end of file diff --git a/samples/Sorgan/Sorgan.BlazorHybrid.Client/MainWindow.xaml b/samples/Sorgan/Sorgan.BlazorHybrid.Client/MainWindow.xaml new file mode 100644 index 00000000..ee0d1118 --- /dev/null +++ b/samples/Sorgan/Sorgan.BlazorHybrid.Client/MainWindow.xaml @@ -0,0 +1,17 @@ + + + + + + + + + diff --git a/samples/Sorgan/Sorgan.BlazorHybrid.Client/MainWindow.xaml.cs b/samples/Sorgan/Sorgan.BlazorHybrid.Client/MainWindow.xaml.cs new file mode 100644 index 00000000..a2159720 --- /dev/null +++ b/samples/Sorgan/Sorgan.BlazorHybrid.Client/MainWindow.xaml.cs @@ -0,0 +1,15 @@ +using System; +using System.Windows; +using Dapplo.Microsoft.Extensions.Hosting.Wpf; + +namespace Sorgan.BlazorHybrid.Client; + +public partial class MainWindow : Window, IWpfShell +{ + public MainWindow(IServiceProvider provider) + { + InitializeComponent(); + + Resources.Add("services", provider); + } +} diff --git a/samples/Sorgan/Sorgan.BlazorHybrid.Client/Program.cs b/samples/Sorgan/Sorgan.BlazorHybrid.Client/Program.cs new file mode 100644 index 00000000..4c9ae94a --- /dev/null +++ b/samples/Sorgan/Sorgan.BlazorHybrid.Client/Program.cs @@ -0,0 +1,95 @@ +using System.IO; +using System.Windows; +using Dapplo.Microsoft.Extensions.Hosting.Wpf; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Sorgan.BlazorHybrid.Client; + +[assembly: ThemeInfo(ResourceDictionaryLocation.None, ResourceDictionaryLocation.SourceAssembly)] + +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 = "{6FBAFC6B-528A-4CB7-A99A-B5DDF5812943}") + // + .ConfigureLogging(options => options.AddDebug()) + .ConfigureServices(services => + { + services.AddDbContext(options => + { + options.UseSqlite($"Filename={Path.Combine(Path.GetTempPath(), "openiddict-sorgan-blazorhybrid-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("2388b26eab831adab80d") + // Note: GitHub doesn't allow creating public clients and requires using a client secret. + .SetClientSecret("5115eeb4c840aeaaa19f7be7ea8b13b992dca765") + // Note: GitHub doesn't support the recommended ":/" syntax and requires using "://", but allows + // using a dynamic/random port that will be dynamically chosen by the OpenIddict system integration. + .SetRedirectUri("http://localhost/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(); + + services.AddWpfBlazorWebView(); + }) + .ConfigureWpf(options => + { + options.UseApplication(); + options.UseWindow(); + }) + .UseWpfLifetime() + .Build(); + +await host.RunAsync(); \ No newline at end of file diff --git a/samples/Sorgan/Sorgan.BlazorHybrid.Client/Sorgan.BlazorHybrid.Client.csproj b/samples/Sorgan/Sorgan.BlazorHybrid.Client/Sorgan.BlazorHybrid.Client.csproj new file mode 100644 index 00000000..6af10674 --- /dev/null +++ b/samples/Sorgan/Sorgan.BlazorHybrid.Client/Sorgan.BlazorHybrid.Client.csproj @@ -0,0 +1,23 @@ + + + + WinExe + net8.0-windows + enable + true + Sorgan.BlazorHybrid.Client + false + + + + + + + + + + + + + + diff --git a/samples/Sorgan/Sorgan.BlazorHybrid.Client/Worker.cs b/samples/Sorgan/Sorgan.BlazorHybrid.Client/Worker.cs new file mode 100644 index 00000000..27271823 --- /dev/null +++ b/samples/Sorgan/Sorgan.BlazorHybrid.Client/Worker.cs @@ -0,0 +1,26 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Sorgan.BlazorHybrid.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); + } + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; +} diff --git a/samples/Sorgan/Sorgan.BlazorHybrid.Client/_Imports.razor b/samples/Sorgan/Sorgan.BlazorHybrid.Client/_Imports.razor new file mode 100644 index 00000000..2b9557ce --- /dev/null +++ b/samples/Sorgan/Sorgan.BlazorHybrid.Client/_Imports.razor @@ -0,0 +1 @@ +@using Microsoft.AspNetCore.Components.Web \ No newline at end of file diff --git a/samples/Sorgan/Sorgan.BlazorHybrid.Client/wwwroot/Index.html b/samples/Sorgan/Sorgan.BlazorHybrid.Client/wwwroot/Index.html new file mode 100644 index 00000000..4929d57d --- /dev/null +++ b/samples/Sorgan/Sorgan.BlazorHybrid.Client/wwwroot/Index.html @@ -0,0 +1,25 @@ + + + + + + + OpenIddict Sorgan Blazor Hybrid client + + + + + + + +
Loading...
+ +
+ An unhandled error has occurred. + Reload + 🗙 +
+ + + + diff --git a/samples/Sorgan/Sorgan.BlazorHybrid.Client/wwwroot/css/app.css b/samples/Sorgan/Sorgan.BlazorHybrid.Client/wwwroot/css/app.css new file mode 100644 index 00000000..89c9caa8 --- /dev/null +++ b/samples/Sorgan/Sorgan.BlazorHybrid.Client/wwwroot/css/app.css @@ -0,0 +1,48 @@ +html, body { + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; +} + +h1:focus { + outline: none; +} + +a, .btn-link { + color: #0071c1; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.valid.modified:not([type=checkbox]) { + outline: 1px solid #26b050; +} + +.invalid { + outline: 1px solid red; +} + +.validation-message { + color: red; +} + +#blazor-error-ui { + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; +} + +#blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; +} diff --git a/samples/Sorgan/Sorgan.WinForms.Client/MainForm.cs b/samples/Sorgan/Sorgan.WinForms.Client/MainForm.cs index e6ef91b7..7c1c4a04 100644 --- a/samples/Sorgan/Sorgan.WinForms.Client/MainForm.cs +++ b/samples/Sorgan/Sorgan.WinForms.Client/MainForm.cs @@ -1,4 +1,5 @@ using System; +using System.Security.Claims; using System.Threading; using System.Windows.Forms; using Dapplo.Microsoft.Extensions.Hosting.WinForms; @@ -48,7 +49,7 @@ private async void LoginButton_Click(object sender, EventArgs e) Caption = "Authentication successful", Heading = "Authentication successful", Icon = TaskDialogIcon.ShieldSuccessGreenBar, - Text = $"Welcome, {principal.FindFirst(Claims.Name)!.Value}." + Text = $"Welcome, {principal.FindFirst(ClaimTypes.Name)!.Value}." }); } diff --git a/samples/Sorgan/Sorgan.WinForms.Client/MainForm.resx b/samples/Sorgan/Sorgan.WinForms.Client/MainForm.resx new file mode 100644 index 00000000..1af7de15 --- /dev/null +++ b/samples/Sorgan/Sorgan.WinForms.Client/MainForm.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ 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 index 42ca575e..7faaf275 100644 --- a/samples/Sorgan/Sorgan.WinForms.Client/Sorgan.WinForms.Client.csproj +++ b/samples/Sorgan/Sorgan.WinForms.Client/Sorgan.WinForms.Client.csproj @@ -2,7 +2,7 @@ WinExe - net8.0-windows7.0 + net8.0-windows true true diff --git a/samples/Sorgan/Sorgan.Wpf.Client/App.xaml b/samples/Sorgan/Sorgan.Wpf.Client/App.xaml index c9d81efc..06e1e96f 100644 --- a/samples/Sorgan/Sorgan.Wpf.Client/App.xaml +++ b/samples/Sorgan/Sorgan.Wpf.Client/App.xaml @@ -1,7 +1,4 @@  - - - diff --git a/samples/Sorgan/Sorgan.Wpf.Client/MainWindow.xaml b/samples/Sorgan/Sorgan.Wpf.Client/MainWindow.xaml index 9fdd52bc..abb1c823 100644 --- a/samples/Sorgan/Sorgan.Wpf.Client/MainWindow.xaml +++ b/samples/Sorgan/Sorgan.Wpf.Client/MainWindow.xaml @@ -4,7 +4,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" - Title="OpenIddict WPF client" Height="450" Width="800"> + Title="OpenIddict Sorgan WPF client" Height="450" Width="800">