Browse Source

Update the system integration package to support interactive logout

pull/2012/head
Kévin Chalet 2 years ago
parent
commit
6bb44132b3
  1. 17
      sandbox/OpenIddict.Sandbox.AspNetCore.Server/Worker.cs
  2. 43
      sandbox/OpenIddict.Sandbox.Console.Client/InteractiveService.cs
  3. 3
      sandbox/OpenIddict.Sandbox.Console.Client/Program.cs
  4. 2
      sandbox/OpenIddict.Sandbox.Console.Client/Worker.cs
  5. 17
      sandbox/OpenIddict.Sandbox.WinForms.Client/MainForm.Designer.cs
  6. 122
      sandbox/OpenIddict.Sandbox.WinForms.Client/MainForm.cs
  7. 1
      sandbox/OpenIddict.Sandbox.WinForms.Client/Program.cs
  8. 2
      sandbox/OpenIddict.Sandbox.WinForms.Client/Worker.cs
  9. 24
      sandbox/OpenIddict.Sandbox.Wpf.Client/MainWindow.xaml
  10. 78
      sandbox/OpenIddict.Sandbox.Wpf.Client/MainWindow.xaml.cs
  11. 1
      sandbox/OpenIddict.Sandbox.Wpf.Client/Program.cs
  12. 2
      sandbox/OpenIddict.Sandbox.Wpf.Client/Worker.cs
  13. 27
      src/OpenIddict.Abstractions/OpenIddictResources.resx
  14. 6
      src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationAuthenticationMode.cs
  15. 4
      src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationBuilder.cs
  16. 2
      src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlers.Authentication.cs
  17. 314
      src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlers.Session.cs
  18. 195
      src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlers.cs
  19. 7
      src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationMarshal.cs
  20. 2
      src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationOptions.cs
  21. 9
      src/OpenIddict.Client/OpenIddictClientEvents.Session.cs
  22. 3
      src/OpenIddict.Client/OpenIddictClientHandlers.Session.cs
  23. 18
      src/OpenIddict.Client/OpenIddictClientHandlers.cs
  24. 66
      src/OpenIddict.Client/OpenIddictClientModels.cs
  25. 87
      src/OpenIddict.Client/OpenIddictClientService.cs

17
sandbox/OpenIddict.Sandbox.AspNetCore.Server/Worker.cs

@ -59,6 +59,12 @@ public class Worker : IHostedService
{
[CultureInfo.GetCultureInfo("fr-FR")] = "Application cliente console"
},
PostLogoutRedirectUris =
{
// Note: the port must not be explicitly specified as it is selected
// dynamically at runtime by the OpenIddict client system integration.
new Uri("http://localhost/callback/logout/local")
},
RedirectUris =
{
// Note: the port must not be explicitly specified as it is selected
@ -70,6 +76,7 @@ public class Worker : IHostedService
Permissions.Endpoints.Authorization,
Permissions.Endpoints.Device,
Permissions.Endpoints.Introspection,
Permissions.Endpoints.Logout,
Permissions.Endpoints.Revocation,
Permissions.Endpoints.Token,
Permissions.GrantTypes.AuthorizationCode,
@ -162,6 +169,10 @@ public class Worker : IHostedService
{
[CultureInfo.GetCultureInfo("fr-FR")] = "Application cliente WinForms"
},
PostLogoutRedirectUris =
{
new Uri("com.openiddict.sandbox.winforms.client:/callback/logout/local")
},
RedirectUris =
{
new Uri("com.openiddict.sandbox.winforms.client:/callback/login/local")
@ -169,6 +180,7 @@ public class Worker : IHostedService
Permissions =
{
Permissions.Endpoints.Authorization,
Permissions.Endpoints.Logout,
Permissions.Endpoints.Token,
Permissions.GrantTypes.AuthorizationCode,
Permissions.GrantTypes.RefreshToken,
@ -198,6 +210,10 @@ public class Worker : IHostedService
{
[CultureInfo.GetCultureInfo("fr-FR")] = "Application cliente WPF"
},
PostLogoutRedirectUris =
{
new Uri("com.openiddict.sandbox.wpf.client:/callback/logout/local")
},
RedirectUris =
{
new Uri("com.openiddict.sandbox.wpf.client:/callback/login/local")
@ -205,6 +221,7 @@ public class Worker : IHostedService
Permissions =
{
Permissions.Endpoints.Authorization,
Permissions.Endpoints.Logout,
Permissions.Endpoints.Token,
Permissions.GrantTypes.AuthorizationCode,
Permissions.GrantTypes.RefreshToken,

43
sandbox/OpenIddict.Sandbox.Console.Client/InteractiveService.cs

@ -139,7 +139,8 @@ public class InteractiveService : BackgroundService
AnsiConsole.MarkupLine("[cyan]Waiting for the user to approve the authorization demand.[/]");
// Wait for the user to complete the authorization process.
// Wait for the user to complete the authorization process and authenticate the callback request,
// which allows resolving all the claims contained in the merged principal created by OpenIddict.
var response = await _service.AuthenticateInteractivelyAsync(new()
{
CancellationToken = stoppingToken,
@ -194,6 +195,34 @@ public class InteractiveService : BackgroundService
RefreshToken = response.RefreshToken
})).Principal));
}
// If the authorization server supports RP-initiated logout,
// ask the user if a logout operation should be started.
if (configuration.EndSessionEndpoint is not null && await LogOutAsync(stoppingToken))
{
AnsiConsole.MarkupLine("[cyan]Launching the system browser.[/]");
// Ask OpenIddict to initiate the logout flow (typically, by starting the system browser).
var nonce = (await _service.SignOutInteractivelyAsync(new()
{
CancellationToken = stoppingToken,
ProviderName = provider
})).Nonce;
AnsiConsole.MarkupLine("[cyan]Waiting for the user to approve the logout demand.[/]");
// Wait for the user to complete the logout process and authenticate the callback request.
//
// Note: in this case, only the claims contained in the state token can be resolved since
// the authorization server doesn't return any other user identity during a logout dance.
await _service.AuthenticateInteractivelyAsync(new()
{
CancellationToken = stoppingToken,
Nonce = nonce
});
AnsiConsole.MarkupLine("[green]Interactive logout successful.[/]");
}
}
}
@ -248,6 +277,18 @@ public class InteractiveService : BackgroundService
return WaitAsync(Task.Run(Prompt, cancellationToken), cancellationToken);
}
static Task<bool> LogOutAsync(CancellationToken cancellationToken)
{
static bool Prompt() => AnsiConsole.Prompt(new ConfirmationPrompt("Would you like to log out?")
{
Comparer = StringComparer.CurrentCultureIgnoreCase,
DefaultValue = false,
ShowDefaultValue = true
});
return WaitAsync(Task.Run(Prompt, cancellationToken), cancellationToken);
}
static Task<bool> RevokeAccessTokenAsync(CancellationToken cancellationToken)
{
static bool Prompt() => AnsiConsole.Prompt(new ConfirmationPrompt(

3
sandbox/OpenIddict.Sandbox.Console.Client/Program.cs

@ -71,7 +71,10 @@ var host = new HostBuilder()
ProviderDisplayName = "Local authorization server",
ClientId = "console",
PostLogoutRedirectUri = new Uri("callback/logout/local", UriKind.Relative),
RedirectUri = new Uri("callback/login/local", UriKind.Relative),
Scopes = { Scopes.Email, Scopes.Profile, Scopes.OfflineAccess, "demo_api" }
});

2
sandbox/OpenIddict.Sandbox.Console.Client/Worker.cs

@ -16,7 +16,7 @@ public class Worker : IHostedService
using var scope = _provider.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<DbContext>();
await context.Database.EnsureCreatedAsync();
await context.Database.EnsureCreatedAsync(cancellationToken);
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;

17
sandbox/OpenIddict.Sandbox.WinForms.Client/MainForm.Designer.cs

@ -31,6 +31,7 @@ partial class MainForm
this.LocalLogin = new System.Windows.Forms.Button();
this.GitHubLogin = new System.Windows.Forms.Button();
this.LocalLoginWithGitHub = new System.Windows.Forms.Button();
this.LocalLogout = new System.Windows.Forms.Button();
this.SuspendLayout();
//
// LocalLogin
@ -45,7 +46,7 @@ partial class MainForm
//
// GitHubLogin
//
this.GitHubLogin.Location = new System.Drawing.Point(214, 321);
this.GitHubLogin.Location = new System.Drawing.Point(214, 210);
this.GitHubLogin.Name = "GitHubLogin";
this.GitHubLogin.Size = new System.Drawing.Size(391, 83);
this.GitHubLogin.TabIndex = 1;
@ -55,7 +56,7 @@ partial class MainForm
//
// LocalLoginWithGitHub
//
this.LocalLoginWithGitHub.Location = new System.Drawing.Point(214, 177);
this.LocalLoginWithGitHub.Location = new System.Drawing.Point(214, 121);
this.LocalLoginWithGitHub.Name = "LocalLoginWithGitHub";
this.LocalLoginWithGitHub.Size = new System.Drawing.Size(391, 83);
this.LocalLoginWithGitHub.TabIndex = 0;
@ -63,11 +64,22 @@ partial class MainForm
this.LocalLoginWithGitHub.UseVisualStyleBackColor = true;
this.LocalLoginWithGitHub.Click += new System.EventHandler(this.LocalLoginWithGitHubButton_Click);
//
// LocalLogout
//
this.LocalLogout.Location = new System.Drawing.Point(214, 336);
this.LocalLogout.Name = "LocalLogout";
this.LocalLogout.Size = new System.Drawing.Size(391, 83);
this.LocalLogout.TabIndex = 2;
this.LocalLogout.Text = "Log out from the local server";
this.LocalLogout.UseVisualStyleBackColor = true;
this.LocalLogout.Click += new System.EventHandler(this.LocalLogoutButton_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.LocalLogout);
this.Controls.Add(this.GitHubLogin);
this.Controls.Add(this.LocalLoginWithGitHub);
this.Controls.Add(this.LocalLogin);
@ -82,4 +94,5 @@ partial class MainForm
private Button LocalLogin;
private Button GitHubLogin;
private Button LocalLoginWithGitHub;
private Button LocalLogout;
}

