Browse Source

Update the system integration package to support response_mode=fragment for requests handled via protocol activation or using the web authentication broker

pull/2100/head
Kévin Chalet 2 years ago
parent
commit
1a2b190480
  1. 34
      shared/OpenIddict.Extensions/OpenIddictHelpers.cs
  2. 93
      src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlers.cs
  3. 93
      src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlers.cs
  4. 222
      src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlers.cs
  5. 4
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs
  6. 181
      src/OpenIddict.Client/OpenIddictClientHandlers.cs

34
shared/OpenIddict.Extensions/OpenIddictHelpers.cs

@ -429,13 +429,37 @@ internal static class OpenIddictHelpers
return query.TrimStart(Separators.QuestionMark[0])
.Split(new[] { Separators.Ampersand[0], Separators.Semicolon[0] }, StringSplitOptions.RemoveEmptyEntries)
.Select(parameter => parameter.Split(Separators.EqualsSign, StringSplitOptions.RemoveEmptyEntries))
.Select(parts => (
.Select(static parameter => parameter.Split(Separators.EqualsSign, StringSplitOptions.RemoveEmptyEntries))
.Select(static parts => (
Key: parts[0] is string key ? Uri.UnescapeDataString(key) : null,
Value: parts.Length > 1 && parts[1] is string value ? Uri.UnescapeDataString(value) : null))
.Where(pair => !string.IsNullOrEmpty(pair.Key))
.GroupBy(pair => pair.Key)
.ToDictionary(pair => pair.Key!, pair => new StringValues(pair.Select(parts => parts.Value).ToArray()));
.Where(static pair => !string.IsNullOrEmpty(pair.Key))
.GroupBy(static pair => pair.Key)
.ToDictionary(static pair => pair.Key!, static pair => new StringValues(pair.Select(parts => parts.Value).ToArray()));
}
/// <summary>
/// Extracts the parameters from the specified fragment.
/// </summary>
/// <param name="fragment">The fragment string, which may start with a '#'.</param>
/// <returns>The parameters extracted from the specified fragment.</returns>
/// <exception cref="ArgumentNullException"><paramref name="fragment"/> is <see langword="null"/>.</exception>
public static IReadOnlyDictionary<string, StringValues> ParseFragment(string fragment)
{
if (fragment is null)
{
throw new ArgumentNullException(nameof(fragment));
}
return fragment.TrimStart(Separators.Hash[0])
.Split(new[] { Separators.Ampersand[0], Separators.Semicolon[0] }, StringSplitOptions.RemoveEmptyEntries)
.Select(static parameter => parameter.Split(Separators.EqualsSign, StringSplitOptions.RemoveEmptyEntries))
.Select(static parts => (
Key: parts[0] is string key ? Uri.UnescapeDataString(key) : null,
Value: parts.Length > 1 && parts[1] is string value ? Uri.UnescapeDataString(value) : null))
.Where(static pair => !string.IsNullOrEmpty(pair.Key))
.GroupBy(static pair => pair.Key)
.ToDictionary(static pair => pair.Key!, static pair => new StringValues(pair.Select(parts => parts.Value).ToArray()));
}
/// <summary>

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

@ -51,6 +51,7 @@ public static partial class OpenIddictClientAspNetCoreHandlers
ValidateChallengeType.Descriptor,
ResolveHostChallengeProperties.Descriptor,
ValidateTransportSecurityRequirementForChallenge.Descriptor,
AttachNonDefaultResponseMode.Descriptor,
GenerateLoginCorrelationCookie.Descriptor,
/*
@ -668,6 +669,98 @@ public static partial class OpenIddictClientAspNetCoreHandlers
}
}
/// <summary>
/// Contains the logic responsible for attaching a non-default response mode to the challenge request.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core.
/// </summary>
public sealed class AttachNonDefaultResponseMode : IOpenIddictClientHandler<ProcessChallengeContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.AddFilter<RequireHttpRequest>()
.AddFilter<RequireInteractiveGrantType>()
.UseSingletonHandler<AttachNonDefaultResponseMode>()
.SetOrder(AttachResponseMode.Descriptor.Order - 500)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessChallengeContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
// If an explicit response type was specified, don't overwrite it.
if (!string.IsNullOrEmpty(context.ResponseMode))
{
return default;
}
// Note: in most cases, the query response mode will be used as it offers the best compatibility and,
// unlike the form_post response mode, is compatible with SameSite=Lax cookies (as it uses GET requests
// for the callback stage). However, some specific response_type/response_mode combinations are not
// allowed (e.g query can never be used with a type containing id_token or token, as required by the
// OAuth 2.0 multiple response types specification. To prevent invalid combinations from being sent to
// the remote server, the response types are taken into account when selecting the best response mode.
if (context.ResponseType?.Split(Separators.Space) is not IList<string> { Count: > 0 } types)
{
return default;
}
context.ResponseMode = (
// Note: if response modes are explicitly listed in the client registration, only use
// the response modes that are both listed and enabled in the global client options.
// Otherwise, always default to the response modes that have been enabled globally.
SupportedClientResponseModes: context.Registration.ResponseModes.Count switch
{
0 => context.Options.ResponseModes as ICollection<string>,
_ => context.Options.ResponseModes.Intersect(context.Registration.ResponseModes, StringComparer.Ordinal).ToList()
},
SupportedServerResponseModes: context.Configuration.ResponseModesSupported) switch
{
// If both the client and the server support response_mode=form_post, use it if the response
// types contain a value that prevents response_mode=query from being used (token/id_token).
({ Count: > 0 } client, { Count: > 0 } server) when
client.Contains(ResponseModes.FormPost) && server.Contains(ResponseModes.FormPost) &&
(types.Contains(ResponseTypes.IdToken) || types.Contains(ResponseTypes.Token))
=> ResponseModes.FormPost,
// If the client supports response_mode=form_post and the server doesn't specify a list
// of response modes, assume it is supported and use it if the response types contain
// a value that prevents response_mode=query from being used (token/id_token).
({ Count: > 0 } client, { Count: 0 }) when client.Contains(ResponseModes.FormPost) &&
(types.Contains(ResponseTypes.IdToken) || types.Contains(ResponseTypes.Token))
=> ResponseModes.FormPost,
// If both the client and the server support response_mode=query, use it.
({ Count: > 0 } client, { Count: > 0 } server) when
client.Contains(ResponseModes.Query) && server.Contains(ResponseModes.Query)
=> ResponseModes.Query,
// If the client supports response_mode=query and the server doesn't
// specify a list of response modes, assume it is supported.
({ Count: > 0 } client, { Count: 0 }) when client.Contains(ResponseModes.Query)
=> ResponseModes.Query,
// If both the client and the server support response_mode=form_post, use it.
({ Count: > 0 } client, { Count: > 0 } server) when
client.Contains(ResponseModes.FormPost) && server.Contains(ResponseModes.FormPost)
=> ResponseModes.FormPost,
// Assign a null value to allow the generic handler present in
// the base client package to negotiate other response modes.
_ => null
};
return default;
}
}
/// <summary>
/// Contains the logic responsible for creating a correlation cookie that serves as a
/// protection against state token injection, forged requests and session fixation attacks.

93
src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlers.cs

@ -46,6 +46,7 @@ public static partial class OpenIddictClientOwinHandlers
ValidateChallengeType.Descriptor,
ResolveHostChallengeProperties.Descriptor,
ValidateTransportSecurityRequirementForChallenge.Descriptor,
AttachNonDefaultResponseMode.Descriptor,
GenerateLoginCorrelationCookie.Descriptor,
/*
@ -695,6 +696,98 @@ public static partial class OpenIddictClientOwinHandlers
}
}
/// <summary>
/// Contains the logic responsible for attaching a non-default response mode to the challenge request.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN.
/// </summary>
public sealed class AttachNonDefaultResponseMode : IOpenIddictClientHandler<ProcessChallengeContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.AddFilter<RequireOwinRequest>()
.AddFilter<RequireInteractiveGrantType>()
.UseSingletonHandler<AttachNonDefaultResponseMode>()
.SetOrder(AttachResponseMode.Descriptor.Order - 500)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessChallengeContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
// If an explicit response type was specified, don't overwrite it.
if (!string.IsNullOrEmpty(context.ResponseMode))
{
return default;
}
// Note: in most cases, the query response mode will be used as it offers the best compatibility and,
// unlike the form_post response mode, is compatible with SameSite=Lax cookies (as it uses GET requests
// for the callback stage). However, some specific response_type/response_mode combinations are not
// allowed (e.g query can never be used with a type containing id_token or token, as required by the
// OAuth 2.0 multiple response types specification. To prevent invalid combinations from being sent to
// the remote server, the response types are taken into account when selecting the best response mode.
if (context.ResponseType?.Split(Separators.Space) is not IList<string> { Count: > 0 } types)
{
return default;
}
context.ResponseMode = (
// Note: if response modes are explicitly listed in the client registration, only use
// the response modes that are both listed and enabled in the global client options.
// Otherwise, always default to the response modes that have been enabled globally.
SupportedClientResponseModes: context.Registration.ResponseModes.Count switch
{
0 => context.Options.ResponseModes as ICollection<string>,
_ => context.Options.ResponseModes.Intersect(context.Registration.ResponseModes, StringComparer.Ordinal).ToList()
},
SupportedServerResponseModes: context.Configuration.ResponseModesSupported) switch
{
// If both the client and the server support response_mode=form_post, use it if the response
// types contain a value that prevents response_mode=query from being used (token/id_token).
({ Count: > 0 } client, { Count: > 0 } server) when
client.Contains(ResponseModes.FormPost) && server.Contains(ResponseModes.FormPost) &&
(types.Contains(ResponseTypes.IdToken) || types.Contains(ResponseTypes.Token))
=> ResponseModes.FormPost,
// If the client supports response_mode=form_post and the server doesn't specify a list
// of response modes, assume it is supported and use it if the response types contain
// a value that prevents response_mode=query from being used (token/id_token).
({ Count: > 0 } client, { Count: 0 }) when client.Contains(ResponseModes.FormPost) &&
(types.Contains(ResponseTypes.IdToken) || types.Contains(ResponseTypes.Token))
=> ResponseModes.FormPost,
// If both the client and the server support response_mode=query, use it.
({ Count: > 0 } client, { Count: > 0 } server) when
client.Contains(ResponseModes.Query) && server.Contains(ResponseModes.Query)
=> ResponseModes.Query,
// If the client supports response_mode=query and the server doesn't
// specify a list of response modes, assume it is supported.
({ Count: > 0 } client, { Count: 0 }) when client.Contains(ResponseModes.Query)
=> ResponseModes.Query,
// If both the client and the server support response_mode=form_post, use it.
({ Count: > 0 } client, { Count: > 0 } server) when
client.Contains(ResponseModes.FormPost) && server.Contains(ResponseModes.FormPost)
=> ResponseModes.FormPost,
// Assign a null value to allow the generic handler present in
// the base client package to negotiate other response modes.
_ => null
};
return default;
}
}
/// <summary>
/// Contains the logic responsible for creating a correlation cookie that serves as a
/// protection against state token injection, forged requests and session fixation attacks.

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

@ -8,6 +8,7 @@ using System.Collections.Immutable;
using System.ComponentModel;
using System.Diagnostics;
using System.Net;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security.Claims;
using System.Text;
@ -75,6 +76,7 @@ public static partial class OpenIddictClientSystemIntegrationHandlers
*/
InferBaseUriFromClientUri.Descriptor,
AttachDynamicPortToRedirectUri.Descriptor,
AttachNonDefaultResponseMode.Descriptor,
AttachInstanceIdentifier.Descriptor,
TrackAuthenticationOperation.Descriptor,
@ -537,12 +539,34 @@ public static partial class OpenIddictClientSystemIntegrationHandlers
throw new ArgumentNullException(nameof(context));
}
context.Transaction.Request = context.Transaction.GetProtocolActivation() switch
if (context.Transaction.GetProtocolActivation() is not { ActivationUri: Uri uri })
{
{ ActivationUri: Uri uri } => new OpenIddictRequest(OpenIddictHelpers.ParseQuery(uri.Query)),
throw new InvalidOperationException(SR.GetResourceString(SR.ID0375));
}
_ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0375))
};
var parameters = new Dictionary<string, StringValues>(StringComparer.Ordinal);
if (!string.IsNullOrEmpty(uri.Query))
{
foreach (var parameter in OpenIddictHelpers.ParseQuery(uri.Query))
{
parameters[parameter.Key] = parameter.Value;
}
}
// Note: the fragment is always processed after the query string to ensure that
// parameters extracted from the fragment are preferred to parameters extracted
// from the query string when they are present in both parts.
if (!string.IsNullOrEmpty(uri.Fragment))
{
foreach (var parameter in OpenIddictHelpers.ParseFragment(uri.Fragment))
{
parameters[parameter.Key] = parameter.Value;
}
}
context.Transaction.Request = new OpenIddictRequest(parameters);
return default;
}
@ -576,14 +600,34 @@ public static partial class OpenIddictClientSystemIntegrationHandlers
}
#if SUPPORTS_WINDOWS_RUNTIME
context.Transaction.Request = context.Transaction.GetWebAuthenticationResult() switch
if (context.Transaction.GetWebAuthenticationResult()
is not { ResponseStatus: WebAuthenticationStatus.Success, ResponseData: string data } ||
!Uri.TryCreate(data, UriKind.Absolute, out Uri? uri))
{
{ ResponseStatus: WebAuthenticationStatus.Success, ResponseData: string data } when
Uri.TryCreate(data, UriKind.Absolute, out Uri? uri)
=> new OpenIddictRequest(OpenIddictHelpers.ParseQuery(uri.Query)),
throw new InvalidOperationException(SR.GetResourceString(SR.ID0393));
}
_ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0393))
};
var parameters = new Dictionary<string, StringValues>(StringComparer.Ordinal);
if (!string.IsNullOrEmpty(uri.Query))
{
foreach (var parameter in OpenIddictHelpers.ParseQuery(uri.Query))
{
parameters[parameter.Key] = parameter.Value;
}
}
// Note: the fragment is always processed after the query string to ensure that
// parameters extracted from the fragment are preferred to parameters extracted
// from the query string when they are present in both parts.
if (!string.IsNullOrEmpty(uri.Fragment))
{
foreach (var parameter in OpenIddictHelpers.ParseFragment(uri.Fragment))
{
parameters[parameter.Key] = parameter.Value;
}
}
return default;
#else
@ -1679,7 +1723,7 @@ public static partial class OpenIddictClientSystemIntegrationHandlers
.AddFilter<RequireInteractiveGrantType>()
// Note: only apply the dynamic port replacement logic if the callback request
// is going to be received by the system browser to ensure it doesn't apply to
// challenge demands handled via a web authentication broker are not affected.
// challenge demands handled via a web authentication broker.
.AddFilter<RequireSystemBrowser>()
.UseSingletonHandler<AttachDynamicPortToRedirectUri>()
.SetOrder(AttachRedirectUri.Descriptor.Order + 500)
@ -1713,6 +1757,162 @@ public static partial class OpenIddictClientSystemIntegrationHandlers
}
}
/// <summary>
/// Contains the logic responsible for attaching a non-default response mode to the challenge request, if applicable.
/// </summary>
public sealed class AttachNonDefaultResponseMode : IOpenIddictClientHandler<ProcessChallengeContext>
{
private readonly OpenIddictClientSystemIntegrationHttpListener _listener;
private readonly IOptionsMonitor<OpenIddictClientSystemIntegrationOptions> _options;
public AttachNonDefaultResponseMode(
OpenIddictClientSystemIntegrationHttpListener listener,
IOptionsMonitor<OpenIddictClientSystemIntegrationOptions> options)
{
_listener = listener ?? throw new ArgumentNullException(nameof(listener));
_options = options ?? throw new ArgumentNullException(nameof(options));
}
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.AddFilter<RequireInteractiveSession>()
.AddFilter<RequireInteractiveGrantType>()
.UseSingletonHandler<AttachNonDefaultResponseMode>()
.SetOrder(AttachResponseMode.Descriptor.Order - 500)
.Build();
/// <inheritdoc/>
public async ValueTask HandleAsync(ProcessChallengeContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
// If an explicit response type was specified, don't overwrite it.
if (!string.IsNullOrEmpty(context.ResponseMode))
{
return;
}
// Some specific response_type/response_mode combinations are not allowed (e.g response_mode=query
// can never be used with a response type containing id_token or token, as required by the OAuth 2.0
// multiple response types specification. To prevent invalid combinations from being sent to the
// remote server, the response types are taken into account when selecting the best response mode.
if (context.ResponseType?.Split(Separators.Space) is not IList<string> { Count: > 0 } types)
{
return;
}
context.ResponseMode = (
// Note: if response modes are explicitly listed in the client registration, only use
// the response modes that are both listed and enabled in the global client options.
// Otherwise, always default to the response modes that have been enabled globally.
SupportedClientResponseModes: context.Registration.ResponseModes.Count switch
{
0 => context.Options.ResponseModes as ICollection<string>,
_ => context.Options.ResponseModes.Intersect(context.Registration.ResponseModes, StringComparer.Ordinal).ToList()
},
SupportedServerResponseModes: context.Configuration.ResponseModesSupported) switch
{
#if SUPPORTS_WINDOWS_RUNTIME
// When using the web authentication broker on Windows, if both the client and
// the server support response_mode=fragment, use it if the response types contain
// a value that prevents response_mode=query from being used (token/id_token).
({ Count: > 0 } client, { Count: > 0 } server) when
OpenIddictClientSystemIntegrationHelpers.IsWebAuthenticationBrokerSupported() &&
IsAuthenticationMode(OpenIddictClientSystemIntegrationAuthenticationMode.WebAuthenticationBroker) &&
client.Contains(ResponseModes.Fragment) && server.Contains(ResponseModes.Fragment) &&
(types.Contains(ResponseTypes.IdToken) || types.Contains(ResponseTypes.Token))
=> ResponseModes.Fragment,
// When using the web authentication broker on Windows, if the client supports
// response_mode=fragment and the server doesn't specify a list of response modes,
// assume it is supported and use it if the response types contain a value that
// prevents response_mode=query from being used (token/id_token).
({ Count: > 0 } client, { Count: 0 }) when
OpenIddictClientSystemIntegrationHelpers.IsWebAuthenticationBrokerSupported() &&
IsAuthenticationMode(OpenIddictClientSystemIntegrationAuthenticationMode.WebAuthenticationBroker) &&
client.Contains(ResponseModes.Fragment) &&
(types.Contains(ResponseTypes.IdToken) || types.Contains(ResponseTypes.Token))
=> ResponseModes.Fragment,
#endif
// When using browser-based authentication with a redirect_uri not pointing to the embedded server,
// if both the client and the server support response_mode=fragment, use it if the response types
// contain a value that prevents response_mode=query from being used (token/id_token).
({ Count: > 0 } client, { Count: > 0 } server) when
IsAuthenticationMode(OpenIddictClientSystemIntegrationAuthenticationMode.SystemBrowser) &&
Uri.TryCreate(context.RedirectUri, UriKind.Absolute, out Uri? uri) &&
!string.Equals(uri.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) &&
uri.Port != await _listener.GetEmbeddedServerPortAsync(context.CancellationToken) &&
client.Contains(ResponseModes.Fragment) && server.Contains(ResponseModes.Fragment) &&
(types.Contains(ResponseTypes.IdToken) || types.Contains(ResponseTypes.Token))
=> ResponseModes.Fragment,
// When using browser-based authentication with a redirect_uri not pointing to the embedded server,
// if the client supports response_mode=fragment and the server doesn't specify a list of response
// modes, assume it is supported and use it if the response types contain a value that prevents
// response_mode=query from being used (token/id_token).
({ Count: > 0 } client, { Count: 0 }) when
IsAuthenticationMode(OpenIddictClientSystemIntegrationAuthenticationMode.SystemBrowser) &&
Uri.TryCreate(context.RedirectUri, UriKind.Absolute, out Uri? uri) &&
!string.Equals(uri.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) &&
uri.Port != await _listener.GetEmbeddedServerPortAsync(context.CancellationToken) &&
client.Contains(ResponseModes.Fragment) &&
(types.Contains(ResponseTypes.IdToken) || types.Contains(ResponseTypes.Token))
=> ResponseModes.Fragment,
// When using browser-based authentication with a redirect_uri pointing to the embedded server,
// if both the client and the server support response_mode=form_post, use it if the response
// types contain a value that prevents response_mode=query from being used (token/id_token).
({ Count: > 0 } client, { Count: > 0 } server) when
IsAuthenticationMode(OpenIddictClientSystemIntegrationAuthenticationMode.SystemBrowser) &&
Uri.TryCreate(context.RedirectUri, UriKind.Absolute, out Uri? uri) &&
string.Equals(uri.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) &&
uri.IsLoopback &&
uri.Port == await _listener.GetEmbeddedServerPortAsync(context.CancellationToken) &&
client.Contains(ResponseModes.FormPost) && server.Contains(ResponseModes.FormPost) &&
(types.Contains(ResponseTypes.IdToken) || types.Contains(ResponseTypes.Token))
=> ResponseModes.FormPost,
// When using browser-based authentication with a redirect_uri pointing to the embedded server,
// if the client supports response_mode=form_post and the server doesn't specify a list
// of response modes, assume it is supported and use it if the response types contain
// a value that prevents response_mode=query from being used (token/id_token).
({ Count: > 0 } client, { Count: 0 }) when
IsAuthenticationMode(OpenIddictClientSystemIntegrationAuthenticationMode.SystemBrowser) &&
Uri.TryCreate(context.RedirectUri, UriKind.Absolute, out Uri? uri) &&
string.Equals(uri.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) &&
uri.IsLoopback &&
uri.Port == await _listener.GetEmbeddedServerPortAsync(context.CancellationToken) &&
client.Contains(ResponseModes.FormPost) &&
(types.Contains(ResponseTypes.IdToken) || types.Contains(ResponseTypes.Token))
=> ResponseModes.FormPost,
// Assign a null value to allow the generic handler present in
// the base client package to negotiate other response modes.
_ => null
};
[MethodImpl(MethodImplOptions.AggressiveInlining)]
bool IsAuthenticationMode(OpenIddictClientSystemIntegrationAuthenticationMode mode)
{
if (context.Transaction.Properties.TryGetValue(
typeof(OpenIddictClientSystemIntegrationAuthenticationMode).FullName!, out var result) &&
result is OpenIddictClientSystemIntegrationAuthenticationMode value)
{
return mode == value;
}
return mode == _options.CurrentValue.AuthenticationMode;
}
}
}
/// <summary>
/// Contains the logic responsible for storing the identifier of the current instance in the state token.
/// Note: this handler is not used when the user session is not interactive.

