diff --git a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Startup.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Startup.cs index 2289a1e9..91f16852 100644 --- a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Startup.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Startup.cs @@ -111,10 +111,13 @@ public class Startup .SetUserinfoEndpointUris("connect/userinfo") .SetVerificationEndpointUris("connect/verify"); - // Note: this sample uses the code, device code, password and refresh token flows, but you - // can enable the other flows if you need to support implicit or client credentials. + // Note: this sample enables all the supported flows but + // you can restrict the list of enabled flows if necessary. options.AllowAuthorizationCodeFlow() .AllowDeviceCodeFlow() + .AllowHybridFlow() + .AllowImplicitFlow() + .AllowNoneFlow() .AllowPasswordFlow() .AllowRefreshTokenFlow(); diff --git a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Worker.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Worker.cs index 784c3802..aaf8556c 100644 --- a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Worker.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Worker.cs @@ -81,9 +81,16 @@ public class Worker : IHostedService Permissions.Endpoints.Token, Permissions.GrantTypes.AuthorizationCode, Permissions.GrantTypes.DeviceCode, + Permissions.GrantTypes.Implicit, Permissions.GrantTypes.Password, Permissions.GrantTypes.RefreshToken, Permissions.ResponseTypes.Code, + Permissions.ResponseTypes.CodeIdToken, + Permissions.ResponseTypes.CodeIdTokenToken, + Permissions.ResponseTypes.CodeToken, + Permissions.ResponseTypes.IdToken, + Permissions.ResponseTypes.IdTokenToken, + Permissions.ResponseTypes.None, Permissions.Scopes.Email, Permissions.Scopes.Profile, Permissions.Scopes.Roles, diff --git a/sandbox/OpenIddict.Sandbox.Console.Client/InteractiveService.cs b/sandbox/OpenIddict.Sandbox.Console.Client/InteractiveService.cs index 44bf52c7..6822ecdc 100644 --- a/sandbox/OpenIddict.Sandbox.Console.Client/InteractiveService.cs +++ b/sandbox/OpenIddict.Sandbox.Console.Client/InteractiveService.cs @@ -1,5 +1,6 @@ using System.Security.Claims; using Microsoft.Extensions.Hosting; +using OpenIddict.Abstractions; using OpenIddict.Client; using Spectre.Console; using static OpenIddict.Abstractions.OpenIddictConstants; @@ -39,174 +40,21 @@ public class InteractiveService : BackgroundService try { - 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.[/]"); - } + var configuration = await _service.GetServerConfigurationByProviderNameAsync(provider, stoppingToken); - else if (type is GrantTypes.Password) + if (await AuthenticateUserInteractivelyAsync(configuration, stoppingToken)) { - 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, - Scopes = [Scopes.OfflineAccess] - }); - - AnsiConsole.MarkupLine("[green]Resource owner password credentials authentication successful:[/]"); - 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:[/]"); - AnsiConsole.Write(CreateClaimTable((await _service.IntrospectTokenAsync(new() - { - CancellationToken = stoppingToken, - ProviderName = provider, - Token = response.AccessToken, - TokenTypeHint = TokenTypeHints.AccessToken - })).Principal)); - } - - // If revocation is supported by the server, ask the user if the access token should be revoked. - if (configuration.RevocationEndpoint is not null && await RevokeAccessTokenAsync(stoppingToken)) - { - await _service.RevokeTokenAsync(new() - { - CancellationToken = stoppingToken, - ProviderName = provider, - Token = response.AccessToken, - TokenTypeHint = TokenTypeHints.AccessToken - }); + var flow = await GetSelectedFlowAsync(configuration, stoppingToken); - AnsiConsole.MarkupLine("[steelblue]Access token revoked.[/]"); - } - - // 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 RefreshTokenAsync(stoppingToken)) - { - AnsiConsole.MarkupLine("[steelblue]Claims extracted from the refreshed identity:[/]"); - AnsiConsole.Write(CreateClaimTable((await _service.AuthenticateWithRefreshTokenAsync(new() - { - CancellationToken = stoppingToken, - ProviderName = provider, - RefreshToken = response.RefreshToken - })).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. - var result = await _service.ChallengeUsingDeviceAsync(new() - { - CancellationToken = stoppingToken, - ProviderName = provider - }); - - if (result.VerificationUriComplete is not null) - { - AnsiConsole.MarkupLineInterpolated($""" - [yellow]Please visit [link]{result.VerificationUriComplete}[/] and confirm the - displayed code is '{result.UserCode}' to complete the authentication demand.[/] - """); - } - - else - { - AnsiConsole.MarkupLineInterpolated($""" - [yellow]Please visit [link]{result.VerificationUri}[/] and enter - '{result.UserCode}' to complete the authentication demand.[/] - """); - } - - AnsiConsole.MarkupLine("[cyan]Waiting for the user to approve the authorization demand.[/]"); - - // Wait for the user to complete the demand on the other device. - var response = await _service.AuthenticateWithDeviceAsync(new() - { - CancellationToken = stoppingToken, - DeviceCode = result.DeviceCode, - Interval = result.Interval, - ProviderName = provider, - Timeout = result.ExpiresIn < TimeSpan.FromMinutes(5) ? result.ExpiresIn : TimeSpan.FromMinutes(5) - }); - - AnsiConsole.MarkupLine("[green]Device authentication successful:[/]"); - 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:[/]"); - AnsiConsole.Write(CreateClaimTable((await _service.IntrospectTokenAsync(new() - { - CancellationToken = stoppingToken, - ProviderName = provider, - Token = response.AccessToken, - TokenTypeHint = TokenTypeHints.AccessToken - })).Principal)); - } - - // If revocation is supported by the server, ask the user if the access token should be revoked. - if (configuration.RevocationEndpoint is not null && await RevokeAccessTokenAsync(stoppingToken)) - { - await _service.RevokeTokenAsync(new() - { - CancellationToken = stoppingToken, - ProviderName = provider, - Token = response.AccessToken, - TokenTypeHint = TokenTypeHints.AccessToken - }); - - AnsiConsole.MarkupLine("[steelblue]Access token revoked.[/]"); - } - - // 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 RefreshTokenAsync(stoppingToken)) - { - AnsiConsole.MarkupLine("[steelblue]Claims extracted from the refreshed identity:[/]"); - AnsiConsole.Write(CreateClaimTable((await _service.AuthenticateWithRefreshTokenAsync(new() - { - CancellationToken = stoppingToken, - ProviderName = provider, - RefreshToken = response.RefreshToken - })).Principal)); - } - } - - else if (type is GrantTypes.AuthorizationCode) - { AnsiConsole.MarkupLine("[cyan]Launching the system browser.[/]"); // Ask OpenIddict to initiate the authentication flow (typically, by starting the system browser). var result = await _service.ChallengeInteractivelyAsync(new() { + GrantType = flow.GrantType, CancellationToken = stoppingToken, - ProviderName = provider + ProviderName = provider, + ResponseType = flow.ResponseType }); AnsiConsole.MarkupLine("[cyan]Waiting for the user to approve the authorization demand.[/]"); @@ -224,7 +72,6 @@ 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)) @@ -297,6 +144,166 @@ public class InteractiveService : BackgroundService AnsiConsole.MarkupLine("[green]Interactive logout successful.[/]"); } } + + else + { + var type = await GetSelectedGrantTypeAsync(configuration, stoppingToken); + if (type is GrantTypes.DeviceCode) + { + // Ask OpenIddict to send a device authorization request and write + // the complete verification endpoint URI to the console output. + var result = await _service.ChallengeUsingDeviceAsync(new() + { + CancellationToken = stoppingToken, + ProviderName = provider + }); + + if (result.VerificationUriComplete is not null) + { + AnsiConsole.MarkupLineInterpolated($""" + [yellow]Please visit [link]{result.VerificationUriComplete}[/] and confirm the + displayed code is '{result.UserCode}' to complete the authentication demand.[/] + """); + } + + else + { + AnsiConsole.MarkupLineInterpolated($""" + [yellow]Please visit [link]{result.VerificationUri}[/] and enter + '{result.UserCode}' to complete the authentication demand.[/] + """); + } + + AnsiConsole.MarkupLine("[cyan]Waiting for the user to approve the authorization demand.[/]"); + + // Wait for the user to complete the demand on the other device. + var response = await _service.AuthenticateWithDeviceAsync(new() + { + CancellationToken = stoppingToken, + DeviceCode = result.DeviceCode, + Interval = result.Interval, + ProviderName = provider, + Timeout = result.ExpiresIn < TimeSpan.FromMinutes(5) ? result.ExpiresIn : TimeSpan.FromMinutes(5) + }); + + AnsiConsole.MarkupLine("[green]Device authentication successful:[/]"); + AnsiConsole.Write(CreateClaimTable(response.Principal)); + + // If introspection is supported by the server, ask the user if the access token should be introspected. + if (configuration.IntrospectionEndpoint is not null && await IntrospectAccessTokenAsync(stoppingToken)) + { + AnsiConsole.MarkupLine("[steelblue]Claims extracted from the token introspection response:[/]"); + AnsiConsole.Write(CreateClaimTable((await _service.IntrospectTokenAsync(new() + { + CancellationToken = stoppingToken, + ProviderName = provider, + Token = response.AccessToken, + TokenTypeHint = TokenTypeHints.AccessToken + })).Principal)); + } + + // If revocation is supported by the server, ask the user if the access token should be revoked. + if (configuration.RevocationEndpoint is not null && await RevokeAccessTokenAsync(stoppingToken)) + { + await _service.RevokeTokenAsync(new() + { + CancellationToken = stoppingToken, + ProviderName = provider, + Token = response.AccessToken, + TokenTypeHint = TokenTypeHints.AccessToken + }); + + AnsiConsole.MarkupLine("[steelblue]Access token revoked.[/]"); + } + + // 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 RefreshTokenAsync(stoppingToken)) + { + AnsiConsole.MarkupLine("[steelblue]Claims extracted from the refreshed identity:[/]"); + AnsiConsole.Write(CreateClaimTable((await _service.AuthenticateWithRefreshTokenAsync(new() + { + CancellationToken = stoppingToken, + ProviderName = provider, + RefreshToken = response.RefreshToken + })).Principal)); + } + } + + 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, + Scopes = [Scopes.OfflineAccess] + }); + + AnsiConsole.MarkupLine("[green]Resource owner password credentials authentication successful:[/]"); + AnsiConsole.Write(CreateClaimTable(response.Principal)); + + // If introspection is supported by the server, ask the user if the access token should be introspected. + if (configuration.IntrospectionEndpoint is not null && await IntrospectAccessTokenAsync(stoppingToken)) + { + AnsiConsole.MarkupLine("[steelblue]Claims extracted from the token introspection response:[/]"); + AnsiConsole.Write(CreateClaimTable((await _service.IntrospectTokenAsync(new() + { + CancellationToken = stoppingToken, + ProviderName = provider, + Token = response.AccessToken, + TokenTypeHint = TokenTypeHints.AccessToken + })).Principal)); + } + + // If revocation is supported by the server, ask the user if the access token should be revoked. + if (configuration.RevocationEndpoint is not null && await RevokeAccessTokenAsync(stoppingToken)) + { + await _service.RevokeTokenAsync(new() + { + CancellationToken = stoppingToken, + ProviderName = provider, + Token = response.AccessToken, + TokenTypeHint = TokenTypeHints.AccessToken + }); + + AnsiConsole.MarkupLine("[steelblue]Access token revoked.[/]"); + } + + // 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 RefreshTokenAsync(stoppingToken)) + { + AnsiConsole.MarkupLine("[steelblue]Claims extracted from the refreshed identity:[/]"); + AnsiConsole.Write(CreateClaimTable((await _service.AuthenticateWithRefreshTokenAsync(new() + { + CancellationToken = stoppingToken, + ProviderName = provider, + RefreshToken = response.RefreshToken + })).Principal)); + } + } + + else 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.[/]"); + } + } } catch (OperationCanceledException) @@ -350,20 +357,101 @@ public class InteractiveService : BackgroundService return WaitAsync(Task.Run(PromptAsync, cancellationToken), cancellationToken); } - Task GetSelectedGrantTypeAsync(string provider, CancellationToken cancellationToken) + Task<(string? GrantType, string? ResponseType)> GetSelectedFlowAsync( + OpenIddictConfiguration configuration, CancellationToken cancellationToken) { - async Task PromptAsync() + static (string? GrantType, string? ResponseType) Prompt(OpenIddictConfiguration configuration) { - List<(string GrantType, string DisplayName)> choices = []; + List<((string? GrantType, string? ResponseType), string DisplayName)> choices = []; + + var types = configuration.ResponseTypesSupported.Select(type => + new HashSet(type.Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries))); - var configuration = await _service.GetServerConfigurationByProviderNameAsync(provider, stoppingToken); if (configuration.GrantTypesSupported.Contains(GrantTypes.AuthorizationCode) && - configuration.AuthorizationEndpoint is not null && - configuration.TokenEndpoint is not null) + types.Any(type => type.Count is 1 && type.Contains(ResponseTypes.Code))) + { + choices.Add((( + GrantType : GrantTypes.AuthorizationCode, + ResponseType: ResponseTypes.Code), "Authorization code flow")); + } + + if (configuration.GrantTypesSupported.Contains(GrantTypes.Implicit)) { - choices.Add((GrantTypes.AuthorizationCode, "Authorization code grant")); + if (types.Any(type => type.Count is 1 && type.Contains(ResponseTypes.IdToken))) + { + choices.Add((( + GrantType : GrantTypes.Implicit, + ResponseType: ResponseTypes.IdToken), "Implicit flow (id_token)")); + } + + if (types.Any(type => type.Count is 2 && type.Contains(ResponseTypes.IdToken) && + type.Contains(ResponseTypes.Token))) + { + choices.Add((( + GrantType : GrantTypes.Implicit, + ResponseType: ResponseTypes.IdToken + ' ' + ResponseTypes.Token), "Implicit flow (id_token + token)")); + } } + if (configuration.GrantTypesSupported.Contains(GrantTypes.AuthorizationCode) && + configuration.GrantTypesSupported.Contains(GrantTypes.Implicit)) + { + if (types.Any(type => type.Count is 2 && type.Contains(ResponseTypes.Code) && + type.Contains(ResponseTypes.IdToken))) + { + choices.Add((( + GrantType : GrantTypes.AuthorizationCode, + ResponseType: ResponseTypes.Code + ' ' + ResponseTypes.IdToken), "Hybrid flow (code + id_token)")); + } + + if (types.Any(type => type.Count is 3 && type.Contains(ResponseTypes.Code) && + type.Contains(ResponseTypes.IdToken) && + type.Contains(ResponseTypes.Token))) + { + choices.Add((( + GrantType : GrantTypes.AuthorizationCode, + ResponseType: ResponseTypes.Code + ' ' + ResponseTypes.IdToken + ' ' + ResponseTypes.Token), + "Hybrid flow (code + id_token + token)")); + } + + if (types.Any(type => type.Count is 2 && type.Contains(ResponseTypes.Code) && + type.Contains(ResponseTypes.Token))) + { + choices.Add((( + GrantType : GrantTypes.AuthorizationCode, + ResponseType: ResponseTypes.Code + ' ' + ResponseTypes.Token), "Hybrid flow (code + token)")); + } + } + + if (types.Any(type => type.Count is 1 && type.Contains(ResponseTypes.None))) + { + choices.Add((( + GrantType : null, + ResponseType: ResponseTypes.None), "\"None flow\" (no token is returned)")); + } + + if (choices.Count is 0) + { + throw new NotSupportedException("The selected provider doesn't support any of the flows implemented by this sample."); + } + + choices.Insert(0, ((null, null), "Let OpenIddict negotiate the best authentication flow")); + + return AnsiConsole.Prompt(new SelectionPrompt<((string? GrantType, string? ResponseType), string DisplayName)>() + .Title("Select the user interactive grant type you'd like to use.") + .AddChoices(choices) + .UseConverter(choice => choice.DisplayName)).Item1; + } + + return WaitAsync(Task.Run(() => Prompt(configuration), cancellationToken), cancellationToken); + } + + Task GetSelectedGrantTypeAsync(OpenIddictConfiguration configuration, CancellationToken cancellationToken) + { + static string Prompt(OpenIddictConfiguration configuration) + { + List<(string GrantType, string DisplayName)> choices = []; + if (configuration.GrantTypesSupported.Contains(GrantTypes.DeviceCode) && configuration.DeviceAuthorizationEndpoint is not null && configuration.TokenEndpoint is not null) @@ -385,12 +473,7 @@ public class InteractiveService : BackgroundService if (choices.Count is 0) { - choices.Add((GrantTypes.AuthorizationCode, "Authorization code grant")); - } - - if (choices.Count is 1) - { - return choices[0].GrantType; + throw new NotSupportedException("The selected provider doesn't support any of the grant types implemented by this sample."); } return AnsiConsole.Prompt(new SelectionPrompt<(string GrantType, string DisplayName)>() @@ -399,7 +482,27 @@ public class InteractiveService : BackgroundService .UseConverter(choice => choice.DisplayName)).GrantType; } - return WaitAsync(Task.Run(PromptAsync, cancellationToken), cancellationToken); + return WaitAsync(Task.Run(() => Prompt(configuration), cancellationToken), cancellationToken); + } + + Task AuthenticateUserInteractivelyAsync( + OpenIddictConfiguration configuration, CancellationToken cancellationToken) + { + static bool Prompt() => AnsiConsole.Prompt(new ConfirmationPrompt( + "Would you like to use a user-interactive authentication method?") + { + Comparer = StringComparer.CurrentCultureIgnoreCase, + DefaultValue = true, + ShowDefaultValue = true + }); + + if (configuration.GrantTypesSupported.Contains(GrantTypes.AuthorizationCode) || + configuration.GrantTypesSupported.Contains(GrantTypes.Implicit)) + { + return WaitAsync(Task.Run(Prompt, cancellationToken), cancellationToken); + } + + return Task.FromResult(false); } Task GetUsernameAsync(CancellationToken cancellationToken) @@ -424,7 +527,6 @@ public class InteractiveService : BackgroundService return WaitAsync(Task.Run(Prompt, cancellationToken), cancellationToken); } - static Task RefreshTokenAsync(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 34489241..124484b6 100644 --- a/sandbox/OpenIddict.Sandbox.Console.Client/Program.cs +++ b/sandbox/OpenIddict.Sandbox.Console.Client/Program.cs @@ -36,12 +36,14 @@ var host = new HostBuilder() // Register the OpenIddict client components. .AddClient(options => { - // 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. + // Note: this sample enables all the supported flows but + // you can restrict the list of enabled flows if necessary. options.AllowAuthorizationCodeFlow() .AllowClientCredentialsFlow() .AllowDeviceCodeFlow() + .AllowHybridFlow() + .AllowImplicitFlow() + .AllowNoneFlow() .AllowPasswordFlow() .AllowRefreshTokenFlow(); diff --git a/src/OpenIddict.Abstractions/OpenIddictResources.resx b/src/OpenIddict.Abstractions/OpenIddictResources.resx index f4d735eb..95c42163 100644 --- a/src/OpenIddict.Abstractions/OpenIddictResources.resx +++ b/src/OpenIddict.Abstractions/OpenIddictResources.resx @@ -1665,6 +1665,12 @@ To apply post-logout redirection responses, create a class implementing 'IOpenId The base URI could not be resolved from the context, which may indicate an invalid authentication demand or an invalid configuration. When using interactive user authentication flows in desktop or mobile applications, make sure the system integration is registered by calling 'services.AddOpenIddict().AddClient().UseSystemIntegration()'. When using interactive user authentication flows in an ASP.NET Core or OWIN application, use the authentication APIs exposed by 'IAuthenticationService' or 'IAuthenticationManager' instead of the 'OpenIddictClientService' class. + + An explicit response type must be attached when specifying a specific grant type. + + + An explicit grant type must be attached when specifying a specific response type (except when using the special response_type=none value). + The security token is missing. diff --git a/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreConstants.cs b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreConstants.cs index 6131fd7d..6e42f1f8 100644 --- a/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreConstants.cs +++ b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreConstants.cs @@ -16,17 +16,21 @@ public static class OpenIddictClientAspNetCoreConstants public const string AuthorizationCodePrincipal = ".authorization_code_principal"; public const string BackchannelAccessTokenPrincipal = ".backchannel_access_token_principal"; public const string BackchannelIdentityTokenPrincipal = ".backchannel_identity_token_principal"; + public const string CodeChallengeMethod = ".code_challenge_method"; + public const string Error = ".error"; + public const string ErrorDescription = ".error_description"; + public const string ErrorUri = ".error_uri"; public const string FrontchannelAccessTokenPrincipal = ".frontchannel_access_token_principal"; public const string FrontchannelIdentityTokenPrincipal = ".frontchannel_identity_token_principal"; + public const string GrantType = ".grant_type"; public const string IdentityTokenHint = ".identity_token_hint"; - public const string LoginHint = ".login_hint"; public const string Issuer = ".issuer"; - public const string Error = ".error"; - public const string ErrorDescription = ".error_description"; - public const string ErrorUri = ".error_uri"; + public const string LoginHint = ".login_hint"; public const string ProviderName = ".provider_name"; public const string RefreshTokenPrincipal = ".refresh_token_principal"; public const string RegistrationId = ".registration_id"; + public const string ResponseMode = ".response_mode"; + public const string ResponseType = ".response_type"; public const string Scope = ".scope"; public const string StateTokenPrincipal = ".state_token_principal"; public const string UserinfoTokenPrincipal = ".userinfo_token_principal"; diff --git a/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlers.cs b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlers.cs index c01622ef..3ba40cd9 100644 --- a/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlers.cs +++ b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlers.cs @@ -566,14 +566,16 @@ public static partial class OpenIddictClientAspNetCoreHandlers var properties = context.Transaction.GetProperty(typeof(AuthenticationProperties).FullName!); if (properties is { Items.Count: > 0 }) { - // If a registration identifier was explicitly set, update the challenge context to use it. - if (properties.Items.TryGetValue(Properties.RegistrationId, out string? identifier) && - !string.IsNullOrEmpty(identifier)) - { - context.RegistrationId = identifier; - } + context.CodeChallengeMethod = GetProperty(properties, Properties.CodeChallengeMethod); + context.GrantType = GetProperty(properties, Properties.GrantType); + context.IdentityTokenHint = GetProperty(properties, Properties.IdentityTokenHint); + context.LoginHint = GetProperty(properties, Properties.LoginHint); + context.ProviderName = GetProperty(properties, Properties.ProviderName); + context.RegistrationId = GetProperty(properties, Properties.RegistrationId); + context.ResponseMode = GetProperty(properties, Properties.ResponseMode); + context.ResponseType = GetProperty(properties, Properties.ResponseType); + context.TargetLinkUri = properties.RedirectUri; - // If an issuer was explicitly set, update the challenge context to use it. if (properties.Items.TryGetValue(Properties.Issuer, out string? issuer) && !string.IsNullOrEmpty(issuer)) { // Ensure the issuer set by the application is a valid absolute URI. @@ -585,34 +587,6 @@ public static partial class OpenIddictClientAspNetCoreHandlers context.Issuer = uri; } - // If a provider name was explicitly set, update the challenge context to use it. - if (properties.Items.TryGetValue(Properties.ProviderName, out string? provider) && - !string.IsNullOrEmpty(provider)) - { - context.ProviderName = provider; - } - - // If a target link URI was specified, attach it to the context. - if (!string.IsNullOrEmpty(properties.RedirectUri)) - { - context.TargetLinkUri = properties.RedirectUri; - } - - // If an identity token hint was specified, attach it to the context. - if (properties.Items.TryGetValue(Properties.IdentityTokenHint, out string? token) && - !string.IsNullOrEmpty(token)) - { - context.IdentityTokenHint = token; - } - - // If a login hint was specified, attach it to the context. - if (properties.Items.TryGetValue(Properties.LoginHint, out string? hint) && - !string.IsNullOrEmpty(hint)) - { - context.LoginHint = hint; - } - - // If a scope was specified, attach it to the context. if (properties.Items.TryGetValue(Properties.Scope, out string? scope) && !string.IsNullOrEmpty(scope)) { @@ -648,6 +622,9 @@ public static partial class OpenIddictClientAspNetCoreHandlers } return default; + + static string? GetProperty(AuthenticationProperties properties, string name) + => properties.Items.TryGetValue(name, out string? value) ? value : null; } } @@ -813,14 +790,12 @@ public static partial class OpenIddictClientAspNetCoreHandlers var properties = context.Transaction.GetProperty(typeof(AuthenticationProperties).FullName!); if (properties is { Items.Count: > 0 }) { - // If a registration identifier was explicitly set, update the sign-out context to use it. - if (properties.Items.TryGetValue(Properties.RegistrationId, out string? identifier) && - !string.IsNullOrEmpty(identifier)) - { - context.RegistrationId = identifier; - } + context.IdentityTokenHint = GetProperty(properties, Properties.IdentityTokenHint); + context.LoginHint = GetProperty(properties, Properties.LoginHint); + context.ProviderName = GetProperty(properties, Properties.ProviderName); + context.RegistrationId = GetProperty(properties, Properties.RegistrationId); + context.TargetLinkUri = properties.RedirectUri; - // If an issuer was explicitly set, update the sign-out context to use it. if (properties.Items.TryGetValue(Properties.Issuer, out string? issuer) && !string.IsNullOrEmpty(issuer)) { // Ensure the issuer set by the application is a valid absolute URI. @@ -832,33 +807,6 @@ public static partial class OpenIddictClientAspNetCoreHandlers context.Issuer = uri; } - // If a provider name was explicitly set, update the sign-out context to use it. - if (properties.Items.TryGetValue(Properties.ProviderName, out string? provider) && - !string.IsNullOrEmpty(provider)) - { - context.ProviderName = provider; - } - - // If a target link URI was specified, attach it to the context. - if (!string.IsNullOrEmpty(properties.RedirectUri)) - { - context.TargetLinkUri = properties.RedirectUri; - } - - // If an identity token hint was specified, attach it to the context. - if (properties.Items.TryGetValue(Properties.IdentityTokenHint, out string? token) && - !string.IsNullOrEmpty(token)) - { - context.IdentityTokenHint = token; - } - - // If a login hint was specified, attach it to the context. - if (properties.Items.TryGetValue(Properties.LoginHint, out string? hint) && - !string.IsNullOrEmpty(hint)) - { - context.LoginHint = hint; - } - foreach (var property in properties.Items) { context.Properties[property.Key] = property.Value; @@ -888,6 +836,9 @@ public static partial class OpenIddictClientAspNetCoreHandlers } return default; + + static string? GetProperty(AuthenticationProperties properties, string name) + => properties.Items.TryGetValue(name, out string? value) ? value : null; } } diff --git a/src/OpenIddict.Client.Owin/OpenIddictClientOwinConstants.cs b/src/OpenIddict.Client.Owin/OpenIddictClientOwinConstants.cs index 5413566e..ec06a614 100644 --- a/src/OpenIddict.Client.Owin/OpenIddictClientOwinConstants.cs +++ b/src/OpenIddict.Client.Owin/OpenIddictClientOwinConstants.cs @@ -25,17 +25,21 @@ public static class OpenIddictClientOwinConstants public const string AuthorizationCodePrincipal = ".authorization_code_principal"; public const string BackchannelAccessTokenPrincipal = ".backchannel_access_token_principal"; public const string BackchannelIdentityTokenPrincipal = ".backchannel_identity_token_principal"; + public const string CodeChallengeMethod = ".code_challenge_method"; + public const string Error = ".error"; + public const string ErrorDescription = ".error_description"; + public const string ErrorUri = ".error_uri"; public const string FrontchannelAccessTokenPrincipal = ".frontchannel_access_token_principal"; public const string FrontchannelIdentityTokenPrincipal = ".frontchannel_identity_token_principal"; + public const string GrantType = ".grant_type"; public const string IdentityTokenHint = ".identity_token_hint"; - public const string LoginHint = ".login_hint"; public const string Issuer = ".issuer"; - public const string Error = ".error"; - public const string ErrorDescription = ".error_description"; - public const string ErrorUri = ".error_uri"; + public const string LoginHint = ".login_hint"; public const string ProviderName = ".provider_name"; public const string RefreshTokenPrincipal = ".refresh_token_principal"; public const string RegistrationId = ".registration_id"; + public const string ResponseMode = ".response_mode"; + public const string ResponseType = ".response_type"; public const string Scope = ".scope"; public const string StateTokenPrincipal = ".state_token_principal"; public const string UserinfoTokenPrincipal = ".userinfo_token_principal"; diff --git a/src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlers.cs b/src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlers.cs index e6204ec8..5ebfda54 100644 --- a/src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlers.cs +++ b/src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlers.cs @@ -578,14 +578,16 @@ public static partial class OpenIddictClientOwinHandlers return default; } - // If a registration identifier was explicitly set, update the challenge context to use it. - if (properties.Dictionary.TryGetValue(Properties.RegistrationId, out string? identifier) && - !string.IsNullOrEmpty(identifier)) - { - context.RegistrationId = identifier; - } + context.CodeChallengeMethod = GetProperty(properties, Properties.CodeChallengeMethod); + context.GrantType = GetProperty(properties, Properties.GrantType); + context.IdentityTokenHint = GetProperty(properties, Properties.IdentityTokenHint); + context.LoginHint = GetProperty(properties, Properties.LoginHint); + context.ProviderName = GetProperty(properties, Properties.ProviderName); + context.RegistrationId = GetProperty(properties, Properties.RegistrationId); + context.ResponseMode = GetProperty(properties, Properties.ResponseMode); + context.ResponseType = GetProperty(properties, Properties.ResponseType); + context.TargetLinkUri = properties.RedirectUri; - // If an issuer was explicitly set, update the challenge context to use it. if (properties.Dictionary.TryGetValue(Properties.Issuer, out string? issuer) && !string.IsNullOrEmpty(issuer)) { // Ensure the issuer set by the application is a valid absolute URI. @@ -597,34 +599,6 @@ public static partial class OpenIddictClientOwinHandlers context.Issuer = uri; } - // If a provider name was explicitly set, update the challenge context to use it. - if (properties.Dictionary.TryGetValue(Properties.ProviderName, out string? provider) && - !string.IsNullOrEmpty(provider)) - { - context.ProviderName = provider; - } - - // If a target link URI was specified, attach it to the context. - if (!string.IsNullOrEmpty(properties.RedirectUri)) - { - context.TargetLinkUri = properties.RedirectUri; - } - - // If an identity token hint was specified, attach it to the context. - if (properties.Dictionary.TryGetValue(Properties.IdentityTokenHint, out string? token) && - !string.IsNullOrEmpty(token)) - { - context.IdentityTokenHint = token; - } - - // If a login hint was specified, attach it to the context. - if (properties.Dictionary.TryGetValue(Properties.LoginHint, out string? hint) && - !string.IsNullOrEmpty(hint)) - { - context.LoginHint = hint; - } - - // If a scope was specified, attach it to the context. if (properties.Dictionary.TryGetValue(Properties.Scope, out string? scope) && !string.IsNullOrEmpty(scope)) { @@ -675,6 +649,9 @@ public static partial class OpenIddictClientOwinHandlers } return default; + + static string? GetProperty(AuthenticationProperties properties, string name) + => properties.Dictionary.TryGetValue(name, out string? value) ? value : null; } } @@ -851,14 +828,12 @@ public static partial class OpenIddictClientOwinHandlers return default; } - // If a registration identifier was explicitly set, update the sign-out context to use it. - if (properties.Dictionary.TryGetValue(Properties.RegistrationId, out string? identifier) && - !string.IsNullOrEmpty(identifier)) - { - context.RegistrationId = identifier; - } + context.IdentityTokenHint = GetProperty(properties, Properties.IdentityTokenHint); + context.LoginHint = GetProperty(properties, Properties.LoginHint); + context.ProviderName = GetProperty(properties, Properties.ProviderName); + context.RegistrationId = GetProperty(properties, Properties.RegistrationId); + context.TargetLinkUri = properties.RedirectUri; - // If an issuer was explicitly set, update the challenge context to use it. if (properties.Dictionary.TryGetValue(Properties.Issuer, out string? issuer) && !string.IsNullOrEmpty(issuer)) { // Ensure the issuer set by the application is a valid absolute URI. @@ -870,33 +845,6 @@ public static partial class OpenIddictClientOwinHandlers context.Issuer = uri; } - // If a provider name was explicitly set, update the sign-out context to use it. - if (properties.Dictionary.TryGetValue(Properties.ProviderName, out string? provider) && - !string.IsNullOrEmpty(provider)) - { - context.ProviderName = provider; - } - - // If a target link URI was specified, attach it to the context. - if (!string.IsNullOrEmpty(properties.RedirectUri)) - { - context.TargetLinkUri = properties.RedirectUri; - } - - // If an identity token hint was specified, attach it to the context. - if (properties.Dictionary.TryGetValue(Properties.IdentityTokenHint, out string? token) && - !string.IsNullOrEmpty(token)) - { - context.IdentityTokenHint = token; - } - - // If a login hint was specified, attach it to the context. - if (properties.Dictionary.TryGetValue(Properties.LoginHint, out string? hint) && - !string.IsNullOrEmpty(hint)) - { - context.LoginHint = hint; - } - // Note: unlike ASP.NET Core, OWIN's AuthenticationProperties doesn't offer a strongly-typed // dictionary that allows flowing parameters while preserving their original types. To allow // returning custom parameters, the OWIN host allows using AuthenticationProperties.Dictionary @@ -941,6 +889,9 @@ public static partial class OpenIddictClientOwinHandlers } return default; + + static string? GetProperty(AuthenticationProperties properties, string name) + => properties.Dictionary.TryGetValue(name, out string? value) ? value : null; } } diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.cs index e608d6ff..f581edb3 100644 --- a/src/OpenIddict.Client/OpenIddictClientHandlers.cs +++ b/src/OpenIddict.Client/OpenIddictClientHandlers.cs @@ -4259,8 +4259,8 @@ public static partial class OpenIddictClientHandlers throw new InvalidOperationException(SR.GetResourceString(SR.ID0006)); } - // If an explicit grant type was specified, ensure it is - // supported by OpenIddict and enabled in the client options. + // If an explicit grant type was specified, ensure it is supported by OpenIddict and + // enabled in the client options and that an explicit response type was also set. if (!string.IsNullOrEmpty(context.GrantType)) { if (context.GrantType is not ( @@ -4273,6 +4273,19 @@ public static partial class OpenIddictClientHandlers { throw new InvalidOperationException(SR.FormatID0359(context.GrantType)); } + + if (string.IsNullOrEmpty(context.ResponseType)) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0444)); + } + } + + // If a response type was explicitly specified, ensure a grant type was also set unless + // the special response_type=none - for which no grant type is defined - was specified. + if (!string.IsNullOrEmpty(context.ResponseType) && context.ResponseType is not ResponseTypes.None && + string.IsNullOrEmpty(context.GrantType)) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0445)); } // Ensure signing/and encryption credentials are present as they are required to protect state tokens. diff --git a/src/OpenIddict.Client/OpenIddictClientModels.cs b/src/OpenIddict.Client/OpenIddictClientModels.cs index ce8e582c..cae64279 100644 --- a/src/OpenIddict.Client/OpenIddictClientModels.cs +++ b/src/OpenIddict.Client/OpenIddictClientModels.cs @@ -143,6 +143,28 @@ public static class OpenIddictClientModels /// public CancellationToken CancellationToken { get; init; } + /// + /// Gets or sets the code challenge method that will be used for the authorization request. + /// + /// + /// Note: setting this property is generally not recommended, as OpenIddict automatically + /// negotiates the best code challenge method supported by both the client and the server. + /// + [EditorBrowsable(EditorBrowsableState.Advanced)] + public string? CodeChallengeMethod { get; init; } + + /// + /// Gets or sets the grant type that will be used for the authorization request. + /// If this property is set to a non-null value, the + /// property must also be explicitly set to a non-null value. + /// + /// + /// Note: setting this property is generally not recommended, as OpenIddict automatically + /// negotiates the best grant type supported by both the client and the server. + /// + [EditorBrowsable(EditorBrowsableState.Advanced)] + public string? GrantType { get; init; } + /// /// Gets or sets the application-specific properties that will be added to the context. /// @@ -162,6 +184,28 @@ public static class OpenIddictClientModels /// public string? RegistrationId { get; init; } + /// + /// Gets or sets the response mode that will be used for the authorization request. + /// + /// + /// Note: setting this property is generally not recommended, as OpenIddict automatically + /// negotiates the best response mode supported by both the client and the server. + /// + [EditorBrowsable(EditorBrowsableState.Advanced)] + public string? ResponseMode { get; init; } + + /// + /// Gets or sets the response type that will be used for the authorization request. + /// If this property is set to a non-null value, the + /// property must also be explicitly set to a non-null value. + /// + /// + /// Note: setting this property is generally not recommended, as OpenIddict automatically + /// negotiates the best response type supported by both the client and the server. + /// + [EditorBrowsable(EditorBrowsableState.Advanced)] + public string? ResponseType { get; init; } + /// /// Gets the scopes that will be sent to the authorization server. /// diff --git a/src/OpenIddict.Client/OpenIddictClientService.cs b/src/OpenIddict.Client/OpenIddictClientService.cs index 797bf5fc..27b42a4d 100644 --- a/src/OpenIddict.Client/OpenIddictClientService.cs +++ b/src/OpenIddict.Client/OpenIddictClientService.cs @@ -387,12 +387,16 @@ public class OpenIddictClientService var context = new ProcessChallengeContext(transaction) { CancellationToken = request.CancellationToken, + CodeChallengeMethod = request.CodeChallengeMethod, + GrantType = request.GrantType, Issuer = request.Issuer, Principal = new ClaimsPrincipal(new ClaimsIdentity()), ProviderName = request.ProviderName, RegistrationId = request.RegistrationId, Request = request.AdditionalAuthorizationRequestParameters is Dictionary parameters ? new(parameters) : new(), + ResponseMode = request.ResponseMode, + ResponseType = request.ResponseType }; if (request.Scopes is { Count: > 0 })