122
sandbox/OpenIddict.Sandbox.WinForms.Client/MainForm.cs

@ -19,22 +19,26 @@ public partial class MainForm : Form, IWinFormsShell
}
private async void LocalLoginButton_Click(object sender, EventArgs e)
=> await AuthenticateAsync("Local");
=> await LogInAsync("Local");
private async void LocalLoginWithGitHubButton_Click(object sender, EventArgs e)
=> await AuthenticateAsync("Local", new()
=> await LogInAsync("Local", new()
{
[Parameters.IdentityProvider] = Providers.GitHub
});
private async void LocalLogoutButton_Click(object sender, EventArgs e)
=> await LogOutAsync("Local");
private async void GitHubLoginButton_Click(object sender, EventArgs e)
=> await AuthenticateAsync(Providers.GitHub);
=> await LogInAsync(Providers.GitHub);
private async Task AuthenticateAsync(string provider, Dictionary<string, OpenIddictParameter>? parameters = null)
private async Task LogInAsync(string provider, Dictionary<string, OpenIddictParameter>? parameters = null)
{
// Disable the login buttons to prevent concurrent authentication operations.
// Disable the buttons to prevent concurrent operations.
LocalLogin.Enabled = false;
LocalLoginWithGitHub.Enabled = false;
LocalLogout.Enabled = false;
GitHubLogin.Enabled = false;
try
@ -123,9 +127,115 @@ public partial class MainForm : Form, IWinFormsShell
finally
{
// Re-enable the login buttons to allow starting a new authentication operation.
// Re-enable the buttons to allow starting a new operation.
LocalLogin.Enabled = true;
LocalLoginWithGitHub.Enabled = true;
LocalLogout.Enabled = true;
GitHubLogin.Enabled = true;
}
}
private async Task LogOutAsync(string provider, Dictionary<string, OpenIddictParameter>? parameters = null)
{
// Disable the buttons to prevent concurrent operations.
LocalLogin.Enabled = false;
LocalLoginWithGitHub.Enabled = false;
LocalLogout.Enabled = false;
GitHubLogin.Enabled = false;
try
{
using var source = new CancellationTokenSource(delay: TimeSpan.FromSeconds(90));
try
{
// Ask OpenIddict to initiate the logout flow (typically, by starting the system browser).
var result = await _service.SignOutInteractivelyAsync(new()
{
AdditionalLogoutRequestParameters = parameters,
CancellationToken = source.Token,
ProviderName = provider
});
// Wait for the user to complete the logout process and authenticate the callback request.
//
// Note: in this case, only the claims contained in the state token can be resolved since
// the authorization server doesn't return any other user identity during a logout dance.
await _service.AuthenticateInteractivelyAsync(new()
{
CancellationToken = source.Token,
Nonce = result.Nonce
});
#if SUPPORTS_WINFORMS_TASK_DIALOG
TaskDialog.ShowDialog(new TaskDialogPage
{
Caption = "Logout successful",
Heading = "Logout successful",
Icon = TaskDialogIcon.ShieldSuccessGreenBar,
Text = "The user was successfully logged out from the local server."
});
#else
MessageBox.Show("The user was successfully logged out from the local server.",
"Logout successful", MessageBoxButtons.OK, MessageBoxIcon.Information);
#endif
}
catch (OperationCanceledException)
{
#if SUPPORTS_WINFORMS_TASK_DIALOG
TaskDialog.ShowDialog(new TaskDialogPage
{
Caption = "Logout timed out",
Heading = "Logout timed out",
Icon = TaskDialogIcon.Warning,
Text = "The logout process was aborted."
});
#else
MessageBox.Show("The authentication process was aborted.",
"Logout 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 = "Logout denied",
Heading = "Logout denied",
Icon = TaskDialogIcon.Warning,
Text = "The logout demand was denied by the end user."
});
#else
MessageBox.Show("The logout demand was denied by the end user.",
"Logout denied", MessageBoxButtons.OK, MessageBoxIcon.Warning);
#endif
}
catch
{
#if SUPPORTS_WINFORMS_TASK_DIALOG
TaskDialog.ShowDialog(new TaskDialogPage
{
Caption = "Logout failed",
Heading = "Logout failed",
Icon = TaskDialogIcon.Error,
Text = "An error occurred while trying to log the user out."
});
#else
MessageBox.Show("An error occurred while trying to log the user out.",
"Logout failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
#endif
}
}
finally
{
// Re-enable the buttons to allow starting a new operation.
LocalLogin.Enabled = true;
LocalLoginWithGitHub.Enabled = true;
LocalLogout.Enabled = true;
GitHubLogin.Enabled = true;
}
}

1
sandbox/OpenIddict.Sandbox.WinForms.Client/Program.cs

@ -69,6 +69,7 @@ var host = new HostBuilder()
// For more information on how to construct private-use URI schemes,
// read https://www.rfc-editor.org/rfc/rfc8252#section-7.1 and
// https://www.rfc-editor.org/rfc/rfc7595#section-3.8.
PostLogoutRedirectUri = new Uri("com.openiddict.sandbox.winforms.client:/callback/logout/local", UriKind.Absolute),
RedirectUri = new Uri("com.openiddict.sandbox.winforms.client:/callback/login/local", UriKind.Absolute),
Scopes = { Scopes.Email, Scopes.Profile, Scopes.OfflineAccess, "demo_api" }

2
sandbox/OpenIddict.Sandbox.WinForms.Client/Worker.cs

@ -18,7 +18,7 @@ public class Worker : IHostedService
using var scope = _provider.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<DbContext>();
await context.Database.EnsureCreatedAsync();
await context.Database.EnsureCreatedAsync(cancellationToken);
// Create the registry entries necessary to handle URI protocol activations.
//

24
sandbox/OpenIddict.Sandbox.Wpf.Client/MainWindow.xaml

@ -4,10 +4,26 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Title="OpenIddict WPF client" Height="450" Width="800">
Title="OpenIddict WPF client" Height="500" Width="800">
<Grid>
<Button Name="LocalLogin" Content="Log in using the local server" HorizontalAlignment="Center" VerticalAlignment="Top" Click="LocalLoginButton_Click" Height="64" Width="540" FontSize="20" Margin="0,50,0,0" />
<Button Name="LocalLoginWithGitHub" Content="Log in using the local server (preferred service: GitHub)" HorizontalAlignment="Center" VerticalAlignment="Center" Click="LocalLoginWithGitHubButton_Click" Height="64" Width="539" FontSize="20" />
<Button Name="GitHubLogin" Content="Log in using GitHub" HorizontalAlignment="Center" VerticalAlignment="Bottom" Click="GitHubLoginButton_Click" Height="64" Width="539" FontSize="20" Margin="0,0,0,50" />
<Button Name="LocalLogin" Content="Log in using the local server"
VerticalAlignment="Top" HorizontalAlignment="Center"
Height="64" Width="540" FontSize="20" Margin="0,35,0,0"
Click="LocalLoginButton_Click" />
<Button Name="LocalLoginWithGitHub" Content="Log in using the local server (preferred service: GitHub)"
VerticalAlignment="Top" HorizontalAlignment="Center"
Height="64" Width="540" FontSize="20" Margin="0,125,0,0"
Click="LocalLoginWithGitHubButton_Click" />
<Button Name="GitHubLogin" Content="Log in using GitHub"
VerticalAlignment="Top" HorizontalAlignment="Center"
Height="64" Width="540" FontSize="20" Margin="0,215,0,0"
Click="GitHubLoginButton_Click" />
<Button Name="LocalLogout" Content="Log out from the local server"
VerticalAlignment="Bottom" HorizontalAlignment="Center"
Height="64" Width="540" FontSize="20" Margin="0,0,0,35"
Click="LocalLogoutButton_Click" />
</Grid>
</Window>

78
sandbox/OpenIddict.Sandbox.Wpf.Client/MainWindow.xaml.cs

@ -20,22 +20,25 @@ public partial class MainWindow : Window, IWpfShell
}
private async void LocalLoginButton_Click(object sender, RoutedEventArgs e)
=> await AuthenticateAsync("Local");
=> await LogInAsync("Local");
private async void LocalLoginWithGitHubButton_Click(object sender, RoutedEventArgs e)
=> await AuthenticateAsync("Local", new()
=> await LogInAsync("Local", new()
{
[Parameters.IdentityProvider] = Providers.GitHub
});
private async void LocalLogoutButton_Click(object sender, RoutedEventArgs e)
=> await LogOutAsync("Local");
private async void GitHubLoginButton_Click(object sender, RoutedEventArgs e)
=> await AuthenticateAsync(Providers.GitHub);
=> await LogInAsync(Providers.GitHub);
private async Task AuthenticateAsync(string provider, Dictionary<string, OpenIddictParameter>? parameters = null)
private async Task LogInAsync(string provider, Dictionary<string, OpenIddictParameter>? parameters = null)
{
// Disable the login buttons to prevent concurrent authentication operations.
// Disable the buttons to prevent concurrent operations.
LocalLogin.IsEnabled = false;
LocalLoginWithGitHub.IsEnabled = false;
LocalLogout.IsEnabled = false;
GitHubLogin.IsEnabled = false;
try
@ -52,7 +55,8 @@ public partial class MainWindow : Window, IWpfShell
ProviderName = provider
});
// Wait for the user to complete the authorization process.
// Wait for the user to complete the authorization process and authenticate the callback request,
// which allows resolving all the claims contained in the merged principal created by OpenIddict.
var principal = (await _service.AuthenticateInteractivelyAsync(new()
{
CancellationToken = source.Token,
@ -84,9 +88,69 @@ public partial class MainWindow : Window, IWpfShell
finally
{
// Re-enable the login buttons to allow starting a new authentication operation.
// Re-enable the buttons to allow starting a new operation.
LocalLogin.IsEnabled = true;
LocalLoginWithGitHub.IsEnabled = true;
LocalLogout.IsEnabled = true;
GitHubLogin.IsEnabled = true;
}
}
private async Task LogOutAsync(string provider, Dictionary<string, OpenIddictParameter>? parameters = null)
{
// Disable the buttons to prevent concurrent operations.
LocalLogin.IsEnabled = false;
LocalLoginWithGitHub.IsEnabled = false;
LocalLogout.IsEnabled = false;
GitHubLogin.IsEnabled = false;
try
{
using var source = new CancellationTokenSource(delay: TimeSpan.FromSeconds(90));
try
{
// Ask OpenIddict to initiate the logout flow (typically, by starting the system browser).
var result = await _service.SignOutInteractivelyAsync(new()
{
AdditionalLogoutRequestParameters = parameters,
CancellationToken = source.Token,
ProviderName = provider
});
// Wait for the user to complete the logout process and authenticate the callback request.
//
// Note: in this case, only the claims contained in the state token can be resolved since
// the authorization server doesn't return any other user identity during a logout dance.
await _service.AuthenticateInteractivelyAsync(new()
{
CancellationToken = source.Token,
Nonce = result.Nonce
});
MessageBox.Show($"The user was successfully logged out from the local server.",
"Logout demand successful", MessageBoxButton.OK, MessageBoxImage.Information);
}
catch (OperationCanceledException)
{
MessageBox.Show("The logout process was aborted.",
"Logout timed out", MessageBoxButton.OK, MessageBoxImage.Warning);
}
catch
{
MessageBox.Show("An error occurred while trying to log the user out.",
"Logout failed", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
finally
{
// Re-enable the buttons to allow starting a new operation.
LocalLogin.IsEnabled = true;
LocalLoginWithGitHub.IsEnabled = true;
LocalLogout.IsEnabled = true;
GitHubLogin.IsEnabled = true;
}
}

1
sandbox/OpenIddict.Sandbox.Wpf.Client/Program.cs

@ -70,6 +70,7 @@ var host = new HostBuilder()
// For more information on how to construct private-use URI schemes,
// read https://www.rfc-editor.org/rfc/rfc8252#section-7.1 and
// https://www.rfc-editor.org/rfc/rfc7595#section-3.8.
PostLogoutRedirectUri = new Uri("com.openiddict.sandbox.wpf.client:/callback/logout/local", UriKind.Absolute),
RedirectUri = new Uri("com.openiddict.sandbox.wpf.client:/callback/login/local", UriKind.Absolute),
Scopes = { Scopes.Email, Scopes.Profile, Scopes.OfflineAccess, "demo_api" }

2
sandbox/OpenIddict.Sandbox.Wpf.Client/Worker.cs

@ -18,7 +18,7 @@ public class Worker : IHostedService
using var scope = _provider.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<DbContext>();
await context.Database.EnsureCreatedAsync();
await context.Database.EnsureCreatedAsync(cancellationToken);
// Create the registry entries necessary to handle URI protocol activations.
//

27
src/OpenIddict.Abstractions/OpenIddictResources.resx

@ -1629,6 +1629,9 @@ To apply post-logout redirection responses, create a class implementing 'IOpenId
Error description: {1}
Error URI: {2}</value>
</data>
<data name="ID0434" xml:space="preserve">
<value>An error occurred while signing the user out.</value>
</data>
<data name="ID2000" xml:space="preserve">
<value>The security token is missing.</value>
</data>
@ -2074,40 +2077,40 @@ To apply post-logout redirection responses, create a class implementing 'IOpenId
<value>The userinfo request was rejected by the remote server.</value>
</data>
<data name="ID2149" xml:space="preserve">
<value>The authorization demand was denied by the user or by the identity provider.</value>
<value>The authentication demand was denied by the user or by the identity provider.</value>
</data>
<data name="ID2150" xml:space="preserve">
<value>The authorization request was rejected due to a missing or invalid parameter.</value>
<value>The authentication demand was rejected due to a missing or invalid parameter.</value>
</data>
<data name="ID2151" xml:space="preserve">
<value>The authorization request was rejected due to an invalid scope.</value>
<value>The authentication demand was rejected due to an invalid scope.</value>
</data>
<data name="ID2152" xml:space="preserve">
<value>The authorization request was rejected due to a remote server error.</value>
<value>The authentication demand was rejected due to a remote server error.</value>
</data>
<data name="ID2153" xml:space="preserve">
<value>The authorization request was rejected due to a transient error.</value>
<value>The authentication demand was rejected due to a transient error.</value>
</data>
<data name="ID2154" xml:space="preserve">
<value>The authorization request was rejected due to the use of an incorrect or unauthorized grant type.</value>
<value>The authentication demand was rejected due to the use of an incorrect or unauthorized grant type.</value>
</data>
<data name="ID2155" xml:space="preserve">
<value>The authorization request was rejected due to an unsupported response type.</value>
<value>The authentication demand was rejected due to an unsupported response type.</value>
</data>
<data name="ID2156" xml:space="preserve">
<value>The authorization request was rejected because the user didn't select a user account.</value>
<value>The authentication demand was rejected because the user didn't select a user account.</value>
</data>
<data name="ID2157" xml:space="preserve">
<value>The authorization request was rejected because user consent was required to proceed the request.</value>
<value>The authentication demand was rejected because user consent was required to proceed the request.</value>
</data>
<data name="ID2158" xml:space="preserve">
<value>The authorization request was rejected because user interaction was required to proceed the request.</value>
<value>The authentication demand was rejected because user interaction was required to proceed the request.</value>
</data>
<data name="ID2159" xml:space="preserve">
<value>The authorization request was rejected because user (re-)authentication was required to proceed the request.</value>
<value>The authentication demand was rejected because user (re-)authentication was required to proceed the request.</value>
</data>
<data name="ID2160" xml:space="preserve">
<value>The authorization request was rejected by the identity provider.</value>
<value>The authentication demand was rejected by the identity provider.</value>
</data>
<data name="ID2161" xml:space="preserve">
<value>A generic {StatusCode} error was returned by the remote authorization server.</value>

6
src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationAuthenticationMode.cs

@ -9,17 +9,17 @@ using System.Runtime.Versioning;
namespace OpenIddict.Client.SystemIntegration;
/// <summary>
/// Provides various settings needed to configure the OpenIddict client system integration.
/// Represents the authentication mode used to start interactive authentication and logout flows.
/// </summary>
public enum OpenIddictClientSystemIntegrationAuthenticationMode
{
/// <summary>
/// Browser-based authentication.
/// Browser-based authentication and logout.
/// </summary>
SystemBrowser = 0,
/// <summary>
/// Windows web authentication broker-based authentication.
/// Windows web authentication broker-based authentication and logout.
/// </summary>
/// <remarks>
/// Note: the web authentication broker is only supported in UWP applications

4
src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationBuilder.cs

@ -50,7 +50,7 @@ public sealed class OpenIddictClientSystemIntegrationBuilder
}
/// <summary>
/// Uses the Windows web authentication broker to start authentication flows.
/// Uses the Windows web authentication broker to start interactive authentication and logout flows.
/// </summary>
/// <remarks>
/// Note: the web authentication broker is only supported in UWP applications
@ -70,7 +70,7 @@ public sealed class OpenIddictClientSystemIntegrationBuilder
}
/// <summary>
/// Uses the system browser to start authentication flows.
/// Uses the system browser to start interactive authentication and logout flows.
/// </summary>
/// <returns>The <see cref="OpenIddictClientSystemIntegrationBuilder"/>.</returns>
public OpenIddictClientSystemIntegrationBuilder UseSystemBrowser()

2
src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlers.Authentication.cs

@ -107,7 +107,7 @@ public static partial class OpenIddictClientSystemIntegrationHandlers
// - The challenge, during which the user is redirected to the authorization server, either
// by launching the system browser or, as in this case, using a web-view-like approach.
//
// - The authentication that takes place after the authorization server and the user approved
// - The callback validation that takes place after the authorization server and the user approved
// the demand and redirected the user agent to the client (using either protocol activation,
// an embedded web server or by tracking the return URL of the web view created for the process).
//

314
src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlers.Session.cs

@ -0,0 +1,314 @@
/*
* 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 System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text;
using Microsoft.Extensions.Primitives;
using OpenIddict.Extensions;
#if SUPPORTS_WINDOWS_RUNTIME
using Windows.Security.Authentication.Web;
using Windows.UI.Core;
#endif
namespace OpenIddict.Client.SystemIntegration;
public static partial class OpenIddictClientSystemIntegrationHandlers
{
public static class Session
{
public static ImmutableArray<OpenIddictClientHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create([
/*
* Logout request processing:
*/
InvokeWebAuthenticationBroker.Descriptor,
LaunchSystemBrowser.Descriptor,
/*
* Post-logout redirection request extraction:
*/
ExtractGetHttpListenerRequest<ExtractPostLogoutRedirectionRequestContext>.Descriptor,
ExtractProtocolActivationParameters<ExtractPostLogoutRedirectionRequestContext>.Descriptor,
ExtractWebAuthenticationResultData<ExtractPostLogoutRedirectionRequestContext>.Descriptor,
/*
* Post-logout redirection response handling:
*/
AttachHttpResponseCode<ApplyPostLogoutRedirectionResponseContext>.Descriptor,
AttachCacheControlHeader<ApplyPostLogoutRedirectionResponseContext>.Descriptor,
ProcessEmptyHttpResponse.Descriptor,
ProcessProtocolActivationResponse<ApplyPostLogoutRedirectionResponseContext>.Descriptor,
ProcessWebAuthenticationResultResponse<ApplyPostLogoutRedirectionResponseContext>.Descriptor
]);
/// <summary>
/// Contains the logic responsible for initiating logout requests using the web authentication broker.
/// Note: this handler is not used when the user session is not interactive.
/// </summary>
public class InvokeWebAuthenticationBroker : IOpenIddictClientHandler<ApplyLogoutRequestContext>
{
private readonly OpenIddictClientSystemIntegrationService _service;
public InvokeWebAuthenticationBroker(OpenIddictClientSystemIntegrationService service)
=> _service = service ?? throw new ArgumentNullException(nameof(service));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ApplyLogoutRequestContext>()
.AddFilter<RequireInteractiveSession>()
.AddFilter<RequireWebAuthenticationBroker>()
.UseSingletonHandler<InvokeWebAuthenticationBroker>()
.SetOrder(100_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
[SupportedOSPlatform("windows10.0.17763")]
#pragma warning disable CS1998
public async ValueTask HandleAsync(ApplyLogoutRequestContext context)
#pragma warning restore CS1998
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.Transaction.Request is not null, SR.GetResourceString(SR.ID4008));
#if SUPPORTS_WINDOWS_RUNTIME
if (string.IsNullOrEmpty(context.PostLogoutRedirectUri))
{
return;
}
// Note: WebAuthenticationBroker internally requires a pointer to the CoreWindow object associated
// to the thread from which the challenge operation is started. Unfortunately, CoreWindow - and by
// extension WebAuthenticationBroker - are only supported on UWP and cannot be used in Win32 apps.
//
// To ensure a meaningful exception is returned when the web authentication broker is used with an
// incompatible application model (e.g WinUI 3.0), the presence of a CoreWindow is verified here.
//
// See https://github.com/microsoft/WindowsAppSDK/issues/398 for more information.
if (!OpenIddictClientSystemIntegrationHelpers.IsWebAuthenticationBrokerSupported() ||
CoreWindow.GetForCurrentThread() is null)
{
throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0392));
}
// OpenIddict represents the complete interactive logout dance as a two-phase process:
// - The sign-out, during which the user is redirected to the authorization server, either
// by launching the system browser or, as in this case, using a web-view-like approach.
//
// - The callback validation that takes place after the authorization server and the user approved
// the demand and redirected the user agent to the client (using either protocol activation,
// an embedded web server or by tracking the return URL of the web view created for the process).
//
// Unlike OpenIddict, WebAuthenticationBroker materializes this process as a single/one-shot API
// that opens the system-managed authentication host, navigates to the specified request URI and
// doesn't return until the specified callback URI is reached or the modal closed by the user.
// To accomodate OpenIddict's model, successful results are processed as any other callback request.
// Note: IAsyncOperation<T>.AsTask(context.CancellationToken) is deliberately not used here as
// the asynchronous operation returned by the web authentication broker is not cancellable.
switch (await WebAuthenticationBroker.AuthenticateAsync(
options : WebAuthenticationOptions.None,
requestUri : OpenIddictHelpers.AddQueryStringParameters(
uri: new Uri(context.EndSessionEndpoint, UriKind.Absolute),
parameters: context.Transaction.Request.GetParameters().ToDictionary(
parameter => parameter.Key,
parameter => new StringValues((string?[]?) parameter.Value))),
callbackUri: new Uri(context.PostLogoutRedirectUri, UriKind.Absolute)))
{
case { ResponseStatus: WebAuthenticationStatus.Success } result:
await _service.HandleWebAuthenticationResultAsync(result, context.CancellationToken);
context.HandleRequest();
return;
// Since the result of this operation is known by the time WebAuthenticationBroker.AuthenticateAsync()
// returns, some errors can directly be handled and surfaced here, as part of the challenge handling.
case { ResponseStatus: WebAuthenticationStatus.UserCancel }:
context.Reject(
error: Errors.AccessDenied,
description: SR.GetResourceString(SR.ID2149),
uri: SR.FormatID8000(SR.ID2149));
return;
case { ResponseStatus: WebAuthenticationStatus.ErrorHttp } result:
context.Reject(
error: result.ResponseErrorDetail switch
{
400 => Errors.InvalidRequest,
401 => Errors.InvalidToken,
403 => Errors.InsufficientAccess,
429 => Errors.SlowDown,
500 => Errors.ServerError,
503 => Errors.TemporarilyUnavailable,
_ => Errors.ServerError
},
description: SR.FormatID2161(result.ResponseErrorDetail),
uri: SR.FormatID8000(SR.ID2161));
return;
default:
context.Reject(
error: Errors.ServerError,
description: SR.GetResourceString(SR.ID2136),
uri: SR.FormatID8000(SR.ID2136));
return;
}
#else
throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0392));
#endif
}
}
/// <summary>
/// Contains the logic responsible for initiating logout requests using the system browser.
/// Note: this handler is not used when the user session is not interactive.
/// </summary>
public class LaunchSystemBrowser : IOpenIddictClientHandler<ApplyLogoutRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ApplyLogoutRequestContext>()
.AddFilter<RequireInteractiveSession>()
.AddFilter<RequireSystemBrowser>()
.UseSingletonHandler<LaunchSystemBrowser>()
.SetOrder(InvokeWebAuthenticationBroker.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public async ValueTask HandleAsync(ApplyLogoutRequestContext 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(
uri: new Uri(context.EndSessionEndpoint, UriKind.Absolute),
parameters: context.Transaction.Request.GetParameters().ToDictionary(
parameter => parameter.Key,
parameter => new StringValues((string?[]?) parameter.Value)));
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
// Note: on Windows, multiple application models exist and must be supported to cover most scenarios:
//
// - Classical Win32 applications, for which no application-specific restriction is enforced.
// - Win32 applications running in an AppContainer, that are very similar to UWP applications.
// - Classical UWP applications, for which strict application restrictions are enforced.
// - Full-trust UWP applications, that are rare but very similar to classical Win32 applications.
// - Modern/hybrid Windows applications, that can be sandboxed or run as full-trust applications.
//
// Since .NET Standard 2.0 support for UWP was only introduced in Windows 10 1709 (also known
// as Fall Creators Update) and OpenIddict requires Windows 10 1809 as the minimum supported
// version, Windows 8/8.1's Metro-style/universal applications are deliberately not supported.
//
// While Process.Start()/ShellExecuteEx() can typically be used without any particular restriction
// by non-sandboxed desktop applications to launch the default system browser, calling these
// APIs in sandboxed applications will result in an UnauthorizedAccessException being thrown.
//
// To avoid that, the OpenIddict host needs to determine whether the platform supports Windows
// Runtime APIs and favor the Launcher.LaunchUriAsync() API when it's offered by the platform.
#if SUPPORTS_WINDOWS_RUNTIME
if (OpenIddictClientSystemIntegrationHelpers.IsUriLauncherSupported() && await
OpenIddictClientSystemIntegrationHelpers.TryLaunchBrowserWithWindowsRuntimeAsync(uri))
{
context.HandleRequest();
return;
}
#endif
if (await OpenIddictClientSystemIntegrationHelpers.TryLaunchBrowserWithShellExecuteAsync(uri))
{
context.HandleRequest();
return;
}
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) &&
await OpenIddictClientSystemIntegrationHelpers.TryLaunchBrowserWithXdgOpenAsync(uri))
{
context.HandleRequest();
return;
}
throw new InvalidOperationException(SR.GetResourceString(SR.ID0385));
}
}
/// <summary>
/// Contains the logic responsible for processing OpenID Connect responses that don't specify any parameter.
/// Note: this handler is not used when the OpenID Connect request is not handled by the embedded web server.
/// </summary>
public sealed class ProcessEmptyHttpResponse : IOpenIddictClientHandler<ApplyPostLogoutRedirectionResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ApplyPostLogoutRedirectionResponseContext>()
.AddFilter<RequireHttpListenerContext>()
.UseSingletonHandler<ProcessEmptyHttpResponse>()
.SetOrder(int.MaxValue - 100_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public async ValueTask HandleAsync(ApplyPostLogoutRedirectionResponseContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.Transaction.Response is not null, SR.GetResourceString(SR.ID4007));
// This handler only applies to HTTP listener requests. If the HTTP context cannot be resolved,
// this may indicate that the request was incorrectly processed by another server stack.
var response = context.Transaction.GetHttpListenerContext()?.Response ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0390));
// Always return a 200 status, even for responses indicating that the authentication failed.
response.StatusCode = 200;
response.ContentType = "text/plain";
// Return a message indicating whether the sign-out process
// succeeded or failed and that will be visible by the user.
var buffer = Encoding.UTF8.GetBytes(context.Transaction.Response.Error switch
{
null or { Length: 0 } => "Logout completed. Please return to the application.",
Errors.AccessDenied => "Logout denied. Please return to the application.",
_ => "Logout failed. Please return to the application."
});
#if SUPPORTS_STREAM_MEMORY_METHODS
await response.OutputStream.WriteAsync(buffer);
#else
await response.OutputStream.WriteAsync(buffer, 0, buffer.Length);
#endif
await response.OutputStream.FlushAsync();
context.HandleRequest();
}
}
}
}