4
src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs

@ -1468,9 +1468,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.AddFilter<RequireInteractiveGrantType>()
.UseSingletonHandler<OverrideResponseMode>()
// Note: this handler MUST be invoked after the scopes have been attached to the
// context to support overriding the response mode based on the requested scopes.
.SetOrder(AttachScopes.Descriptor.Order + 500)
.SetOrder(AttachResponseMode.Descriptor.Order + 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();

181
src/OpenIddict.Client/OpenIddictClientHandlers.cs

@ -107,13 +107,13 @@ public static partial class OpenIddictClientHandlers
AttachGrantTypeAndResponseType.Descriptor,
EvaluateGeneratedChallengeTokens.Descriptor,
AttachChallengeHostProperties.Descriptor,
AttachResponseMode.Descriptor,
AttachClientId.Descriptor,
AttachRedirectUri.Descriptor,
AttachRequestForgeryProtection.Descriptor,
AttachScopes.Descriptor,
AttachNonce.Descriptor,
AttachCodeChallengeParameters.Descriptor,
AttachResponseMode.Descriptor,
PrepareLoginStateTokenPrincipal.Descriptor,
GenerateLoginStateToken.Descriptor,
AttachChallengeParameters.Descriptor,
@ -4569,7 +4569,7 @@ public static partial class OpenIddictClientHandlers
// Hybrid flow with grant_type=authorization_code/implicit and response_type=code id_token:
(var client, var server) when
// Ensure grant_type=authorization_code and grant_type=implicit are supported.
(client.GrantTypes.Contains(GrantTypes.AuthorizationCode) && client.GrantTypes.Contains(GrantTypes.Implicit)) &&
(client.GrantTypes.Contains(GrantTypes.AuthorizationCode) && client.GrantTypes.Contains(GrantTypes.Implicit)) &&
(server.GrantTypes.Count is 0 || // If empty, assume the code and implicit grants are supported by the server.
(server.GrantTypes.Contains(GrantTypes.AuthorizationCode) && server.GrantTypes.Contains(GrantTypes.Implicit))) &&
@ -4606,7 +4606,7 @@ public static partial class OpenIddictClientHandlers
// Hybrid flow with grant_type=authorization_code/implicit and response_type=code id_token token.
(var client, var server) when
// Ensure grant_type=authorization_code and grant_type=implicit are supported.
(client.GrantTypes.Contains(GrantTypes.AuthorizationCode) && client.GrantTypes.Contains(GrantTypes.Implicit)) &&
(client.GrantTypes.Contains(GrantTypes.AuthorizationCode) && client.GrantTypes.Contains(GrantTypes.Implicit)) &&
(server.GrantTypes.Count is 0 || // If empty, assume the code and implicit grants are supported by the server.
(server.GrantTypes.Contains(GrantTypes.AuthorizationCode) && server.GrantTypes.Contains(GrantTypes.Implicit))) &&
@ -4623,7 +4623,7 @@ public static partial class OpenIddictClientHandlers
// Hybrid flow with grant_type=authorization_code/implicit and response_type=code token.
(var client, var server) when
// Ensure grant_type=authorization_code and grant_type=implicit are supported.
(client.GrantTypes.Contains(GrantTypes.AuthorizationCode) && client.GrantTypes.Contains(GrantTypes.Implicit)) &&
(client.GrantTypes.Contains(GrantTypes.AuthorizationCode) && client.GrantTypes.Contains(GrantTypes.Implicit)) &&
(server.GrantTypes.Count is 0 || // If empty, assume the code and implicit grants are supported by the server.
(server.GrantTypes.Contains(GrantTypes.AuthorizationCode) && server.GrantTypes.Contains(GrantTypes.Implicit))) &&
@ -4762,109 +4762,6 @@ public static partial class OpenIddictClientHandlers
}
}
/// <summary>
/// Contains the logic responsible for attaching the response mode to the challenge request.
/// </summary>
public sealed class AttachResponseMode : IOpenIddictClientHandler<ProcessChallengeContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.AddFilter<RequireInteractiveGrantType>()
.UseSingletonHandler<AttachResponseMode>()
.SetOrder(AttachChallengeHostProperties.Descriptor.Order + 1_000)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessChallengeContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
// If an explicit response type was specified, don't overwrite it.
if (!string.IsNullOrEmpty(context.ResponseMode))
{
return default;
}
// Note: in most cases, the query response mode will be used as it offers the
// best compatibility and, unlike the form_post response mode, is compatible
// with SameSite=Lax cookies (as it uses GET requests for the callback stage).
//
// However, browser-based hosts like Blazor may typically want to use the fragment
// response mode as it offers a better protection for SPA applications.
// Unfortunately, server-side clients like ASP.NET Core applications cannot
// natively use response_mode=fragment as URI fragments are never sent to servers.
//
// As such, this handler will not choose response_mode=fragment by default and it is
// expected that specialized hosts like Blazor implement custom event handlers to
// opt for fragment by default, if it is supported by the authorization server.
// Some specific response_type/response_mode combinations are not allowed (e.g response_mode=query
// can never be used with a response type containing id_token or token, as required by the OAuth 2.0
// multiple response types specification. To prevent invalid combinations from being sent to the
// remote server, the response types are taken into account when selecting the best response mode.
if (context.ResponseType?.Split(Separators.Space) is not IList<string> { Count: > 0 } types)
{
return default;
}
context.ResponseMode = (
// Note: if response modes are explicitly listed in the client registration, only use
// the response modes that are both listed and enabled in the global client options.
// Otherwise, always default to the response modes that have been enabled globally.
SupportedClientResponseModes: context.Registration.ResponseModes.Count switch
{
0 => context.Options.ResponseModes as ICollection<string>,
_ => context.Options.ResponseModes.Intersect(context.Registration.ResponseModes, StringComparer.Ordinal).ToList()
},
SupportedServerResponseModes: context.Configuration.ResponseModesSupported) switch
{
// If the list of response modes supported by the client is empty, abort the challenge operation.
({ Count: 0 }, { Count: _ }) => throw new InvalidOperationException(SR.GetResourceString(SR.ID0362)),
// If both the client and the server support response_mode=form_post, use it if the response
// types contain a value that prevents response_mode=query from being used (token/id_token).
({ Count: > 0 } client, { Count: > 0 } server) when
client.Contains(ResponseModes.FormPost) && server.Contains(ResponseModes.FormPost) &&
(types.Contains(ResponseTypes.IdToken) || types.Contains(ResponseTypes.Token))
=> ResponseModes.FormPost,
// If the client support response_mode=form_post and the server doesn't specify a list
// of response modes, assume it is supported and use it if the response types contain
// a value that prevents response_mode=query from being used (token/id_token).
({ Count: > 0 } client, { Count: 0 }) when client.Contains(ResponseModes.FormPost) &&
(types.Contains(ResponseTypes.IdToken) || types.Contains(ResponseTypes.Token))
=> ResponseModes.FormPost,
// If both the client and the server support response_mode=query, use it.
({ Count: > 0 } client, { Count: > 0 } server) when
client.Contains(ResponseModes.Query) && server.Contains(ResponseModes.Query)
=> ResponseModes.Query,
// If the client support response_mode=query and the server doesn't
// specify a list of response modes, assume it is supported.
({ Count: > 0 } client, { Count: 0 }) when client.Contains(ResponseModes.Query)
=> ResponseModes.Query,
// If both the client and the server support response_mode=form_post, use it.
({ Count: > 0 } client, { Count: > 0 } server) when
client.Contains(ResponseModes.FormPost) && server.Contains(ResponseModes.FormPost)
=> ResponseModes.FormPost,
// If no common response mode can be negotiated, abort the challenge operation.
_ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0299))
};
return default;
}
}
/// <summary>
/// Contains the logic responsible for attaching the client identifier to the challenge request.
/// </summary>
@ -4876,7 +4773,7 @@ public static partial class OpenIddictClientHandlers
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.UseSingletonHandler<AttachClientId>()
.SetOrder(AttachResponseMode.Descriptor.Order + 1_000)
.SetOrder(AttachChallengeHostProperties.Descriptor.Order + 1_000)
.Build();
/// <inheritdoc/>
@ -5176,6 +5073,72 @@ public static partial class OpenIddictClientHandlers
}
}
/// <summary>
/// Contains the logic responsible for attaching the response mode to the challenge request.
/// </summary>
public sealed class AttachResponseMode : IOpenIddictClientHandler<ProcessChallengeContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.AddFilter<RequireInteractiveGrantType>()
.UseSingletonHandler<AttachResponseMode>()
.SetOrder(AttachCodeChallengeParameters.Descriptor.Order + 1_000)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessChallengeContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
// If an explicit response type was specified, don't overwrite it.
if (!string.IsNullOrEmpty(context.ResponseMode))
{
return default;
}
context.ResponseMode = (
// Note: if response modes are explicitly listed in the client registration, only use
// the response modes that are both listed and enabled in the global client options.
// Otherwise, always default to the response modes that have been enabled globally.
SupportedClientResponseModes: context.Registration.ResponseModes.Count switch
{
0 => context.Options.ResponseModes as ICollection<string>,
_ => context.Options.ResponseModes.Intersect(context.Registration.ResponseModes, StringComparer.Ordinal).ToList()
},
SupportedServerResponseModes: context.Configuration.ResponseModesSupported) switch
{
// If the list of response modes supported by the client is empty, abort the challenge operation.
({ Count: 0 }, { Count: _ }) => throw new InvalidOperationException(SR.GetResourceString(SR.ID0362)),
// If both the client and the server support response_mode=query, use it.
({ Count: > 0 } client, { Count: > 0 } server) when
client.Contains(ResponseModes.Query) && server.Contains(ResponseModes.Query)
=> ResponseModes.Query,
// If the client supports response_mode=query and the server doesn't
// specify a list of response modes, assume it is supported.
({ Count: > 0 } client, { Count: 0 }) when client.Contains(ResponseModes.Query)
=> ResponseModes.Query,
// Note: other response modes - like form_post or fragment - are never negotiated
// by this generic handler but can be selected by more specialized handlers, such
// as the one present in the ASP.NET Core/OWIN hosts or in the system integration.
// If no common response mode can be negotiated, abort the challenge operation.
_ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0299))
};
return default;
}
}
/// <summary>
/// Contains the logic responsible for preparing and attaching the claims principal
/// used to generate the state token, if one is going to be returned.
@ -5189,7 +5152,7 @@ public static partial class OpenIddictClientHandlers
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.AddFilter<RequireLoginStateTokenGenerated>()
.UseSingletonHandler<PrepareLoginStateTokenPrincipal>()
.SetOrder(AttachCodeChallengeParameters.Descriptor.Order + 1_000)
.SetOrder(AttachResponseMode.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();

Loading…
Cancel
Save