Browse Source

Allow specifying an explicit code challenge method/grant type/response type/response mode per challenge when using OpenIddictClientService or the ASP.NET Core/OWIN integrations

pull/2090/head
Kévin Chalet 2 years ago
parent
commit
4f1566665a
  1. 7
      sandbox/OpenIddict.Sandbox.AspNetCore.Server/Startup.cs
  2. 7
      sandbox/OpenIddict.Sandbox.AspNetCore.Server/Worker.cs
  3. 452
      sandbox/OpenIddict.Sandbox.Console.Client/InteractiveService.cs
  4. 8
      sandbox/OpenIddict.Sandbox.Console.Client/Program.cs
  5. 6
      src/OpenIddict.Abstractions/OpenIddictResources.resx
  6. 12
      src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreConstants.cs
  7. 89
      src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlers.cs
  8. 12
      src/OpenIddict.Client.Owin/OpenIddictClientOwinConstants.cs
  9. 89
      src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlers.cs
  10. 17
      src/OpenIddict.Client/OpenIddictClientHandlers.cs
  11. 44
      src/OpenIddict.Client/OpenIddictClientModels.cs
  12. 4
      src/OpenIddict.Client/OpenIddictClientService.cs

7
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();

7
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,

452
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<string> GetSelectedGrantTypeAsync(string provider, CancellationToken cancellationToken)
Task<(string? GrantType, string? ResponseType)> GetSelectedFlowAsync(
OpenIddictConfiguration configuration, CancellationToken cancellationToken)
{
async Task<string> 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<string>(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<string> 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<bool> 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<string> GetUsernameAsync(CancellationToken cancellationToken)
@ -424,7 +527,6 @@ public class InteractiveService : BackgroundService
return WaitAsync(Task.Run(Prompt, cancellationToken), cancellationToken);
}
static Task<bool> RefreshTokenAsync(CancellationToken cancellationToken)
{
static bool Prompt() => AnsiConsole.Prompt(new ConfirmationPrompt(

8
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();

6
src/OpenIddict.Abstractions/OpenIddictResources.resx

@ -1665,6 +1665,12 @@ To apply post-logout redirection responses, create a class implementing 'IOpenId
<data name="ID0443" xml:space="preserve">
<value>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.</value>
</data>
<data name="ID0444" xml:space="preserve">
<value>An explicit response type must be attached when specifying a specific grant type.</value>
</data>
<data name="ID0445" xml:space="preserve">
<value>An explicit grant type must be attached when specifying a specific response type (except when using the special response_type=none value).</value>
</data>
<data name="ID2000" xml:space="preserve">
<value>The security token is missing.</value>
</data>

12
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";

89
src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlers.cs

@ -566,14 +566,16 @@ public static partial class OpenIddictClientAspNetCoreHandlers
var properties = context.Transaction.GetProperty<AuthenticationProperties>(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<AuthenticationProperties>(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;
}
}

12
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";

89
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;
}
}

17
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.

44
src/OpenIddict.Client/OpenIddictClientModels.cs

@ -143,6 +143,28 @@ public static class OpenIddictClientModels
/// </summary>
public CancellationToken CancellationToken { get; init; }
/// <summary>
/// Gets or sets the code challenge method that will be used for the authorization request.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public string? CodeChallengeMethod { get; init; }
/// <summary>
/// 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 <see cref="ResponseType"/>
/// property must also be explicitly set to a non-null value.
/// </summary>
/// <remarks>
/// Note: setting this property is generally not recommended, as OpenIddict automatically
/// negotiates the best grant type supported by both the client and the server.
/// </remarks>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public string? GrantType { get; init; }
/// <summary>
/// Gets or sets the application-specific properties that will be added to the context.
/// </summary>
@ -162,6 +184,28 @@ public static class OpenIddictClientModels
/// </summary>
public string? RegistrationId { get; init; }
/// <summary>
/// Gets or sets the response mode that will be used for the authorization request.
/// </summary>
/// <remarks>
/// Note: setting this property is generally not recommended, as OpenIddict automatically
/// negotiates the best response mode supported by both the client and the server.
/// </remarks>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public string? ResponseMode { get; init; }
/// <summary>
/// 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 <see cref="GrantType"/>
/// property must also be explicitly set to a non-null value.
/// </summary>
/// <remarks>
/// Note: setting this property is generally not recommended, as OpenIddict automatically
/// negotiates the best response type supported by both the client and the server.
/// </remarks>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public string? ResponseType { get; init; }
/// <summary>
/// Gets the scopes that will be sent to the authorization server.
/// </summary>

4
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<string, OpenIddictParameter> parameters ? new(parameters) : new(),
ResponseMode = request.ResponseMode,
ResponseType = request.ResponseType
};
if (request.Scopes is { Count: > 0 })

Loading…
Cancel
Save