195
src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlers.cs

@ -77,12 +77,21 @@ public static partial class OpenIddictClientSystemIntegrationHandlers
AttachInstanceIdentifier.Descriptor,
TrackAuthenticationOperation.Descriptor,
/*
* Sign-out processing:
*/
InferLogoutBaseUriFromClientUri.Descriptor,
AttachDynamicPortToPostLogoutRedirectUri.Descriptor,
AttachLogoutInstanceIdentifier.Descriptor,
TrackLogoutOperation.Descriptor,
/*
* Error processing:
*/
AbortAuthenticationDemand.Descriptor,
.. Authentication.DefaultHandlers
.. Authentication.DefaultHandlers,
.. Session.DefaultHandlers
]);
/// <summary>
@ -549,7 +558,7 @@ public static partial class OpenIddictClientSystemIntegrationHandlers
}
// Allow a single authentication operation at the same time with the same nonce.
if (!_marshal.TryAcquireLock(context.Nonce))
if (!await _marshal.TryAcquireLockAsync(context.Nonce, context.CancellationToken))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0379));
}
@ -902,8 +911,9 @@ public static partial class OpenIddictClientSystemIntegrationHandlers
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireRedirectionRequest>()
.AddFilter<RequireAuthenticationNonce>()
.AddFilter<RequireStateTokenPrincipal>()
.AddFilter<RequireStateTokenValidated>()
.UseSingletonHandler<ResolveRequestForgeryProtection>()
.SetOrder(ValidateRequestForgeryProtection.Descriptor.Order - 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
@ -1468,8 +1478,9 @@ public static partial class OpenIddictClientSystemIntegrationHandlers
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireRedirectionRequest>()
.AddFilter<RequireAuthenticationNonce>()
.AddFilter<RequireStateTokenPrincipal>()
.AddFilter<RequireStateTokenValidated>()
.UseSingletonHandler<CompleteAuthenticationOperation>()
.SetOrder(int.MaxValue - 50_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
@ -1715,6 +1726,182 @@ public static partial class OpenIddictClientSystemIntegrationHandlers
}
}
/// <summary>
/// Contains the logic responsible for inferring the base URI from the client URI set in the options.
/// Note: this handler is not used when the user session is not interactive.
/// </summary>
public sealed class InferLogoutBaseUriFromClientUri : IOpenIddictClientHandler<ProcessSignOutContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessSignOutContext>()
.AddFilter<RequireInteractiveSession>()
.UseSingletonHandler<InferLogoutBaseUriFromClientUri>()
.SetOrder(ValidateSignOutDemand.Descriptor.Order + 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessSignOutContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
context.BaseUri ??= context.Options.ClientUri;
return default;
}
}
/// <summary>
/// Contains the logic responsible for attaching the listening port of the
/// embedded web server to the post_logout_redirect_uri, if applicable.
/// Note: this handler is not used when the user session is not interactive.
/// </summary>
public sealed class AttachDynamicPortToPostLogoutRedirectUri : IOpenIddictClientHandler<ProcessSignOutContext>
{
private readonly OpenIddictClientSystemIntegrationHttpListener _listener;
public AttachDynamicPortToPostLogoutRedirectUri(OpenIddictClientSystemIntegrationHttpListener listener)
=> _listener = listener ?? throw new ArgumentNullException(nameof(listener));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessSignOutContext>()
.AddFilter<RequireInteractiveSession>()
.UseSingletonHandler<AttachDynamicPortToPostLogoutRedirectUri>()
.SetOrder(AttachPostLogoutRedirectUri.Descriptor.Order + 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public async ValueTask HandleAsync(ProcessSignOutContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
// If the post_logout_redirect_uri uses a loopback host/IP as the authority and doesn't include a non-default port,
// determine whether the embedded web server is running: if so, override the port in the post_logout_redirect_uri
// by the port used by the embedded web server (guaranteed to be running if a value is returned).
if (!string.IsNullOrEmpty(context.PostLogoutRedirectUri) &&
Uri.TryCreate(context.PostLogoutRedirectUri, UriKind.Absolute, out Uri? uri) &&
string.Equals(uri.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) &&
uri.IsLoopback && uri.IsDefaultPort &&
await _listener.GetEmbeddedServerPortAsync(context.CancellationToken) is int port)
{
var builder = new UriBuilder(context.PostLogoutRedirectUri)
{
Port = port
};
context.PostLogoutRedirectUri = builder.Uri.AbsoluteUri;
}
}
}
/// <summary>
/// Contains the logic responsible for storing the identifier of the current instance in the state token.
/// Note: this handler is not used when the user session is not interactive.
/// </summary>
public sealed class AttachLogoutInstanceIdentifier : IOpenIddictClientHandler<ProcessSignOutContext>
{
private readonly IOptionsMonitor<OpenIddictClientSystemIntegrationOptions> _options;
public AttachLogoutInstanceIdentifier(IOptionsMonitor<OpenIddictClientSystemIntegrationOptions> options)
=> _options = options ?? throw new ArgumentNullException(nameof(options));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessSignOutContext>()
.AddFilter<RequireInteractiveSession>()
.AddFilter<RequireLogoutStateTokenGenerated>()
.UseSingletonHandler<AttachLogoutInstanceIdentifier>()
.SetOrder(PrepareLogoutStateTokenPrincipal.Descriptor.Order + 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessSignOutContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.StateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
// Most applications (except Windows UWP applications) are multi-instanced. As such, any protocol activation
// triggered by launching one of the URI schemes associated with the application will create a new instance,
// different from the one that initially started the logout flow. To deal with that without having to share
// persistent state between instances, OpenIddict stores the identifier of the instance that starts the
// logout process and uses it when handling the callback to determine whether the protocol activation
// should be redirected to a different instance using inter-process communication.
context.StateTokenPrincipal.SetClaim(Claims.Private.InstanceId, _options.CurrentValue.InstanceIdentifier);
return default;
}
}
/// <summary>
/// Contains the logic responsible for asking the marshal to track the logout operation.
/// Note: this handler is not used when the user session is not interactive.
/// </summary>
public sealed class TrackLogoutOperation : IOpenIddictClientHandler<ProcessSignOutContext>
{
private readonly OpenIddictClientSystemIntegrationMarshal _marshal;
public TrackLogoutOperation(OpenIddictClientSystemIntegrationMarshal marshal)
=> _marshal = marshal ?? throw new ArgumentNullException(nameof(marshal));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessSignOutContext>()
.AddFilter<RequireInteractiveSession>()
.AddFilter<RequireLogoutStateTokenGenerated>()
.UseSingletonHandler<TrackLogoutOperation>()
.SetOrder(100_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessSignOutContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
if (string.IsNullOrEmpty(context.Nonce))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0352));
}
if (string.IsNullOrEmpty(context.RequestForgeryProtection))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0343));
}
if (!_marshal.TryAdd(context.Nonce, context.RequestForgeryProtection))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0378));
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for informing the authentication service the demand is aborted.
/// </summary>

