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
+
+
+
+
+
+
+
+