58 changed files with 4063 additions and 86 deletions
@ -0,0 +1,82 @@ |
|||
using Microsoft.Extensions.Hosting; |
|||
using OpenIddict.Client; |
|||
using static OpenIddict.Abstractions.OpenIddictConstants; |
|||
using static OpenIddict.Abstractions.OpenIddictExceptions; |
|||
using static System.Console; |
|||
|
|||
namespace OpenIddict.Sandbox.Console.Client; |
|||
|
|||
public class InteractiveService : BackgroundService |
|||
{ |
|||
private readonly IHostApplicationLifetime _lifetime; |
|||
private readonly OpenIddictClientService _service; |
|||
|
|||
public InteractiveService( |
|||
IHostApplicationLifetime lifetime, |
|||
OpenIddictClientService service) |
|||
{ |
|||
_lifetime = lifetime; |
|||
_service = service; |
|||
} |
|||
|
|||
protected override async Task ExecuteAsync(CancellationToken stoppingToken) |
|||
{ |
|||
// Wait for the host to confirm that the application has started.
|
|||
var source = new TaskCompletionSource(); |
|||
using (_lifetime.ApplicationStarted.Register(static state => ((TaskCompletionSource) state!).SetResult(), source)) |
|||
{ |
|||
await source.Task; |
|||
} |
|||
|
|||
string? provider; |
|||
|
|||
while (!stoppingToken.IsCancellationRequested) |
|||
{ |
|||
do |
|||
{ |
|||
await Out.WriteLineAsync("Type '1' + ENTER to log in using the local server or '2' + ENTER to log in using Twitter"); |
|||
|
|||
provider = await In.ReadLineAsync(stoppingToken) switch |
|||
{ |
|||
"1" => "Local", |
|||
"2" => "Twitter", |
|||
_ => null |
|||
}; |
|||
} |
|||
|
|||
while (string.IsNullOrEmpty(provider)); |
|||
|
|||
await Out.WriteLineAsync("Launching the system browser."); |
|||
|
|||
try |
|||
{ |
|||
// Ask OpenIddict to initiate the challenge and launch the system browser
|
|||
// to allow the user to complete the interactive authentication dance.
|
|||
var nonce = await _service.ChallengeWithBrowserAsync( |
|||
provider, cancellationToken: stoppingToken); |
|||
|
|||
// Wait until the user approved or rejected the authorization
|
|||
// demand and retrieve the resulting claims-based principal.
|
|||
var (_, _, principal) = await _service.AuthenticateWithBrowserAsync( |
|||
nonce, cancellationToken: stoppingToken); |
|||
|
|||
await Out.WriteLineAsync($"Welcome, {principal.FindFirst(Claims.Name)!.Value}."); |
|||
} |
|||
|
|||
catch (OperationCanceledException) |
|||
{ |
|||
await Error.WriteLineAsync("The authentication process was aborted."); |
|||
} |
|||
|
|||
catch (ProtocolException exception) when (exception.Error is Errors.AccessDenied) |
|||
{ |
|||
await Error.WriteLineAsync("The authorization was denied by the end user."); |
|||
} |
|||
|
|||
catch |
|||
{ |
|||
await Error.WriteLineAsync("An error occurred while trying to authenticate the user."); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<PropertyGroup> |
|||
<OutputType>Exe</OutputType> |
|||
<TargetFramework>net7.0-windows</TargetFramework> |
|||
<EnablePreviewFeatures>true</EnablePreviewFeatures> |
|||
<IsShipping>false</IsShipping> |
|||
<SignAssembly>false</SignAssembly> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\src\OpenIddict.Client.SystemNetHttp\OpenIddict.Client.SystemNetHttp.csproj" /> |
|||
<ProjectReference Include="..\..\src\OpenIddict.Client.WebIntegration\OpenIddict.Client.WebIntegration.csproj" /> |
|||
<ProjectReference Include="..\..\src\OpenIddict.Client.Windows\OpenIddict.Client.Windows.csproj" /> |
|||
<ProjectReference Include="..\..\src\OpenIddict.EntityFrameworkCore\OpenIddict.EntityFrameworkCore.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Dapplo.Microsoft.Extensions.Hosting.AppServices" /> |
|||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" /> |
|||
<PackageReference Include="Microsoft.Extensions.Hosting" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,99 @@ |
|||
using Microsoft.EntityFrameworkCore; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.DependencyInjection.Extensions; |
|||
using Microsoft.Extensions.Hosting; |
|||
using Microsoft.Extensions.Logging; |
|||
using OpenIddict.Client; |
|||
using OpenIddict.Sandbox.Console.Client; |
|||
using static OpenIddict.Abstractions.OpenIddictConstants; |
|||
|
|||
var host = Host.CreateDefaultBuilder(args) |
|||
// 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 = "{802A478D-00E8-4DAE-9A27-27B31A47CB39}")
|
|||
//
|
|||
.ConfigureServices(services => |
|||
{ |
|||
services.AddDbContext<DbContext>(options => |
|||
{ |
|||
options.UseSqlite($"Filename={Path.Combine(Path.GetTempPath(), "openiddict-sandbox-console-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<DbContext>(); |
|||
}) |
|||
|
|||
// 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(); |
|||
|
|||
// Register the Windows host.
|
|||
options.UseWindows(); |
|||
|
|||
// Set the client URI that will uniquely identify this application.
|
|||
options.SetClientUri(new Uri("openiddict-sandbox-console-client://localhost/", UriKind.Absolute)); |
|||
|
|||
// 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); |
|||
|
|||
// Add a client registration matching the client application definition in the server project.
|
|||
options.AddRegistration(new OpenIddictClientRegistration |
|||
{ |
|||
Issuer = new Uri("https://localhost:44395/", UriKind.Absolute), |
|||
ProviderName = "Local", |
|||
|
|||
ClientId = "console", |
|||
RedirectUri = new Uri("callback/login/local", UriKind.Relative), |
|||
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() |
|||
.UseTwitter() |
|||
.SetClientId("bXgwc0U3N3A3YWNuaWVsdlRmRWE6MTpjaQ") |
|||
.SetClientSecret("VcohOgBp-6yQCurngo4GAyKeZh0D6SUCCSjJgEo1uRzJarjIUS") |
|||
.SetRedirectUri(new Uri("callback/login/twitter", UriKind.Relative)); |
|||
}); |
|||
|
|||
// 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<Worker>(); |
|||
|
|||
// Register the background service responsible for handling the console interactions.
|
|||
services.AddHostedService<InteractiveService>(); |
|||
|
|||
services.RemoveAll<ILoggerProvider>(); |
|||
}) |
|||
.UseConsoleLifetime() |
|||
.Build(); |
|||
|
|||
await host.RunAsync(); |
|||
@ -0,0 +1,54 @@ |
|||
using Microsoft.EntityFrameworkCore; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Hosting; |
|||
using Microsoft.Win32; |
|||
|
|||
namespace OpenIddict.Sandbox.Console.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<DbContext>(); |
|||
await context.Database.EnsureCreatedAsync(); |
|||
|
|||
RegistryKey? root = null; |
|||
|
|||
// Create the registry entries necessary to handle URI protocol activations.
|
|||
// Note: the application MUST be run once as an administrator for this to work.
|
|||
try |
|||
{ |
|||
root = Registry.ClassesRoot.OpenSubKey("openiddict-sandbox-console-client"); |
|||
|
|||
if (root is null) |
|||
{ |
|||
root = Registry.ClassesRoot.CreateSubKey("openiddict-sandbox-console-client"); |
|||
root.SetValue(string.Empty, "URL:openiddict-sandbox-console-client"); |
|||
root.SetValue("URL Protocol", string.Empty); |
|||
|
|||
using var command = root.CreateSubKey("shell\\open\\command"); |
|||
command.SetValue(string.Empty, string.Format("\"{0}\" \"%1\"", |
|||
#if SUPPORTS_ENVIRONMENT_PROCESS_PATH
|
|||
Environment.ProcessPath |
|||
#else
|
|||
Process.GetCurrentProcess().MainModule.FileName |
|||
#endif
|
|||
)); |
|||
} |
|||
} |
|||
|
|||
finally |
|||
{ |
|||
root?.Dispose(); |
|||
} |
|||
} |
|||
|
|||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; |
|||
} |
|||
@ -0,0 +1,73 @@ |
|||
namespace OpenIddict.Sandbox.WinForms.Client |
|||
{ |
|||
partial class MainForm |
|||
{ |
|||
/// <summary>
|
|||
/// Required designer variable.
|
|||
/// </summary>
|
|||
private System.ComponentModel.IContainer components = null; |
|||
|
|||
/// <summary>
|
|||
/// Clean up any resources being used.
|
|||
/// </summary>
|
|||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
|||
protected override void Dispose(bool disposing) |
|||
{ |
|||
if (disposing && (components != null)) |
|||
{ |
|||
components.Dispose(); |
|||
} |
|||
base.Dispose(disposing); |
|||
} |
|||
|
|||
#region Windows Form Designer generated code
|
|||
|
|||
/// <summary>
|
|||
/// Required method for Designer support - do not modify
|
|||
/// the contents of this method with the code editor.
|
|||
/// </summary>
|
|||
private void InitializeComponent() |
|||
{ |
|||
this.LocalLogin = new System.Windows.Forms.Button(); |
|||
this.TwitterLogin = new System.Windows.Forms.Button(); |
|||
this.SuspendLayout(); |
|||
//
|
|||
// LocalLogin
|
|||
//
|
|||
this.LocalLogin.Location = new System.Drawing.Point(258, 93); |
|||
this.LocalLogin.Name = "LocalLogin"; |
|||
this.LocalLogin.Size = new System.Drawing.Size(283, 83); |
|||
this.LocalLogin.TabIndex = 0; |
|||
this.LocalLogin.Text = "Log in using the local server"; |
|||
this.LocalLogin.UseVisualStyleBackColor = true; |
|||
this.LocalLogin.Click += new System.EventHandler(this.LocalLoginButton_Click); |
|||
//
|
|||
// TwitterLogin
|
|||
//
|
|||
this.TwitterLogin.Location = new System.Drawing.Point(258, 258); |
|||
this.TwitterLogin.Name = "TwitterLogin"; |
|||
this.TwitterLogin.Size = new System.Drawing.Size(283, 83); |
|||
this.TwitterLogin.TabIndex = 1; |
|||
this.TwitterLogin.Text = "Log in using Twitter"; |
|||
this.TwitterLogin.UseVisualStyleBackColor = true; |
|||
this.TwitterLogin.Click += new System.EventHandler(this.TwitterLoginButton_Click); |
|||
//
|
|||
// MainForm
|
|||
//
|
|||
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); |
|||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; |
|||
this.ClientSize = new System.Drawing.Size(800, 450); |
|||
this.Controls.Add(this.TwitterLogin); |
|||
this.Controls.Add(this.LocalLogin); |
|||
this.Name = "MainForm"; |
|||
this.Text = "OpenIddict WinForms client"; |
|||
this.ResumeLayout(false); |
|||
|
|||
} |
|||
|
|||
#endregion
|
|||
|
|||
private Button LocalLogin; |
|||
private Button TwitterLogin; |
|||
} |
|||
} |
|||
@ -0,0 +1,105 @@ |
|||
using Dapplo.Microsoft.Extensions.Hosting.WinForms; |
|||
using OpenIddict.Client; |
|||
using static OpenIddict.Abstractions.OpenIddictConstants; |
|||
using static OpenIddict.Abstractions.OpenIddictExceptions; |
|||
using static OpenIddict.Client.WebIntegration.OpenIddictClientWebIntegrationConstants; |
|||
|
|||
namespace OpenIddict.Sandbox.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 LocalLoginButton_Click(object sender, EventArgs e) |
|||
=> await AuthenticateAsync("Local"); |
|||
|
|||
private async void TwitterLoginButton_Click(object sender, EventArgs e) |
|||
=> await AuthenticateAsync(Providers.Twitter); |
|||
|
|||
private async Task AuthenticateAsync(string provider) |
|||
{ |
|||
using var source = new CancellationTokenSource(delay: TimeSpan.FromSeconds(90)); |
|||
|
|||
try |
|||
{ |
|||
// Ask OpenIddict to initiate the challenge and launch the system browser
|
|||
// to allow the user to complete the interactive authentication dance.
|
|||
var nonce = await _service.ChallengeWithBrowserAsync( |
|||
provider, cancellationToken: source.Token); |
|||
|
|||
// Wait until the user approved or rejected the authorization
|
|||
// demand and retrieve the resulting claims-based principal.
|
|||
var (_, _, principal) = await _service.AuthenticateWithBrowserAsync( |
|||
nonce, cancellationToken: source.Token); |
|||
|
|||
#if SUPPORTS_WINFORMS_TASK_DIALOG
|
|||
TaskDialog.ShowDialog(new TaskDialogPage |
|||
{ |
|||
Caption = "Authentication successful", |
|||
Heading = "Authentication successful", |
|||
Icon = TaskDialogIcon.ShieldSuccessGreenBar, |
|||
Text = $"Welcome, {principal.FindFirst(Claims.Name)!.Value}." |
|||
}); |
|||
#else
|
|||
MessageBox.Show($"Welcome, {principal.FindFirst(Claims.Name)!.Value}.", |
|||
"Authentication successful", MessageBoxButtons.OK, MessageBoxIcon.Information); |
|||
#endif
|
|||
} |
|||
|
|||
catch (OperationCanceledException) |
|||
{ |
|||
#if SUPPORTS_WINFORMS_TASK_DIALOG
|
|||
TaskDialog.ShowDialog(new TaskDialogPage |
|||
{ |
|||
Caption = "Authentication timed out", |
|||
Heading = "Authentication timed out", |
|||
Icon = TaskDialogIcon.Warning, |
|||
Text = "The authentication process was aborted." |
|||
}); |
|||
#else
|
|||
MessageBox.Show("The authentication process was aborted.", |
|||
"Authentication timed out", MessageBoxButtons.OK, MessageBoxIcon.Warning); |
|||
#endif
|
|||
} |
|||
|
|||
catch (ProtocolException exception) when (exception.Error is Errors.AccessDenied) |
|||
{ |
|||
#if SUPPORTS_WINFORMS_TASK_DIALOG
|
|||
TaskDialog.ShowDialog(new TaskDialogPage |
|||
{ |
|||
Caption = "Authorization denied", |
|||
Heading = "Authorization denied", |
|||
Icon = TaskDialogIcon.Warning, |
|||
Text = "The authorization was denied by the end user." |
|||
}); |
|||
#else
|
|||
MessageBox.Show("The authorization was denied by the end user.", |
|||
"Authorization denied", MessageBoxButtons.OK, MessageBoxIcon.Warning); |
|||
#endif
|
|||
} |
|||
|
|||
catch |
|||
{ |
|||
#if SUPPORTS_WINFORMS_TASK_DIALOG
|
|||
TaskDialog.ShowDialog(new TaskDialogPage |
|||
{ |
|||
Caption = "Authentication failed", |
|||
Heading = "Authentication failed", |
|||
Icon = TaskDialogIcon.Error, |
|||
Text = "An error occurred while trying to authenticate the user." |
|||
}); |
|||
#else
|
|||
MessageBox.Show("An error occurred while trying to authenticate the user.", |
|||
"Authentication failed", MessageBoxButtons.OK, MessageBoxIcon.Error); |
|||
#endif
|
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<PropertyGroup> |
|||
<OutputType>WinExe</OutputType> |
|||
<TargetFrameworks>net48;net7.0-windows</TargetFrameworks> |
|||
<EnablePreviewFeatures>true</EnablePreviewFeatures> |
|||
<EnableWindowsTargeting>true</EnableWindowsTargeting> |
|||
<UseWindowsForms>true</UseWindowsForms> |
|||
<IsShipping>false</IsShipping> |
|||
<SignAssembly>false</SignAssembly> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\src\OpenIddict.Client.SystemNetHttp\OpenIddict.Client.SystemNetHttp.csproj" /> |
|||
<ProjectReference Include="..\..\src\OpenIddict.Client.WebIntegration\OpenIddict.Client.WebIntegration.csproj" /> |
|||
<ProjectReference Include="..\..\src\OpenIddict.Client.Windows\OpenIddict.Client.Windows.csproj" /> |
|||
<ProjectReference Include="..\..\src\OpenIddict.EntityFrameworkCore\OpenIddict.EntityFrameworkCore.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Dapplo.Microsoft.Extensions.Hosting.AppServices" /> |
|||
<PackageReference Include="Dapplo.Microsoft.Extensions.Hosting.WinForms" /> |
|||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" /> |
|||
<PackageReference Include="Microsoft.Extensions.Hosting" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<Using Remove="System.Net.Http" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,94 @@ |
|||
using Dapplo.Microsoft.Extensions.Hosting.WinForms; |
|||
using Microsoft.EntityFrameworkCore; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Hosting; |
|||
using OpenIddict.Client; |
|||
using OpenIddict.Sandbox.WinForms.Client; |
|||
using static OpenIddict.Abstractions.OpenIddictConstants; |
|||
|
|||
var host = Host.CreateDefaultBuilder(args) |
|||
// 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 = "{D6FEAFC8-3079-4881-B9F2-0B78EAF38B85}")
|
|||
//
|
|||
.ConfigureServices(services => |
|||
{ |
|||
services.AddDbContext<DbContext>(options => |
|||
{ |
|||
options.UseSqlite($"Filename={Path.Combine(Path.GetTempPath(), "openiddict-sandbox-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<DbContext>(); |
|||
}) |
|||
|
|||
// 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(); |
|||
|
|||
// Register the Windows host.
|
|||
options.UseWindows(); |
|||
|
|||
// Set the client URI that will uniquely identify this application.
|
|||
options.SetClientUri(new Uri("openiddict-sandbox-winforms-client://localhost/", UriKind.Absolute)); |
|||
|
|||
// 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); |
|||
|
|||
// Add a client registration matching the client application definition in the server project.
|
|||
options.AddRegistration(new OpenIddictClientRegistration |
|||
{ |
|||
Issuer = new Uri("https://localhost:44395/", UriKind.Absolute), |
|||
ProviderName = "Local", |
|||
|
|||
ClientId = "winforms", |
|||
RedirectUri = new Uri("callback/login/local", UriKind.Relative), |
|||
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() |
|||
.UseTwitter() |
|||
.SetClientId("bXgwc0U3N3A3YWNuaWVsdlRmRWE6MTpjaQ") |
|||
.SetClientSecret("VcohOgBp-6yQCurngo4GAyKeZh0D6SUCCSjJgEo1uRzJarjIUS") |
|||
.SetRedirectUri(new Uri("callback/login/twitter", UriKind.Relative)); |
|||
}); |
|||
|
|||
// 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<Worker>(); |
|||
}) |
|||
.ConfigureWinForms<MainForm>() |
|||
.UseWinFormsLifetime() |
|||
.Build(); |
|||
|
|||
await host.RunAsync(); |
|||
@ -0,0 +1,55 @@ |
|||
using System.Diagnostics; |
|||
using Microsoft.EntityFrameworkCore; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Hosting; |
|||
using Microsoft.Win32; |
|||
|
|||
namespace OpenIddict.Sandbox.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<DbContext>(); |
|||
await context.Database.EnsureCreatedAsync(); |
|||
|
|||
RegistryKey? root = null; |
|||
|
|||
// Create the registry entries necessary to handle URI protocol activations.
|
|||
// Note: the application MUST be run once as an administrator for this to work.
|
|||
try |
|||
{ |
|||
root = Registry.ClassesRoot.OpenSubKey("openiddict-sandbox-winforms-client"); |
|||
|
|||
if (root is null) |
|||
{ |
|||
root = Registry.ClassesRoot.CreateSubKey("openiddict-sandbox-winforms-client"); |
|||
root.SetValue(string.Empty, "URL:openiddict-sandbox-winforms-client"); |
|||
root.SetValue("URL Protocol", string.Empty); |
|||
|
|||
using var command = root.CreateSubKey("shell\\open\\command"); |
|||
command.SetValue(string.Empty, string.Format("\"{0}\" \"%1\"", |
|||
#if SUPPORTS_ENVIRONMENT_PROCESS_PATH
|
|||
Environment.ProcessPath |
|||
#else
|
|||
Process.GetCurrentProcess().MainModule.FileName |
|||
#endif
|
|||
)); |
|||
} |
|||
} |
|||
|
|||
finally |
|||
{ |
|||
root?.Dispose(); |
|||
} |
|||
} |
|||
|
|||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; |
|||
} |
|||
@ -0,0 +1,7 @@ |
|||
<Application x:Class="OpenIddict.Sandbox.Wpf.Client.App" |
|||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> |
|||
<Application.Resources> |
|||
|
|||
</Application.Resources> |
|||
</Application> |
|||
@ -0,0 +1,8 @@ |
|||
using System.Windows; |
|||
|
|||
namespace OpenIddict.Sandbox.Wpf.Client |
|||
{ |
|||
public partial class App : Application |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
<Window x:Class="OpenIddict.Sandbox.Wpf.Client.MainWindow" |
|||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
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"> |
|||
<Grid> |
|||
<Button Content="Log in using the local server" HorizontalAlignment="Center" VerticalAlignment="Top" Click="LocalLoginButton_Click" Height="64" Width="252" FontSize="20" Margin="0,130,0,0" /> |
|||
<Button Content="Log in using Twitter" HorizontalAlignment="Center" VerticalAlignment="Top" Click="TwitterLoginButton_Click" Height="64" Width="252" FontSize="20" Margin="0,234,0,0" /> |
|||
</Grid> |
|||
</Window> |
|||
@ -0,0 +1,66 @@ |
|||
using System.Windows; |
|||
using Dapplo.Microsoft.Extensions.Hosting.Wpf; |
|||
using OpenIddict.Client; |
|||
using static OpenIddict.Abstractions.OpenIddictConstants; |
|||
using static OpenIddict.Abstractions.OpenIddictExceptions; |
|||
using static OpenIddict.Client.WebIntegration.OpenIddictClientWebIntegrationConstants; |
|||
|
|||
namespace OpenIddict.Sandbox.Wpf.Client |
|||
{ |
|||
public partial class MainWindow : Window, IWpfShell |
|||
{ |
|||
private readonly OpenIddictClientService _service; |
|||
|
|||
public MainWindow(OpenIddictClientService service) |
|||
{ |
|||
_service = service ?? throw new ArgumentNullException(nameof(service)); |
|||
|
|||
InitializeComponent(); |
|||
} |
|||
|
|||
private async void LocalLoginButton_Click(object sender, RoutedEventArgs e) |
|||
=> await AuthenticateAsync("Local"); |
|||
|
|||
private async void TwitterLoginButton_Click(object sender, RoutedEventArgs e) |
|||
=> await AuthenticateAsync(Providers.Twitter); |
|||
|
|||
private async Task AuthenticateAsync(string provider) |
|||
{ |
|||
using var source = new CancellationTokenSource(delay: TimeSpan.FromSeconds(90)); |
|||
|
|||
try |
|||
{ |
|||
// Ask OpenIddict to initiate the challenge and launch the system browser
|
|||
// to allow the user to complete the interactive authentication dance.
|
|||
var nonce = await _service.ChallengeWithBrowserAsync( |
|||
provider, cancellationToken: source.Token); |
|||
|
|||
// Wait until the user approved or rejected the authorization
|
|||
// demand and retrieve the resulting claims-based principal.
|
|||
var (_, _, principal) = await _service.AuthenticateWithBrowserAsync( |
|||
nonce, cancellationToken: source.Token); |
|||
|
|||
MessageBox.Show($"Welcome, {principal.FindFirst(Claims.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); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,32 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<PropertyGroup> |
|||
<OutputType>WinExe</OutputType> |
|||
<TargetFrameworks>net48;net7.0-windows</TargetFrameworks> |
|||
<EnablePreviewFeatures>true</EnablePreviewFeatures> |
|||
<EnableWindowsTargeting>true</EnableWindowsTargeting> |
|||
<UseWPF>true</UseWPF> |
|||
<IsShipping>false</IsShipping> |
|||
<SignAssembly>false</SignAssembly> |
|||
<EnableDefaultApplicationDefinition>false</EnableDefaultApplicationDefinition> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\src\OpenIddict.Client.SystemNetHttp\OpenIddict.Client.SystemNetHttp.csproj" /> |
|||
<ProjectReference Include="..\..\src\OpenIddict.Client.WebIntegration\OpenIddict.Client.WebIntegration.csproj" /> |
|||
<ProjectReference Include="..\..\src\OpenIddict.Client.Windows\OpenIddict.Client.Windows.csproj" /> |
|||
<ProjectReference Include="..\..\src\OpenIddict.EntityFrameworkCore\OpenIddict.EntityFrameworkCore.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Dapplo.Microsoft.Extensions.Hosting.AppServices" /> |
|||
<PackageReference Include="Dapplo.Microsoft.Extensions.Hosting.Wpf" /> |
|||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" /> |
|||
<PackageReference Include="Microsoft.Extensions.Hosting" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<Using Remove="System.Net.Http" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,99 @@ |
|||
using System.IO; |
|||
using Dapplo.Microsoft.Extensions.Hosting.Wpf; |
|||
using Microsoft.EntityFrameworkCore; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Hosting; |
|||
using OpenIddict.Client; |
|||
using OpenIddict.Sandbox.Wpf.Client; |
|||
using static OpenIddict.Abstractions.OpenIddictConstants; |
|||
|
|||
var host = Host.CreateDefaultBuilder(args) |
|||
// 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 = "{C587B9EA-A870-4CF3-8B00-33DF67FCA143}")
|
|||
//
|
|||
.ConfigureServices(services => |
|||
{ |
|||
services.AddDbContext<DbContext>(options => |
|||
{ |
|||
options.UseSqlite($"Filename={Path.Combine(Path.GetTempPath(), "openiddict-sandbox-wpf-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<DbContext>(); |
|||
}) |
|||
|
|||
// 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(); |
|||
|
|||
// Register the Windows host.
|
|||
options.UseWindows(); |
|||
|
|||
// Set the client URI that will uniquely identify this application.
|
|||
options.SetClientUri(new Uri("openiddict-sandbox-wpf-client://localhost/", UriKind.Absolute)); |
|||
|
|||
// 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); |
|||
|
|||
// Add a client registration matching the client application definition in the server project.
|
|||
options.AddRegistration(new OpenIddictClientRegistration |
|||
{ |
|||
Issuer = new Uri("https://localhost:44395/", UriKind.Absolute), |
|||
ProviderName = "Local", |
|||
|
|||
ClientId = "wpf", |
|||
RedirectUri = new Uri("callback/login/local", UriKind.Relative), |
|||
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() |
|||
.UseTwitter() |
|||
.SetClientId("bXgwc0U3N3A3YWNuaWVsdlRmRWE6MTpjaQ") |
|||
.SetClientSecret("VcohOgBp-6yQCurngo4GAyKeZh0D6SUCCSjJgEo1uRzJarjIUS") |
|||
.SetRedirectUri(new Uri("callback/login/twitter", UriKind.Relative)); |
|||
}); |
|||
|
|||
// 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<Worker>(); |
|||
}) |
|||
.ConfigureWpf(options => |
|||
{ |
|||
options.UseApplication<App>(); |
|||
options.UseWindow<MainWindow>(); |
|||
}) |
|||
.UseWpfLifetime() |
|||
.Build(); |
|||
|
|||
await host.RunAsync(); |
|||
@ -0,0 +1,55 @@ |
|||
using System.Diagnostics; |
|||
using Microsoft.EntityFrameworkCore; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Hosting; |
|||
using Microsoft.Win32; |
|||
|
|||
namespace OpenIddict.Sandbox.Wpf.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<DbContext>(); |
|||
await context.Database.EnsureCreatedAsync(); |
|||
|
|||
RegistryKey? root = null; |
|||
|
|||
// Create the registry entries necessary to handle URI protocol activations.
|
|||
// Note: the application MUST be run once as an administrator for this to work.
|
|||
try |
|||
{ |
|||
root = Registry.ClassesRoot.OpenSubKey("openiddict-sandbox-wpf-client"); |
|||
|
|||
if (root is null) |
|||
{ |
|||
root = Registry.ClassesRoot.CreateSubKey("openiddict-sandbox-wpf-client"); |
|||
root.SetValue(string.Empty, "URL:openiddict-sandbox-wpf-client"); |
|||
root.SetValue("URL Protocol", string.Empty); |
|||
|
|||
using var command = root.CreateSubKey("shell\\open\\command"); |
|||
command.SetValue(string.Empty, string.Format("\"{0}\" \"%1\"", |
|||
#if SUPPORTS_ENVIRONMENT_PROCESS_PATH
|
|||
Environment.ProcessPath |
|||
#else
|
|||
Process.GetCurrentProcess().MainModule.FileName |
|||
#endif
|
|||
)); |
|||
} |
|||
} |
|||
|
|||
finally |
|||
{ |
|||
root?.Dispose(); |
|||
} |
|||
} |
|||
|
|||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; |
|||
} |
|||
@ -0,0 +1,41 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFrameworks>net461;netcoreapp3.1;net6.0-windows;net7.0-windows</TargetFrameworks> |
|||
<EnablePreviewFeatures>true</EnablePreviewFeatures> |
|||
</PropertyGroup> |
|||
|
|||
<PropertyGroup> |
|||
<Description>Windows integration package for the OpenIddict client services.</Description> |
|||
<PackageTags>$(PackageTags);client;windows</PackageTags> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\OpenIddict.Client\OpenIddict.Client.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup |
|||
Condition=" ('$(TargetFrameworkIdentifier)' == '.NETCoreApp' And $([MSBuild]::VersionLessThan($(TargetFrameworkVersion), '5.0'))) "> |
|||
<PackageReference Include="NamedPipeServerStream.NetFrameworkVersion" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<Compile Include="..\..\shared\OpenIddict.Extensions\*\*.cs" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<Using Include="OpenIddict.Abstractions" /> |
|||
<Using Include="OpenIddict.Abstractions.OpenIddictConstants" Static="true" /> |
|||
<Using Include="OpenIddict.Abstractions.OpenIddictResources" Alias="SR" /> |
|||
<Using Include="OpenIddict.Client.OpenIddictClientEvents" Static="true" /> |
|||
<Using Include="OpenIddict.Client.OpenIddictClientHandlers" Static="true" /> |
|||
<Using Include="OpenIddict.Client.OpenIddictClientHandlerFilters" Static="true" /> |
|||
<Using Include="OpenIddict.Client.Windows.OpenIddictClientWindowsHandlers" Static="true" /> |
|||
<Using Include="OpenIddict.Client.Windows.OpenIddictClientWindowsHandlerFilters" Static="true" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,29 @@ |
|||
/* |
|||
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
|
|||
* See https://github.com/openiddict/openiddict-core for more information concerning
|
|||
* the license and the contributors participating to this project. |
|||
*/ |
|||
|
|||
using System.Collections.Immutable; |
|||
using System.ComponentModel; |
|||
|
|||
namespace OpenIddict.Client.Windows; |
|||
|
|||
/// <summary>
|
|||
/// Represents a Windows application activation.
|
|||
/// </summary>
|
|||
[EditorBrowsable(EditorBrowsableState.Advanced)] |
|||
public sealed class OpenIddictClientWindowsActivation |
|||
{ |
|||
/// <summary>
|
|||
/// Gets or sets the command line arguments used to
|
|||
/// launch the current instance of the application.
|
|||
/// </summary>
|
|||
public ImmutableArray<string> CommandLineArguments { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a boolean indicating whether the activation
|
|||
/// was redirected from another instance of the application.
|
|||
/// </summary>
|
|||
public bool IsActivationRedirected { get; set; } |
|||
} |
|||
@ -0,0 +1,131 @@ |
|||
/* |
|||
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
|
|||
* See https://github.com/openiddict/openiddict-core for more information concerning
|
|||
* the license and the contributors participating to this project. |
|||
*/ |
|||
|
|||
using System.ComponentModel; |
|||
using System.IO.Pipes; |
|||
using OpenIddict.Client.Windows; |
|||
|
|||
namespace Microsoft.Extensions.DependencyInjection; |
|||
|
|||
/// <summary>
|
|||
/// Exposes the necessary methods required to configure
|
|||
/// the OpenIddict client Windows integration.
|
|||
/// </summary>
|
|||
public sealed class OpenIddictClientWindowsBuilder |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of <see cref="OpenIddictClientWindowsBuilder"/>.
|
|||
/// </summary>
|
|||
/// <param name="services">The services collection.</param>
|
|||
public OpenIddictClientWindowsBuilder(IServiceCollection services) |
|||
=> Services = services ?? throw new ArgumentNullException(nameof(services)); |
|||
|
|||
/// <summary>
|
|||
/// Gets the services collection.
|
|||
/// </summary>
|
|||
[EditorBrowsable(EditorBrowsableState.Never)] |
|||
public IServiceCollection Services { get; } |
|||
|
|||
/// <summary>
|
|||
/// Amends the default OpenIddict client Windows configuration.
|
|||
/// </summary>
|
|||
/// <param name="configuration">The delegate used to configure the OpenIddict options.</param>
|
|||
/// <remarks>This extension can be safely called multiple times.</remarks>
|
|||
/// <returns>The <see cref="OpenIddictClientWindowsBuilder"/>.</returns>
|
|||
public OpenIddictClientWindowsBuilder Configure(Action<OpenIddictClientWindowsOptions> configuration) |
|||
{ |
|||
if (configuration is null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(configuration)); |
|||
} |
|||
|
|||
Services.Configure(configuration); |
|||
|
|||
return this; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Sets the timeout after which authentication demands that
|
|||
/// are not completed are automatically aborted by OpenIddict.
|
|||
/// </summary>
|
|||
/// <param name="timeout">The authentication timeout.</param>
|
|||
/// <returns>The <see cref="OpenIddictClientWindowsBuilder"/>.</returns>
|
|||
public OpenIddictClientWindowsBuilder SetAuthenticationTimeout(TimeSpan timeout) |
|||
=> Configure(options => options.AuthenticationTimeout = timeout); |
|||
|
|||
/// <summary>
|
|||
/// Sets the identifier used to represent the current application
|
|||
/// instance and redirect protocol activations when necessary.
|
|||
/// </summary>
|
|||
/// <param name="identifier">The identifier of the current instance.</param>
|
|||
/// <returns>The <see cref="OpenIddictClientWindowsBuilder"/>.</returns>
|
|||
[EditorBrowsable(EditorBrowsableState.Advanced)] |
|||
public OpenIddictClientWindowsBuilder SetInstanceIdentifier(string identifier) |
|||
{ |
|||
if (string.IsNullOrEmpty(identifier)) |
|||
{ |
|||
throw new ArgumentException(SR.FormatID0366(nameof(identifier)), nameof(identifier)); |
|||
} |
|||
|
|||
return Configure(options => options.InstanceIdentifier = identifier); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Sets the base name of the pipe created by OpenIddict to enable
|
|||
/// inter-process communication and handle protocol activation redirections.
|
|||
/// </summary>
|
|||
/// <param name="name">The name of the pipe.</param>
|
|||
/// <returns>The <see cref="OpenIddictClientWindowsBuilder"/>.</returns>
|
|||
[EditorBrowsable(EditorBrowsableState.Advanced)] |
|||
public OpenIddictClientWindowsBuilder SetPipeName(string name) |
|||
{ |
|||
if (string.IsNullOrEmpty(name)) |
|||
{ |
|||
throw new ArgumentException(SR.FormatID0366(nameof(name)), nameof(name)); |
|||
} |
|||
|
|||
return Configure(options => options.PipeName = name); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Sets the security policy applied to the pipe created by OpenIddict to enable
|
|||
/// inter-process communication and handle protocol activation redirections.
|
|||
/// </summary>
|
|||
/// <param name="security">The security policy applied to the pipe.</param>
|
|||
/// <returns>The <see cref="OpenIddictClientWindowsBuilder"/>.</returns>
|
|||
[EditorBrowsable(EditorBrowsableState.Advanced)] |
|||
public OpenIddictClientWindowsBuilder SetPipeSecurity(PipeSecurity security) |
|||
{ |
|||
if (security is null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(security)); |
|||
} |
|||
|
|||
return Configure(options => options.PipeSecurity = security); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Determines whether the specified object is equal to the current object.
|
|||
/// </summary>
|
|||
/// <param name="obj">The object to compare with the current object.</param>
|
|||
/// <returns><see langword="true"/> if the specified object is equal to the current object; otherwise, false.</returns>
|
|||
[EditorBrowsable(EditorBrowsableState.Never)] |
|||
public override bool Equals(object? obj) => base.Equals(obj); |
|||
|
|||
/// <summary>
|
|||
/// Serves as the default hash function.
|
|||
/// </summary>
|
|||
/// <returns>A hash code for the current object.</returns>
|
|||
[EditorBrowsable(EditorBrowsableState.Never)] |
|||
public override int GetHashCode() => base.GetHashCode(); |
|||
|
|||
/// <summary>
|
|||
/// Returns a string that represents the current object.
|
|||
/// </summary>
|
|||
/// <returns>A string that represents the current object.</returns>
|
|||
[EditorBrowsable(EditorBrowsableState.Never)] |
|||
public override string? ToString() => base.ToString(); |
|||
} |
|||
@ -0,0 +1,108 @@ |
|||
/* |
|||
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
|
|||
* See https://github.com/openiddict/openiddict-core for more information concerning
|
|||
* the license and the contributors participating to this project. |
|||
*/ |
|||
|
|||
using System.ComponentModel; |
|||
using System.IO.Pipes; |
|||
using System.Security.AccessControl; |
|||
using System.Security.Principal; |
|||
using System.Text; |
|||
using Microsoft.Extensions.Hosting; |
|||
using Microsoft.Extensions.Options; |
|||
using Microsoft.IdentityModel.Tokens; |
|||
using OpenIddict.Extensions; |
|||
|
|||
#if !SUPPORTS_HOST_ENVIRONMENT
|
|||
using IHostEnvironment = Microsoft.Extensions.Hosting.IHostingEnvironment; |
|||
#endif
|
|||
|
|||
namespace OpenIddict.Client.Windows; |
|||
|
|||
/// <summary>
|
|||
/// Contains the methods required to ensure that the OpenIddict client configuration is valid.
|
|||
/// </summary>
|
|||
[EditorBrowsable(EditorBrowsableState.Advanced)] |
|||
public sealed class OpenIddictClientWindowsConfiguration : IConfigureOptions<OpenIddictClientOptions>, |
|||
IPostConfigureOptions<OpenIddictClientOptions>, |
|||
IPostConfigureOptions<OpenIddictClientWindowsOptions> |
|||
{ |
|||
private readonly IHostEnvironment _environment; |
|||
|
|||
/// <summary>
|
|||
/// Creates a new instance of the <see cref="OpenIddictClientWindowsConfiguration"/> class.
|
|||
/// </summary>
|
|||
/// <param name="environment">The host environment.</param>
|
|||
public OpenIddictClientWindowsConfiguration(IHostEnvironment environment) |
|||
=> _environment = environment ?? throw new ArgumentNullException(nameof(environment)); |
|||
|
|||
/// <inheritdoc/>
|
|||
public void Configure(OpenIddictClientOptions options) |
|||
{ |
|||
if (options is null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(options)); |
|||
} |
|||
|
|||
// Register the built-in event handlers used by the OpenIddict Windows client components.
|
|||
options.Handlers.AddRange(OpenIddictClientWindowsHandlers.DefaultHandlers); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void PostConfigure(string? name, OpenIddictClientOptions options) |
|||
{ |
|||
if (options is null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(options)); |
|||
} |
|||
|
|||
// Ensure an explicit client URI was set when using the Windows integration.
|
|||
if (options.ClientUri is not { IsAbsoluteUri: true }) |
|||
{ |
|||
throw new InvalidOperationException(SR.GetResourceString(SR.ID0384)); |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void PostConfigure(string? name, OpenIddictClientWindowsOptions options) |
|||
{ |
|||
if (options is null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(options)); |
|||
} |
|||
|
|||
// If no explicit instance identifier was specified, use a random GUID.
|
|||
if (string.IsNullOrEmpty(options.InstanceIdentifier)) |
|||
{ |
|||
options.InstanceIdentifier = Guid.NewGuid().ToString(); |
|||
} |
|||
|
|||
// If no explicit pipe name was specified, compute the SHA-256 hash of the
|
|||
// application name resolved from the host and use it as a unique identifier.
|
|||
//
|
|||
// Note: the pipe name is deliberately prefixed with "LOCAL\" to support
|
|||
// partial trust/sandboxed applications that are executed in an AppContainer
|
|||
// and cannot communicate with applications outside the sandbox container.
|
|||
if (string.IsNullOrEmpty(options.PipeName)) |
|||
{ |
|||
options.PipeName = $@"LOCAL\OpenIddict.Client.Windows\{
|
|||
Base64UrlEncoder.Encode(OpenIddictHelpers.ComputeSha256Hash( |
|||
Encoding.UTF8.GetBytes(_environment.ApplicationName))) |
|||
}";
|
|||
} |
|||
|
|||
// If no explicit pipe security policy was specified, grant the current user
|
|||
// full control over the created pipe and allow cross-process communication
|
|||
// between elevated and non-elevated processes.
|
|||
if (options.PipeSecurity is null) |
|||
{ |
|||
using var identity = WindowsIdentity.GetCurrent(); |
|||
|
|||
options.PipeSecurity = new PipeSecurity(); |
|||
options.PipeSecurity.SetOwner(identity.User!); |
|||
options.PipeSecurity.AddAccessRule(new PipeAccessRule(identity.User!, |
|||
PipeAccessRights.FullControl, AccessControlType.Allow)); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
/* |
|||
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
|
|||
* See https://github.com/openiddict/openiddict-core for more information concerning
|
|||
* the license and the contributors participating to this project. |
|||
*/ |
|||
|
|||
namespace OpenIddict.Client.Windows; |
|||
|
|||
/// <summary>
|
|||
/// Exposes common constants used by the OpenIddict Windows host.
|
|||
/// </summary>
|
|||
public static class OpenIddictClientWindowsConstants |
|||
{ |
|||
public static class Tokens |
|||
{ |
|||
public const string AuthorizationCode = "authorization_code"; |
|||
public const string BackchannelAccessToken = "backchannel_access_token"; |
|||
public const string BackchannelIdentityToken = "backchannel_id_token"; |
|||
public const string FrontchannelAccessToken = "frontchannel_access_token"; |
|||
public const string FrontchannelIdentityToken = "frontchannel_id_token"; |
|||
public const string RefreshToken = "refresh_token"; |
|||
public const string StateToken = "state_token"; |
|||
public const string UserinfoToken = "userinfo_token"; |
|||
} |
|||
} |
|||
@ -0,0 +1,95 @@ |
|||
/* |
|||
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
|
|||
* See https://github.com/openiddict/openiddict-core for more information concerning
|
|||
* the license and the contributors participating to this project. |
|||
*/ |
|||
|
|||
using Microsoft.Extensions.DependencyInjection.Extensions; |
|||
using Microsoft.Extensions.Hosting; |
|||
using Microsoft.Extensions.Options; |
|||
using OpenIddict.Client; |
|||
using OpenIddict.Client.Windows; |
|||
|
|||
namespace Microsoft.Extensions.DependencyInjection; |
|||
|
|||
/// <summary>
|
|||
/// Exposes extensions allowing to register the OpenIddict client services.
|
|||
/// </summary>
|
|||
public static class OpenIddictClientWindowsExtensions |
|||
{ |
|||
/// <summary>
|
|||
/// Registers the OpenIddict client services for Windows in the DI container.
|
|||
/// </summary>
|
|||
/// <param name="builder">The services builder used by OpenIddict to register new services.</param>
|
|||
/// <remarks>This extension can be safely called multiple times.</remarks>
|
|||
/// <returns>The <see cref="OpenIddictClientWindowsBuilder"/>.</returns>
|
|||
public static OpenIddictClientWindowsBuilder UseWindows(this OpenIddictClientBuilder builder) |
|||
{ |
|||
if (builder is null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(builder)); |
|||
} |
|||
|
|||
// Note: the OpenIddict IHostedService implementation is deliberately registered as early as possible to
|
|||
// ensure protocol activations can be handled before another service can stop the initialization of the
|
|||
// application (e.g Dapplo.Microsoft.Extensions.Hosting.AppServices relies on an IHostedService to implement
|
|||
// single instantiation, which would prevent the OpenIddict service from handling the protocol activation
|
|||
// if the OpenIddict IHostedService implementation was not registered before the Dapplo IHostedService).
|
|||
if (!builder.Services.Any(static descriptor => descriptor.ServiceType == typeof(IHostedService) && |
|||
descriptor.ImplementationType == typeof(OpenIddictClientWindowsService))) |
|||
{ |
|||
builder.Services.Insert(0, ServiceDescriptor.Singleton<IHostedService, OpenIddictClientWindowsService>()); |
|||
} |
|||
|
|||
// Register the marshaller responsible for managing authentication operations.
|
|||
builder.Services.TryAddSingleton<OpenIddictClientWindowsMarshaller>(); |
|||
|
|||
// Register the built-in filters used by the default OpenIddict Windows client event handlers.
|
|||
builder.Services.TryAddSingleton<RequireAuthenticationNonce>(); |
|||
builder.Services.TryAddSingleton<RequireInteractiveSession>(); |
|||
builder.Services.TryAddSingleton<RequireWindowsActivation>(); |
|||
|
|||
// Register the built-in event handlers used by the OpenIddict Windows client components.
|
|||
// Note: the order used here is not important, as the actual order is set in the options.
|
|||
builder.Services.TryAdd(OpenIddictClientWindowsHandlers.DefaultHandlers.Select(descriptor => descriptor.ServiceDescriptor)); |
|||
|
|||
// Register the option initializer and the background service used by the OpenIddict Windows client integration services.
|
|||
// Note: TryAddEnumerable() is used here to ensure the initializers and the background service are only registered once.
|
|||
builder.Services.TryAddEnumerable(new[] |
|||
{ |
|||
ServiceDescriptor.Singleton<IHostedService, OpenIddictClientWindowsListener>(), |
|||
|
|||
ServiceDescriptor.Singleton<IConfigureOptions<OpenIddictClientOptions>, OpenIddictClientWindowsConfiguration>(), |
|||
ServiceDescriptor.Singleton<IPostConfigureOptions<OpenIddictClientOptions>, OpenIddictClientWindowsConfiguration>(), |
|||
|
|||
ServiceDescriptor.Singleton<IPostConfigureOptions<OpenIddictClientWindowsOptions>, OpenIddictClientWindowsConfiguration>() |
|||
}); |
|||
|
|||
return new OpenIddictClientWindowsBuilder(builder.Services); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Registers the OpenIddict client services for Windows in the DI container.
|
|||
/// </summary>
|
|||
/// <param name="builder">The services builder used by OpenIddict to register new services.</param>
|
|||
/// <param name="configuration">The configuration delegate used to configure the client services.</param>
|
|||
/// <remarks>This extension can be safely called multiple times.</remarks>
|
|||
/// <returns>The <see cref="OpenIddictClientBuilder"/>.</returns>
|
|||
public static OpenIddictClientBuilder UseWindows( |
|||
this OpenIddictClientBuilder builder, Action<OpenIddictClientWindowsBuilder> configuration) |
|||
{ |
|||
if (builder is null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(builder)); |
|||
} |
|||
|
|||
if (configuration is null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(configuration)); |
|||
} |
|||
|
|||
configuration(builder.UseWindows()); |
|||
|
|||
return builder; |
|||
} |
|||
} |
|||
@ -0,0 +1,68 @@ |
|||
/* |
|||
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
|
|||
* See https://github.com/openiddict/openiddict-core for more information concerning
|
|||
* the license and the contributors participating to this project. |
|||
*/ |
|||
|
|||
using System.ComponentModel; |
|||
|
|||
namespace OpenIddict.Client.Windows; |
|||
|
|||
/// <summary>
|
|||
/// Contains a collection of event handler filters commonly used by the Windows handlers.
|
|||
/// </summary>
|
|||
[EditorBrowsable(EditorBrowsableState.Advanced)] |
|||
public static class OpenIddictClientWindowsHandlerFilters |
|||
{ |
|||
/// <summary>
|
|||
/// Represents a filter that excludes the associated handlers
|
|||
/// if no explicit nonce was attached to the authentication context.
|
|||
/// </summary>
|
|||
public sealed class RequireAuthenticationNonce : IOpenIddictClientHandlerFilter<ProcessAuthenticationContext> |
|||
{ |
|||
/// <inheritdoc/>
|
|||
public ValueTask<bool> IsActiveAsync(ProcessAuthenticationContext context) |
|||
{ |
|||
if (context is null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(context)); |
|||
} |
|||
|
|||
return new(!string.IsNullOrEmpty(context.Nonce)); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Represents a filter that excludes the associated handlers if no interactive user session was detected.
|
|||
/// </summary>
|
|||
public sealed class RequireInteractiveSession : IOpenIddictClientHandlerFilter<BaseContext> |
|||
{ |
|||
/// <inheritdoc/>
|
|||
public ValueTask<bool> IsActiveAsync(BaseContext context) |
|||
{ |
|||
if (context is null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(context)); |
|||
} |
|||
|
|||
return new(Environment.UserInteractive); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Represents a filter that excludes the associated handlers if no Windows activation was found.
|
|||
/// </summary>
|
|||
public sealed class RequireWindowsActivation : IOpenIddictClientHandlerFilter<BaseContext> |
|||
{ |
|||
/// <inheritdoc/>
|
|||
public ValueTask<bool> IsActiveAsync(BaseContext context) |
|||
{ |
|||
if (context is null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(context)); |
|||
} |
|||
|
|||
return new(context.Transaction.GetWindowsActivation() is not null); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,82 @@ |
|||
/* |
|||
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
|
|||
* See https://github.com/openiddict/openiddict-core for more information concerning
|
|||
* the license and the contributors participating to this project. |
|||
*/ |
|||
|
|||
using System.Collections.Immutable; |
|||
using System.Diagnostics; |
|||
using Microsoft.Extensions.Primitives; |
|||
using OpenIddict.Extensions; |
|||
|
|||
namespace OpenIddict.Client.Windows; |
|||
|
|||
public static partial class OpenIddictClientWindowsHandlers |
|||
{ |
|||
public static class Authentication |
|||
{ |
|||
public static ImmutableArray<OpenIddictClientHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create( |
|||
/* |
|||
* Authorization request processing: |
|||
*/ |
|||
LaunchSystemBrowser.Descriptor, |
|||
|
|||
/* |
|||
* Redirection request extraction: |
|||
*/ |
|||
ExtractRequestUriParameters<ExtractRedirectionRequestContext>.Descriptor, |
|||
|
|||
/* |
|||
* Redirection request handling: |
|||
*/ |
|||
ProcessResponse<HandleRedirectionRequestContext>.Descriptor, |
|||
|
|||
/* |
|||
* Redirection response handling: |
|||
*/ |
|||
ProcessResponse<ApplyRedirectionResponseContext>.Descriptor); |
|||
|
|||
/// <summary>
|
|||
/// Contains the logic responsible for initiating authorization requests using the system browser.
|
|||
/// Note: this handler is not used when the OpenID Connect request is not initially handled by Windows.
|
|||
/// </summary>
|
|||
public class LaunchSystemBrowser : IOpenIddictClientHandler<ApplyAuthorizationRequestContext> |
|||
{ |
|||
/// <summary>
|
|||
/// Gets the default descriptor definition assigned to this handler.
|
|||
/// </summary>
|
|||
public static OpenIddictClientHandlerDescriptor Descriptor { get; } |
|||
= OpenIddictClientHandlerDescriptor.CreateBuilder<ApplyAuthorizationRequestContext>() |
|||
.AddFilter<RequireInteractiveSession>() |
|||
.UseSingletonHandler<LaunchSystemBrowser>() |
|||
.SetOrder(50_000) |
|||
.SetType(OpenIddictClientHandlerType.BuiltIn) |
|||
.Build(); |
|||
|
|||
/// <inheritdoc/>
|
|||
public ValueTask HandleAsync(ApplyAuthorizationRequestContext context) |
|||
{ |
|||
if (context is null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(context)); |
|||
} |
|||
|
|||
Debug.Assert(context.Transaction.Request is not null, SR.GetResourceString(SR.ID4008)); |
|||
|
|||
var uri = OpenIddictHelpers.AddQueryStringParameters( |
|||
new Uri(context.AuthorizationEndpoint, UriKind.Absolute), |
|||
context.Transaction.Request.GetParameters().ToDictionary( |
|||
parameter => parameter.Key, |
|||
parameter => new StringValues((string?[]?) parameter.Value))); |
|||
|
|||
Process.Start(new ProcessStartInfo |
|||
{ |
|||
FileName = uri.AbsoluteUri, |
|||
UseShellExecute = true |
|||
}); |
|||
|
|||
return default; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
File diff suppressed because it is too large
@ -0,0 +1,21 @@ |
|||
/* |
|||
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
|
|||
* See https://github.com/openiddict/openiddict-core for more information concerning
|
|||
* the license and the contributors participating to this project. |
|||
*/ |
|||
|
|||
namespace OpenIddict.Client.Windows; |
|||
|
|||
/// <summary>
|
|||
/// Exposes companion extensions for the OpenIddict/Windows integration.
|
|||
/// </summary>
|
|||
public static class OpenIddictClientWindowsHelpers |
|||
{ |
|||
/// <summary>
|
|||
/// Gets the <see cref="OpenIddictClientWindowsActivation"/> associated with the current context.
|
|||
/// </summary>
|
|||
/// <param name="transaction">The transaction instance.</param>
|
|||
/// <returns>The <see cref="OpenIddictClientWindowsActivation"/> instance or <see langword="null"/> if it couldn't be found.</returns>
|
|||
public static OpenIddictClientWindowsActivation? GetWindowsActivation(this OpenIddictClientTransaction transaction) |
|||
=> transaction.GetProperty<OpenIddictClientWindowsActivation>(typeof(OpenIddictClientWindowsActivation).FullName!); |
|||
} |
|||
@ -0,0 +1,161 @@ |
|||
/* |
|||
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
|
|||
* See https://github.com/openiddict/openiddict-core for more information concerning
|
|||
* the license and the contributors participating to this project. |
|||
*/ |
|||
|
|||
using System.Collections.Immutable; |
|||
using System.ComponentModel; |
|||
using System.IO.Pipes; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Hosting; |
|||
using Microsoft.Extensions.Logging; |
|||
using Microsoft.Extensions.Options; |
|||
|
|||
#if !SUPPORTS_HOST_APPLICATION_LIFETIME
|
|||
using IHostApplicationLifetime = Microsoft.Extensions.Hosting.IApplicationLifetime; |
|||
#endif
|
|||
|
|||
namespace OpenIddict.Client.Windows; |
|||
|
|||
/// <summary>
|
|||
/// Contains the logic necessary to handle URI protocol activations that
|
|||
/// are redirected by other instances using inter-process communication.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Note: initial URI protocol activations are handled by <see cref="OpenIddictClientWindowsService"/>.
|
|||
/// </remarks>
|
|||
[EditorBrowsable(EditorBrowsableState.Never)] |
|||
public sealed class OpenIddictClientWindowsListener : BackgroundService |
|||
{ |
|||
private readonly ILogger<OpenIddictClientWindowsListener> _logger; |
|||
private readonly IOptionsMonitor<OpenIddictClientWindowsOptions> _options; |
|||
private readonly IServiceProvider _provider; |
|||
|
|||
public OpenIddictClientWindowsListener( |
|||
ILogger<OpenIddictClientWindowsListener> logger, |
|||
IOptionsMonitor<OpenIddictClientWindowsOptions> options, |
|||
IServiceProvider provider) |
|||
{ |
|||
_logger = logger ?? throw new ArgumentNullException(nameof(logger)); |
|||
_options = options ?? throw new ArgumentNullException(nameof(options)); |
|||
_provider = provider ?? throw new ArgumentNullException(nameof(provider)); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
protected override async Task ExecuteAsync(CancellationToken stoppingToken) |
|||
{ |
|||
while (!stoppingToken.IsCancellationRequested) |
|||
{ |
|||
using var buffer = new MemoryStream(); |
|||
using var reader = new BinaryReader(buffer); |
|||
|
|||
#if SUPPORTS_NAMED_PIPE_CONSTRUCTOR_WITH_ACL
|
|||
using var stream = new NamedPipeServerStream( |
|||
#elif SUPPORTS_NAMED_PIPE_STATIC_FACTORY_WITH_ACL
|
|||
using var stream = NamedPipeServerStreamAcl.Create( |
|||
#else
|
|||
using var stream = NamedPipeServerStreamConstructors.New( |
|||
#endif
|
|||
pipeName : $@"{_options.CurrentValue.PipeName}\{_options.CurrentValue.InstanceIdentifier}", |
|||
direction : PipeDirection.In, |
|||
maxNumberOfServerInstances: 1, |
|||
transmissionMode : PipeTransmissionMode.Message, |
|||
options : PipeOptions.Asynchronous, |
|||
inBufferSize : 0, |
|||
outBufferSize : 0, |
|||
pipeSecurity : _options.CurrentValue.PipeSecurity, |
|||
inheritability : HandleInheritability.None, |
|||
additionalAccessRights : 0); |
|||
|
|||
// Wait for a writer to connect to the named pipe,
|
|||
// copy its content to the memory stream and rewind it.
|
|||
await stream.WaitForConnectionAsync(stoppingToken); |
|||
await stream.CopyToAsync(buffer, bufferSize: 81_920, stoppingToken); |
|||
buffer.Seek(0L, SeekOrigin.Begin); |
|||
|
|||
var scope = _provider.CreateScope(); |
|||
|
|||
try |
|||
{ |
|||
var dispatcher = scope.ServiceProvider.GetRequiredService<IOpenIddictClientDispatcher>(); |
|||
var factory = scope.ServiceProvider.GetRequiredService<IOpenIddictClientFactory>(); |
|||
|
|||
switch (reader.ReadInt32()) |
|||
{ |
|||
case 0x01: // Protocol activations
|
|||
{ |
|||
// Ensure the binary serialization format is supported.
|
|||
var version = reader.ReadInt32(); |
|||
if (version is not 0x01) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
var length = reader.ReadInt32(); |
|||
if (length is not > 0) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
var builder = ImmutableArray.CreateBuilder<string>(length); |
|||
for (var index = 0; index < length; index++) |
|||
{ |
|||
builder.Add(reader.ReadString()); |
|||
} |
|||
|
|||
// Create a client transaction and store the command line arguments so they can be
|
|||
// retrieved by the Windows-specific client event handlers that need to access them.
|
|||
var transaction = await factory.CreateTransactionAsync(); |
|||
transaction.SetProperty(typeof(OpenIddictClientWindowsActivation).FullName!, |
|||
new OpenIddictClientWindowsActivation |
|||
{ |
|||
CommandLineArguments = builder.MoveToImmutable(), |
|||
IsActivationRedirected = true |
|||
}); |
|||
|
|||
var context = new ProcessRequestContext(transaction) |
|||
{ |
|||
CancellationToken = stoppingToken |
|||
}; |
|||
|
|||
await dispatcher.DispatchAsync(context); |
|||
|
|||
if (context.IsRejected) |
|||
{ |
|||
await dispatcher.DispatchAsync(new ProcessErrorContext(transaction) |
|||
{ |
|||
CancellationToken = stoppingToken, |
|||
Error = context.Error ?? Errors.InvalidRequest, |
|||
ErrorDescription = context.ErrorDescription, |
|||
ErrorUri = context.ErrorUri, |
|||
Response = new OpenIddictResponse() |
|||
}); |
|||
} |
|||
|
|||
break; |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Swallow all exceptions to ensure the service doesn't exit when encountering an exception.
|
|||
catch (Exception exception) |
|||
{ |
|||
_logger.LogWarning(exception, SR.GetResourceString(SR.ID6213)); |
|||
} |
|||
|
|||
finally |
|||
{ |
|||
if (scope is IAsyncDisposable disposable) |
|||
{ |
|||
await disposable.DisposeAsync(); |
|||
} |
|||
|
|||
else |
|||
{ |
|||
scope.Dispose(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,130 @@ |
|||
/* |
|||
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
|
|||
* See https://github.com/openiddict/openiddict-core for more information concerning
|
|||
* the license and the contributors participating to this project. |
|||
*/ |
|||
|
|||
using System.Collections.Concurrent; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
|
|||
namespace OpenIddict.Client.Windows; |
|||
|
|||
/// <summary>
|
|||
/// Contains the APIs needed to coordinate authentication operations that happen in a different context.
|
|||
/// </summary>
|
|||
public sealed class OpenIddictClientWindowsMarshaller |
|||
{ |
|||
private readonly ConcurrentDictionary<string, Lazy<( |
|||
string RequestForgeryProtection, |
|||
SemaphoreSlim Semaphore, |
|||
TaskCompletionSource<ProcessAuthenticationContext> TaskCompletionSource)>> _operations = new(); |
|||
|
|||
/// <summary>
|
|||
/// Determines whether the authentication demand corresponding to the specified nonce is tracked.
|
|||
/// </summary>
|
|||
/// <param name="nonce">The nonce, used as a unique identifier.</param>
|
|||
/// <returns><see langword="true"/> if the operation is tracked, <see langword="false"/> otherwise.</returns>
|
|||
internal bool IsTracked(string nonce) => _operations.ContainsKey(nonce); |
|||
|
|||
/// <summary>
|
|||
/// Tries to add the specified authentication demand to the list of tracked operations.
|
|||
/// </summary>
|
|||
/// <param name="nonce">The nonce, used as a unique identifier.</param>
|
|||
/// <param name="protection">The request forgery protection associated with the specified authentication demand.</param>
|
|||
/// <returns><see langword="true"/> if the operation could be added, <see langword="false"/> otherwise.</returns>
|
|||
internal bool TryAdd(string nonce, string protection) |
|||
=> _operations.TryAdd(nonce, new(() => (protection, new SemaphoreSlim(initialCount: 1, maxCount: 1), new()))); |
|||
|
|||
/// <summary>
|
|||
/// Tries to acquire a lock on the authentication demand corresponding to the specified nonce.
|
|||
/// </summary>
|
|||
/// <param name="nonce">The nonce, used as a unique identifier.</param>
|
|||
/// <returns><see langword="true"/> if the lock could be taken, <see langword="false"/> otherwise.</returns>
|
|||
internal bool TryAcquireLock(string nonce) |
|||
=> _operations.TryGetValue(nonce, out var operation) && operation.Value.Semaphore.Wait(TimeSpan.Zero); |
|||
|
|||
/// <summary>
|
|||
/// Tries to resolve the authentication context associated with the specified nonce.
|
|||
/// </summary>
|
|||
/// <param name="nonce">The nonce, used as a unique identifier.</param>
|
|||
/// <param name="context">The authentication context associated with the tracked operation.</param>
|
|||
/// <returns><see langword="true"/> if the context could be resolved, <see langword="false"/> otherwise.</returns>
|
|||
internal bool TryGetResult(string nonce, [NotNullWhen(true)] out ProcessAuthenticationContext? context) |
|||
{ |
|||
if (!_operations.TryGetValue(nonce, out var operation)) |
|||
{ |
|||
context = null; |
|||
return false; |
|||
} |
|||
|
|||
if (!operation.IsValueCreated || !operation.Value.TaskCompletionSource.Task.IsCompleted) |
|||
{ |
|||
context = null; |
|||
return false; |
|||
} |
|||
|
|||
context = operation.Value.TaskCompletionSource.Task.Result; |
|||
return true; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Tries to wait for the authentication demand associated with the specified nonce to complete.
|
|||
/// </summary>
|
|||
/// <param name="nonce">The nonce, used as a unique identifier.</param>
|
|||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
|||
/// <returns><see langword="true"/> if the authentication demand is tracked, <see langword="false"/> otherwise.</returns>
|
|||
/// <exception cref="OperationCanceledException">The operation was canceled by the user.</exception>
|
|||
internal async Task<bool> TryWaitForCompletionAsync(string nonce, CancellationToken cancellationToken) |
|||
{ |
|||
if (!_operations.TryGetValue(nonce, out var operation)) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
var source = new TaskCompletionSource<bool>(TaskCreationOptions.None); |
|||
using (cancellationToken.Register(static state => ((TaskCompletionSource<bool>) state!).SetResult(true), source)) |
|||
{ |
|||
if (await Task.WhenAny(operation.Value.TaskCompletionSource.Task, source.Task) == source.Task) |
|||
{ |
|||
throw new OperationCanceledException(cancellationToken); |
|||
} |
|||
|
|||
await operation.Value.TaskCompletionSource.Task; |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Tries to resolve the request forgery protection associated with the specified authentication demand.
|
|||
/// </summary>
|
|||
/// <param name="nonce">The nonce, used as a unique identifier.</param>
|
|||
/// <param name="protection">The request forgery protection associated with the specified authentication demand.</param>
|
|||
/// <returns><see langword="true"/> if the operation could be validated, <see langword="false"/> otherwise.</returns>
|
|||
internal bool TryGetRequestForgeryProtection(string nonce, [NotNullWhen(true)] out string? protection) |
|||
{ |
|||
if (_operations.TryGetValue(nonce, out var operation)) |
|||
{ |
|||
protection = operation.Value.RequestForgeryProtection; |
|||
return true; |
|||
} |
|||
|
|||
protection = null; |
|||
return false; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Tries to complete the specified authentication demand.
|
|||
/// </summary>
|
|||
/// <param name="nonce">The nonce, used as a unique identifier.</param>
|
|||
/// <param name="context">The authentication context that will be returned to the caller.</param>
|
|||
/// <returns><see langword="true"/> if the operation could be completed, <see langword="false"/> otherwise.</returns>
|
|||
internal bool TryComplete(string nonce, ProcessAuthenticationContext context) |
|||
=> _operations.TryGetValue(nonce, out var operation) && operation.Value.TaskCompletionSource.TrySetResult(context); |
|||
|
|||
/// <summary>
|
|||
/// Tries to remove the specified authentication operation from the list of tracked operations.
|
|||
/// </summary>
|
|||
/// <param name="nonce">The nonce, used as a unique identifier.</param>
|
|||
/// <returns><see langword="true"/> if the operation could be removed, <see langword="false"/> otherwise.</returns>
|
|||
internal bool TryRemove(string nonce) => _operations.TryRemove(nonce, out _); |
|||
} |
|||
@ -0,0 +1,50 @@ |
|||
/* |
|||
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
|
|||
* See https://github.com/openiddict/openiddict-core for more information concerning
|
|||
* the license and the contributors participating to this project. |
|||
*/ |
|||
|
|||
using System.IO.Pipes; |
|||
using Microsoft.Extensions.Hosting; |
|||
|
|||
#if !SUPPORTS_HOST_ENVIRONMENT
|
|||
using IHostEnvironment = Microsoft.Extensions.Hosting.IHostingEnvironment; |
|||
#endif
|
|||
|
|||
namespace OpenIddict.Client.Windows; |
|||
|
|||
/// <summary>
|
|||
/// Provides various settings needed to configure the OpenIddict Windows client integration.
|
|||
/// </summary>
|
|||
public sealed class OpenIddictClientWindowsOptions |
|||
{ |
|||
/// <summary>
|
|||
/// Gets or sets the timeout after which authentication demands
|
|||
/// that are not completed are automatically aborted by OpenIddict.
|
|||
/// </summary>
|
|||
public TimeSpan AuthenticationTimeout { get; set; } = TimeSpan.FromMinutes(10); |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the identifier used to represent the current application
|
|||
/// instance and redirect protocol activations when necessary.
|
|||
/// </summary>
|
|||
public string? InstanceIdentifier { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the base name of the pipe created by OpenIddict to enable
|
|||
/// inter-process communication and handle protocol activation redirections.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// If no value is explicitly set, a default name is automatically computed.
|
|||
/// </remarks>
|
|||
public string PipeName { get; set; } = default!; |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the security policy applied to the pipe created by OpenIddict
|
|||
/// to enable inter-process communication and handle protocol activation redirections.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// If no value is explicitly set, a default policy is automatically created.
|
|||
/// </remarks>
|
|||
public PipeSecurity PipeSecurity { get; set; } = default!; |
|||
} |
|||
@ -0,0 +1,103 @@ |
|||
/* |
|||
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
|
|||
* See https://github.com/openiddict/openiddict-core for more information concerning
|
|||
* the license and the contributors participating to this project. |
|||
*/ |
|||
|
|||
using System.Collections.Immutable; |
|||
using System.ComponentModel; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Hosting; |
|||
|
|||
#if !SUPPORTS_HOST_APPLICATION_LIFETIME
|
|||
using IHostApplicationLifetime = Microsoft.Extensions.Hosting.IApplicationLifetime; |
|||
#endif
|
|||
|
|||
namespace OpenIddict.Client.Windows; |
|||
|
|||
/// <summary>
|
|||
/// Contains the logic necessary to handle initial URI protocol activations.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Note: redirected URI protocol activations are handled by <see cref="OpenIddictClientWindowsListener"/>.
|
|||
/// </remarks>
|
|||
[EditorBrowsable(EditorBrowsableState.Never)] |
|||
public sealed class OpenIddictClientWindowsService : IHostedService |
|||
{ |
|||
private readonly IServiceProvider _provider; |
|||
|
|||
/// <summary>
|
|||
/// Creates a new instance of the <see cref="OpenIddictClientWindowsService"/> class.
|
|||
/// </summary>
|
|||
/// <param name="provider">The service provider.</param>
|
|||
/// <exception cref="ArgumentNullException"><paramref name="provider"/> is <see langword="null"/>.</exception>
|
|||
public OpenIddictClientWindowsService(IServiceProvider provider) |
|||
=> _provider = provider ?? throw new ArgumentNullException(nameof(provider)); |
|||
|
|||
/// <inheritdoc/>
|
|||
public async Task StartAsync(CancellationToken cancellationToken) |
|||
{ |
|||
cancellationToken.ThrowIfCancellationRequested(); |
|||
|
|||
// Note: initial URI protocol activation handling is implemented as a regular IHostedService
|
|||
// rather than as a BackgroundService to allow blocking the initialization of the host until
|
|||
// the activation is fully processed by the OpenIddict pipeline. By doing that, the UI thread
|
|||
// is not started until redirection requests (like authorization responses) are fully processed,
|
|||
// which allows handling these requests transparently and helps avoid the "flashing window effect":
|
|||
// once a request has been handled by the OpenIddict pipeline, a dedicated handler is responsible
|
|||
// for stopping the application gracefully using the IHostApplicationLifetime.StopApplication() API.
|
|||
|
|||
var scope = _provider.CreateScope(); |
|||
|
|||
try |
|||
{ |
|||
var dispatcher = scope.ServiceProvider.GetRequiredService<IOpenIddictClientDispatcher>(); |
|||
var factory = scope.ServiceProvider.GetRequiredService<IOpenIddictClientFactory>(); |
|||
|
|||
// Create a client transaction and store the command line arguments so they can be
|
|||
// retrieved by the Windows-specific client event handlers that need to access them.
|
|||
var transaction = await factory.CreateTransactionAsync(); |
|||
transaction.SetProperty(typeof(OpenIddictClientWindowsActivation).FullName!, |
|||
new OpenIddictClientWindowsActivation |
|||
{ |
|||
CommandLineArguments = ImmutableArray.CreateRange(Environment.GetCommandLineArgs()), |
|||
IsActivationRedirected = false |
|||
}); |
|||
|
|||
var context = new ProcessRequestContext(transaction) |
|||
{ |
|||
CancellationToken = cancellationToken |
|||
}; |
|||
|
|||
await dispatcher.DispatchAsync(context); |
|||
|
|||
if (context.IsRejected) |
|||
{ |
|||
await dispatcher.DispatchAsync(new ProcessErrorContext(transaction) |
|||
{ |
|||
CancellationToken = cancellationToken, |
|||
Error = context.Error ?? Errors.InvalidRequest, |
|||
ErrorDescription = context.ErrorDescription, |
|||
ErrorUri = context.ErrorUri, |
|||
Response = new OpenIddictResponse() |
|||
}); |
|||
} |
|||
} |
|||
|
|||
finally |
|||
{ |
|||
if (scope is IAsyncDisposable disposable) |
|||
{ |
|||
await disposable.DisposeAsync(); |
|||
} |
|||
|
|||
else |
|||
{ |
|||
scope.Dispose(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; |
|||
} |
|||
Loading…
Reference in new issue