7
src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationMarshal.cs

@ -44,9 +44,12 @@ public sealed class OpenIddictClientSystemIntegrationMarshal
/// 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>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</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);
/// <exception cref="OperationCanceledException">The operation was canceled by the user.</exception>
internal async Task<bool> TryAcquireLockAsync(string nonce, CancellationToken cancellationToken)
=> _operations.TryGetValue(nonce, out var operation) &&
await operation.Value.Semaphore.WaitAsync(TimeSpan.Zero, cancellationToken);
/// <summary>
/// Tries to resolve the authentication context associated with the specified nonce.

2
src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationOptions.cs

@ -15,7 +15,7 @@ namespace OpenIddict.Client.SystemIntegration;
public sealed class OpenIddictClientSystemIntegrationOptions
{
/// <summary>
/// Gets or sets the authentication mode used to start authentication flows.
/// Gets or sets the authentication mode used to start interactive authentication and logout flows..
/// </summary>
/// <remarks>
/// If this property is not explicitly set, its value is automatically set by OpenIddict.

9
src/OpenIddict.Client/OpenIddictClientEvents.Session.cs

@ -62,6 +62,15 @@ public static partial class OpenIddictClientEvents
set => Transaction.Request = value;
}
/// <summary>
/// Gets or sets the post-logout redirect URI that was
/// selected during the sign-out demand, if available.
/// </summary>
public string? PostLogoutRedirectUri { get; set; }
/// <summary>
/// Gets or sets the URI of the remote end session endpoint.
/// </summary>
public string EndSessionEndpoint { get; set; } = null!;
}

