From c3bb97aa8d975830ccad3c515b407f477151b4ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Sun, 3 Mar 2024 16:49:10 +0100 Subject: [PATCH] Update the console sandbox to support testing the client credentials and resource owner password credentials grants --- .../InteractiveService.cs | 160 ++++++++++++++---- .../Program.cs | 7 +- .../OpenIddictResources.resx | 11 +- .../OpenIddictClientService.cs | 12 +- 4 files changed, 145 insertions(+), 45 deletions(-) diff --git a/sandbox/OpenIddict.Sandbox.Console.Client/InteractiveService.cs b/sandbox/OpenIddict.Sandbox.Console.Client/InteractiveService.cs index ca2e7c9e..56edbf97 100644 --- a/sandbox/OpenIddict.Sandbox.Console.Client/InteractiveService.cs +++ b/sandbox/OpenIddict.Sandbox.Console.Client/InteractiveService.cs @@ -39,12 +39,41 @@ public class InteractiveService : BackgroundService try { - // Resolve the server configuration and determine the type of flow - // to use depending on the supported grants and the user selection. - var configuration = await _service.GetServerConfigurationByProviderNameAsync(provider, stoppingToken); - if (configuration.GrantTypesSupported.Contains(GrantTypes.DeviceCode) && - configuration.DeviceAuthorizationEndpoint is not null && - await UseDeviceAuthorizationGrantAsync(stoppingToken)) + var type = await GetSelectedGrantTypeAsync(provider, stoppingToken); + if (type is GrantTypes.ClientCredentials) + { + AnsiConsole.MarkupLine("[cyan]Sending the token request.[/]"); + + // Ask OpenIddict to authenticate the client application using the client credentials grant. + await _service.AuthenticateWithClientCredentialsAsync(new() + { + CancellationToken = stoppingToken, + ProviderName = provider + }); + + AnsiConsole.MarkupLine("[green]Client credentials authentication successful.[/]"); + } + + else if (type is GrantTypes.Password) + { + var (username, password) = (await GetUsernameAsync(stoppingToken), await GetPasswordAsync(stoppingToken)); + + AnsiConsole.MarkupLine("[cyan]Sending the token request.[/]"); + + // Ask OpenIddict to authenticate the user using the resource owner password credentials grant. + var response = await _service.AuthenticateWithPasswordAsync(new() + { + CancellationToken = stoppingToken, + ProviderName = provider, + Username = username, + Password = password + }); + + AnsiConsole.MarkupLine("[green]Resource owner password credentials authentication successful:[/]"); + AnsiConsole.Write(CreateClaimTable(response.Principal)); + } + + else if (type is GrantTypes.DeviceCode) { // Ask OpenIddict to send a device authorization request and write // the complete verification endpoint URI to the console output. @@ -86,6 +115,7 @@ public class InteractiveService : BackgroundService AnsiConsole.Write(CreateClaimTable(response.Principal)); // If introspection is supported by the server, ask the user if the access token should be introspected. + var configuration = await _service.GetServerConfigurationByProviderNameAsync(provider, stoppingToken); if (configuration.IntrospectionEndpoint is not null && await IntrospectAccessTokenAsync(stoppingToken)) { AnsiConsole.MarkupLine("[steelblue]Claims extracted from the token introspection response:[/]"); @@ -114,7 +144,7 @@ public class InteractiveService : BackgroundService // If a refresh token was returned by the authorization server, ask the user // if the access token should be refreshed using the refresh_token grant. - if (!string.IsNullOrEmpty(response.RefreshToken) && await UseRefreshTokenGrantAsync(stoppingToken)) + if (!string.IsNullOrEmpty(response.RefreshToken) && await RefreshTokenAsync(stoppingToken)) { AnsiConsole.MarkupLine("[steelblue]Claims extracted from the refreshed identity:[/]"); AnsiConsole.Write(CreateClaimTable((await _service.AuthenticateWithRefreshTokenAsync(new() @@ -126,7 +156,7 @@ public class InteractiveService : BackgroundService } } - else + else if (type is GrantTypes.AuthorizationCode) { AnsiConsole.MarkupLine("[cyan]Launching the system browser.[/]"); @@ -152,6 +182,7 @@ public class InteractiveService : BackgroundService // If an access token was returned by the authorization server and introspection is // supported by the server, ask the user if the access token should be introspected. + var configuration = await _service.GetServerConfigurationByProviderNameAsync(provider, stoppingToken); if (!string.IsNullOrEmpty(response.BackchannelAccessToken) && configuration.IntrospectionEndpoint is not null && await IntrospectAccessTokenAsync(stoppingToken)) @@ -185,7 +216,7 @@ public class InteractiveService : BackgroundService // If a refresh token was returned by the authorization server, ask the user // if the access token should be refreshed using the refresh_token grant. - if (!string.IsNullOrEmpty(response.RefreshToken) && await UseRefreshTokenGrantAsync(stoppingToken)) + if (!string.IsNullOrEmpty(response.RefreshToken) && await RefreshTokenAsync(stoppingToken)) { AnsiConsole.MarkupLine("[steelblue]Claims extracted from the refreshed identity:[/]"); AnsiConsole.Write(CreateClaimTable((await _service.AuthenticateWithRefreshTokenAsync(new() @@ -264,35 +295,93 @@ public class InteractiveService : BackgroundService return table; } - static Task IntrospectAccessTokenAsync(CancellationToken cancellationToken) + Task GetSelectedProviderAsync(CancellationToken cancellationToken) { - static bool Prompt() => AnsiConsole.Prompt(new ConfirmationPrompt( - "Would you like to introspect the access token?") + async Task PromptAsync() => AnsiConsole.Prompt(new SelectionPrompt() + .Title("Select the authentication provider you'd like to log in with.") + .AddChoices(from registration in await _service.GetClientRegistrationsAsync(stoppingToken) + where !string.IsNullOrEmpty(registration.ProviderName) + where !string.IsNullOrEmpty(registration.ProviderDisplayName) + select registration) + .UseConverter(registration => registration.ProviderDisplayName!)).ProviderName!; + + return WaitAsync(Task.Run(PromptAsync, cancellationToken), cancellationToken); + } + + Task GetSelectedGrantTypeAsync(string provider, CancellationToken cancellationToken) + { + async Task PromptAsync() { - Comparer = StringComparer.CurrentCultureIgnoreCase, - DefaultValue = false, - ShowDefaultValue = true + List<(string GrantType, string DisplayName)> choices = []; + + var configuration = await _service.GetServerConfigurationByProviderNameAsync(provider, stoppingToken); + if (configuration.GrantTypesSupported.Contains(GrantTypes.AuthorizationCode) && + configuration.AuthorizationEndpoint is not null && + configuration.TokenEndpoint is not null) + { + choices.Add((GrantTypes.AuthorizationCode, "Authorization code grant")); + } + + if (configuration.GrantTypesSupported.Contains(GrantTypes.DeviceCode) && + configuration.DeviceAuthorizationEndpoint is not null && + configuration.TokenEndpoint is not null) + { + choices.Add((GrantTypes.DeviceCode, "Device authorization code grant")); + } + + if (configuration.GrantTypesSupported.Contains(GrantTypes.Password) && + configuration.TokenEndpoint is not null) + { + choices.Add((GrantTypes.Password, "Resource owner password credentials grant")); + } + + if (configuration.GrantTypesSupported.Contains(GrantTypes.ClientCredentials) && + configuration.TokenEndpoint is not null) + { + choices.Add((GrantTypes.ClientCredentials, "Client credentials grant (application authentication only)")); + } + + if (choices.Count is 1) + { + return choices[0].GrantType; + } + + return AnsiConsole.Prompt(new SelectionPrompt<(string GrantType, string DisplayName)>() + .Title("Select the grant type you'd like to use.") + .AddChoices(choices) + .UseConverter(choice => choice.DisplayName)).GrantType; + } + + return WaitAsync(Task.Run(PromptAsync, cancellationToken), cancellationToken); + } + + Task GetUsernameAsync(CancellationToken cancellationToken) + { + static string Prompt() => AnsiConsole.Prompt(new TextPrompt("Please enter your username:") + { + AllowEmpty = false, + IsSecret = false }); return WaitAsync(Task.Run(Prompt, cancellationToken), cancellationToken); } - static Task LogOutAsync(CancellationToken cancellationToken) + Task GetPasswordAsync(CancellationToken cancellationToken) { - static bool Prompt() => AnsiConsole.Prompt(new ConfirmationPrompt("Would you like to log out?") + static string Prompt() => AnsiConsole.Prompt(new TextPrompt("Please enter your password:") { - Comparer = StringComparer.CurrentCultureIgnoreCase, - DefaultValue = false, - ShowDefaultValue = true + AllowEmpty = false, + IsSecret = true }); return WaitAsync(Task.Run(Prompt, cancellationToken), cancellationToken); } - static Task RevokeAccessTokenAsync(CancellationToken cancellationToken) + + static Task RefreshTokenAsync(CancellationToken cancellationToken) { static bool Prompt() => AnsiConsole.Prompt(new ConfirmationPrompt( - "Would you like to revoke the access token?") + "Would you like to refresh the user authentication using the refresh token grant?") { Comparer = StringComparer.CurrentCultureIgnoreCase, DefaultValue = false, @@ -302,10 +391,10 @@ public class InteractiveService : BackgroundService return WaitAsync(Task.Run(Prompt, cancellationToken), cancellationToken); } - static Task UseDeviceAuthorizationGrantAsync(CancellationToken cancellationToken) +static Task IntrospectAccessTokenAsync(CancellationToken cancellationToken) { static bool Prompt() => AnsiConsole.Prompt(new ConfirmationPrompt( - "Would you like to authenticate using the device authorization grant?") + "Would you like to introspect the access token?") { Comparer = StringComparer.CurrentCultureIgnoreCase, DefaultValue = false, @@ -315,10 +404,9 @@ public class InteractiveService : BackgroundService return WaitAsync(Task.Run(Prompt, cancellationToken), cancellationToken); } - static Task UseRefreshTokenGrantAsync(CancellationToken cancellationToken) + static Task LogOutAsync(CancellationToken cancellationToken) { - static bool Prompt() => AnsiConsole.Prompt(new ConfirmationPrompt( - "Would you like to refresh the user authentication using the refresh token grant?") + static bool Prompt() => AnsiConsole.Prompt(new ConfirmationPrompt("Would you like to log out?") { Comparer = StringComparer.CurrentCultureIgnoreCase, DefaultValue = false, @@ -328,17 +416,17 @@ public class InteractiveService : BackgroundService return WaitAsync(Task.Run(Prompt, cancellationToken), cancellationToken); } - Task GetSelectedProviderAsync(CancellationToken cancellationToken) + static Task RevokeAccessTokenAsync(CancellationToken cancellationToken) { - async Task PromptAsync() => AnsiConsole.Prompt(new SelectionPrompt() - .Title("Select the authentication provider you'd like to log in with.") - .AddChoices(from registration in await _service.GetClientRegistrationsAsync(stoppingToken) - where !string.IsNullOrEmpty(registration.ProviderName) - where !string.IsNullOrEmpty(registration.ProviderDisplayName) - select registration) - .UseConverter(registration => registration.ProviderDisplayName!)).ProviderName!; + static bool Prompt() => AnsiConsole.Prompt(new ConfirmationPrompt( + "Would you like to revoke the access token?") + { + Comparer = StringComparer.CurrentCultureIgnoreCase, + DefaultValue = false, + ShowDefaultValue = true + }); - return WaitAsync(Task.Run(PromptAsync, cancellationToken), cancellationToken); + return WaitAsync(Task.Run(Prompt, cancellationToken), cancellationToken); } static async Task WaitAsync(Task task, CancellationToken cancellationToken) diff --git a/sandbox/OpenIddict.Sandbox.Console.Client/Program.cs b/sandbox/OpenIddict.Sandbox.Console.Client/Program.cs index 40290148..34489241 100644 --- a/sandbox/OpenIddict.Sandbox.Console.Client/Program.cs +++ b/sandbox/OpenIddict.Sandbox.Console.Client/Program.cs @@ -36,10 +36,13 @@ var host = new HostBuilder() // Register the OpenIddict client components. .AddClient(options => { - // Note: this sample uses the authorization code, device authorization code - // and refresh token flows, but you can enable the other flows if necessary. + // Note: this sample uses the authorization code, client credentials, + // device authorization code, refresh token and resource owner password + // credentials flows, but you can enable the other flows if necessary. options.AllowAuthorizationCodeFlow() + .AllowClientCredentialsFlow() .AllowDeviceCodeFlow() + .AllowPasswordFlow() .AllowRefreshTokenFlow(); // Register the signing and encryption credentials used to protect diff --git a/src/OpenIddict.Abstractions/OpenIddictResources.resx b/src/OpenIddict.Abstractions/OpenIddictResources.resx index dffe707f..116e106b 100644 --- a/src/OpenIddict.Abstractions/OpenIddictResources.resx +++ b/src/OpenIddict.Abstractions/OpenIddictResources.resx @@ -1420,7 +1420,10 @@ To apply post-logout redirection responses, create a class implementing 'IOpenId Only instances of type '{0}' can be used as primary HTTP handlers for the HTTP clients managed by OpenIddict. - An error occurred while authenticating the user. + An error occurred while authenticating the user. + Error: {0} + Error description: {1} + Error URI: {2} The protocol activation cannot be resolved or contains invalid data. @@ -1632,6 +1635,12 @@ To apply post-logout redirection responses, create a class implementing 'IOpenId An error occurred while signing the user out. + + An error occurred while authenticating the client application. + Error: {0} + Error description: {1} + Error URI: {2} + The security token is missing. diff --git a/src/OpenIddict.Client/OpenIddictClientService.cs b/src/OpenIddict.Client/OpenIddictClientService.cs index 47f2a567..b73086c5 100644 --- a/src/OpenIddict.Client/OpenIddictClientService.cs +++ b/src/OpenIddict.Client/OpenIddictClientService.cs @@ -311,7 +311,7 @@ public class OpenIddictClientService if (context.IsRejected) { throw new ProtocolException( - message: SR.GetResourceString(SR.ID0374), + SR.FormatID0374(context.Error, context.ErrorDescription, context.ErrorUri), context.Error, context.ErrorDescription, context.ErrorUri); } @@ -412,7 +412,7 @@ public class OpenIddictClientService if (context.IsRejected) { throw new ProtocolException( - message: SR.GetResourceString(SR.ID0374), + SR.FormatID0374(context.Error, context.ErrorDescription, context.ErrorUri), context.Error, context.ErrorDescription, context.ErrorUri); } @@ -499,7 +499,7 @@ public class OpenIddictClientService if (context.IsRejected) { throw new ProtocolException( - SR.FormatID0319(context.Error, context.ErrorDescription, context.ErrorUri), + SR.FormatID0435(context.Error, context.ErrorDescription, context.ErrorUri), context.Error, context.ErrorDescription, context.ErrorUri); } @@ -604,7 +604,7 @@ public class OpenIddictClientService if (context.IsRejected) { throw new ProtocolException( - message: SR.GetResourceString(SR.ID0374), + SR.FormatID0374(context.Error, context.ErrorDescription, context.ErrorUri), context.Error, context.ErrorDescription, context.ErrorUri); } @@ -718,7 +718,7 @@ public class OpenIddictClientService if (context.IsRejected) { throw new ProtocolException( - message: SR.GetResourceString(SR.ID0374), + SR.FormatID0374(context.Error, context.ErrorDescription, context.ErrorUri), context.Error, context.ErrorDescription, context.ErrorUri); } @@ -810,7 +810,7 @@ public class OpenIddictClientService if (context.IsRejected) { throw new ProtocolException( - SR.FormatID0319(context.Error, context.ErrorDescription, context.ErrorUri), + SR.FormatID0374(context.Error, context.ErrorDescription, context.ErrorUri), context.Error, context.ErrorDescription, context.ErrorUri); }