diff --git a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Worker.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Worker.cs index 5ec23f5b..53250bc4 100644 --- a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Worker.cs +++ b/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, diff --git a/sandbox/OpenIddict.Sandbox.Console.Client/InteractiveService.cs b/sandbox/OpenIddict.Sandbox.Console.Client/InteractiveService.cs index 06b5a97c..ca2e7c9e 100644 --- a/sandbox/OpenIddict.Sandbox.Console.Client/InteractiveService.cs +++ b/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 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 RevokeAccessTokenAsync(CancellationToken cancellationToken) { static bool Prompt() => AnsiConsole.Prompt(new ConfirmationPrompt( diff --git a/sandbox/OpenIddict.Sandbox.Console.Client/Program.cs b/sandbox/OpenIddict.Sandbox.Console.Client/Program.cs index 099c67c1..40290148 100644 --- a/sandbox/OpenIddict.Sandbox.Console.Client/Program.cs +++ b/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" } }); diff --git a/sandbox/OpenIddict.Sandbox.Console.Client/Worker.cs b/sandbox/OpenIddict.Sandbox.Console.Client/Worker.cs index 304deab8..fc054ee5 100644 --- a/sandbox/OpenIddict.Sandbox.Console.Client/Worker.cs +++ b/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(); - await context.Database.EnsureCreatedAsync(); + await context.Database.EnsureCreatedAsync(cancellationToken); } public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; diff --git a/sandbox/OpenIddict.Sandbox.WinForms.Client/MainForm.Designer.cs b/sandbox/OpenIddict.Sandbox.WinForms.Client/MainForm.Designer.cs index 39176684..66bb12c4 100644 --- a/sandbox/OpenIddict.Sandbox.WinForms.Client/MainForm.Designer.cs +++ b/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; } \ No newline at end of file diff --git a/sandbox/OpenIddict.Sandbox.WinForms.Client/MainForm.cs b/sandbox/OpenIddict.Sandbox.WinForms.Client/MainForm.cs index bedf6d0b..b63c7198 100644 --- a/sandbox/OpenIddict.Sandbox.WinForms.Client/MainForm.cs +++ b/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? parameters = null) + private async Task LogInAsync(string provider, Dictionary? 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? 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; } } diff --git a/sandbox/OpenIddict.Sandbox.WinForms.Client/Program.cs b/sandbox/OpenIddict.Sandbox.WinForms.Client/Program.cs index 93a0e85e..82010f43 100644 --- a/sandbox/OpenIddict.Sandbox.WinForms.Client/Program.cs +++ b/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" } diff --git a/sandbox/OpenIddict.Sandbox.WinForms.Client/Worker.cs b/sandbox/OpenIddict.Sandbox.WinForms.Client/Worker.cs index 958395bf..10aed3f9 100644 --- a/sandbox/OpenIddict.Sandbox.WinForms.Client/Worker.cs +++ b/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(); - await context.Database.EnsureCreatedAsync(); + await context.Database.EnsureCreatedAsync(cancellationToken); // Create the registry entries necessary to handle URI protocol activations. // diff --git a/sandbox/OpenIddict.Sandbox.Wpf.Client/MainWindow.xaml b/sandbox/OpenIddict.Sandbox.Wpf.Client/MainWindow.xaml index 105046f3..a92468f7 100644 --- a/sandbox/OpenIddict.Sandbox.Wpf.Client/MainWindow.xaml +++ b/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"> -