3
src/OpenIddict.Client/OpenIddictClientHandlers.Session.cs

@ -115,6 +115,7 @@ public static partial class OpenIddictClientHandlers
{
// Note: the endpoint URI is automatically set by a specialized handler if it's not set here.
EndSessionEndpoint = context.EndSessionEndpoint?.AbsoluteUri!,
PostLogoutRedirectUri = context.PostLogoutRedirectUri
};
await _dispatcher.DispatchAsync(notification);
@ -353,7 +354,7 @@ public static partial class OpenIddictClientHandlers
return;
}
throw new InvalidOperationException(SR.GetResourceString(SR.ID0370));
context.Transaction.Response = new OpenIddictResponse();
}
}

18
src/OpenIddict.Client/OpenIddictClientHandlers.cs

@ -1220,7 +1220,7 @@ public static partial class OpenIddictClientHandlers
}
/// <summary>
/// Contains the logic responsible for rejecting errored authorization responses.
/// Contains the logic responsible for rejecting authentication demands containing frontchannel errors.
/// </summary>
public sealed class HandleFrontchannelErrorResponse : IOpenIddictClientHandler<ProcessAuthenticationContext>
{
@ -1229,7 +1229,6 @@ public static partial class OpenIddictClientHandlers
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireRedirectionRequest>()
.UseSingletonHandler<HandleFrontchannelErrorResponse>()
.SetOrder(ValidateIssuerParameter.Descriptor.Order + 1_000)
.Build();
@ -1242,6 +1241,20 @@ public static partial class OpenIddictClientHandlers
throw new ArgumentNullException(nameof(context));
}
// Note: unlike the redirection endpoint, the post-logout redirection endpoint is not expected
// to be called with an error attached (in this case, the error is typically displayed directly
// by the authorization server). That said, some implementations are known to allow redirecting
// the user to the post-logout redirection URI with error details attached as a non-standard
// extension. To support this scenario, the error details are extracted and validated for both
// the redirection and post-logout redirection endpoints.
//
// See https://openid.net/specs/openid-connect-rpinitiated-1_0.html for more information.
if (context.EndpointType is not (OpenIddictClientEndpointType.PostLogoutRedirection or
OpenIddictClientEndpointType.Redirection))
{
return default;
}
// Note: for more information about the standard error codes,
// see https://www.rfc-editor.org/rfc/rfc6749#section-4.1.2.1 and
// https://openid.net/specs/openid-connect-core-1_0.html#AuthError.
@ -1315,7 +1328,6 @@ public static partial class OpenIddictClientHandlers
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireRedirectionRequest>()
.AddFilter<RequireStateTokenPrincipal>()
.AddFilter<RequireStateTokenValidated>()
.UseSingletonHandler<ResolveGrantTypeAndResponseTypeFromStateToken>()

66
src/OpenIddict.Client/OpenIddictClientModels.cs

@ -193,6 +193,72 @@ public static class OpenIddictClientModels
public required Dictionary<string, string?> Properties { get; init; }
}
/// <summary>
/// Represents an interactive sign-out request.
/// </summary>
public sealed record class InteractiveSignOutRequest
{
/// <summary>
/// Gets or sets the parameters that will be added to the logout request.
/// </summary>
public Dictionary<string, OpenIddictParameter>? AdditionalLogoutRequestParameters { get; init; }
/// <summary>
/// Gets or sets the cancellation token that will be
/// used to determine if the operation was aborted.
/// </summary>
public CancellationToken CancellationToken { get; init; }
/// <summary>
/// Gets or sets the application-specific properties that will be added to the context.
/// </summary>
public Dictionary<string, string?>? Properties { get; init; }
/// <summary>
/// Gets or sets the provider name used to resolve the client registration.
/// </summary>
/// <remarks>
/// Note: if multiple client registrations use the same provider name.
/// the <see cref="RegistrationId"/> property must be explicitly set.
/// </remarks>
public string? ProviderName { get; init; }
/// <summary>
/// Gets or sets the unique identifier of the client registration that will be used.
/// </summary>
public string? RegistrationId { get; init; }
/// <summary>
/// Gets the scopes that will be sent to the authorization server.
/// </summary>
public List<string>? Scopes { get; init; }
/// <summary>
/// Gets or sets the issuer used to resolve the client registration.
/// </summary>
/// <remarks>
/// Note: if multiple client registrations point to the same issuer,
/// the <see cref="RegistrationId"/> property must be explicitly set.
/// </remarks>
public Uri? Issuer { get; init; }
}
/// <summary>
/// Represents an interactive sign-out result.
/// </summary>
public sealed record class InteractiveSignOutResult
{
/// <summary>
/// Gets or sets the nonce that is used as a unique identifier for the sign-out operation.
/// </summary>
public required string Nonce { get; init; }
/// <summary>
/// Gets or sets the application-specific properties that were present in the context.
/// </summary>
public required Dictionary<string, string?> Properties { get; init; }
}
/// <summary>
/// Represents a client credentials authentication request.
/// </summary>

87
src/OpenIddict.Client/OpenIddictClientService.cs

@ -262,6 +262,11 @@ public class OpenIddictClientService
/// <summary>
/// Completes the interactive authentication demand corresponding to the specified nonce.
/// </summary>
/// <remarks>
/// Note: when specifying a nonce returned during a sign-out operation, only the
/// claims contained in the state token can be resolved since the authorization
/// server typically doesn't return any other user identity during a sign-out dance.
/// </remarks>
/// <param name="request">The interactive authentication request.</param>
/// <returns>The interactive authentication result.</returns>
public async ValueTask<InteractiveAuthenticationResult> AuthenticateInteractivelyAsync(InteractiveAuthenticationRequest request)
@ -1256,6 +1261,88 @@ public class OpenIddictClientService
}
}
/// <summary>
/// Initiates an interactive user sign-out demand.
/// </summary>
/// <param name="request">The interactive sign-out request.</param>
/// <returns>The interactive sign-out result.</returns>
public async ValueTask<InteractiveSignOutResult> SignOutInteractivelyAsync(InteractiveSignOutRequest request)
{
if (request is null)
{
throw new ArgumentNullException(nameof(request));
}
request.CancellationToken.ThrowIfCancellationRequested();
// Note: this service is registered as a singleton service. As such, it cannot
// directly depend on scoped services like the validation provider. To work around
// this limitation, a scope is manually created for each method to this service.
var scope = _provider.CreateScope();
// Note: a try/finally block is deliberately used here to ensure the service scope
// can be disposed of asynchronously if it implements IAsyncDisposable.
try
{
var dispatcher = scope.ServiceProvider.GetRequiredService<IOpenIddictClientDispatcher>();
var factory = scope.ServiceProvider.GetRequiredService<IOpenIddictClientFactory>();
var transaction = await factory.CreateTransactionAsync();
var context = new ProcessSignOutContext(transaction)
{
CancellationToken = request.CancellationToken,
Issuer = request.Issuer,
Principal = new ClaimsPrincipal(new ClaimsIdentity()),
ProviderName = request.ProviderName,
RegistrationId = request.RegistrationId,
Request = request.AdditionalLogoutRequestParameters
is Dictionary<string, OpenIddictParameter> parameters ? new(parameters) : new(),
};
if (request.Properties is { Count: > 0 })
{
foreach (var property in request.Properties)
{
context.Properties[property.Key] = property.Value;
}
}
await dispatcher.DispatchAsync(context);
if (context.IsRejected)
{
throw new ProtocolException(
message: SR.GetResourceString(SR.ID0434),
context.Error, context.ErrorDescription, context.ErrorUri);
}
if (string.IsNullOrEmpty(context.Nonce))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0352));
}
return new()
{
Nonce = context.Nonce,
Properties = context.Properties
};
}
finally
{
if (scope is IAsyncDisposable disposable)
{
await disposable.DisposeAsync();
}
else
{
scope.Dispose();
}
}
}
/// <summary>
/// Retrieves the security keys exposed by the specified JWKS endpoint.
/// </summary>

Loading…
Cancel
Save