diff --git a/shared/OpenIddict.Extensions/OpenIddictHelpers.cs b/shared/OpenIddict.Extensions/OpenIddictHelpers.cs
index 00c5c4cb..87fe62d4 100644
--- a/shared/OpenIddict.Extensions/OpenIddictHelpers.cs
+++ b/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()));
+ }
+
+ ///
+ /// Extracts the parameters from the specified fragment.
+ ///
+ /// The fragment string, which may start with a '#'.
+ /// The parameters extracted from the specified fragment.
+ /// is .
+ public static IReadOnlyDictionary 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()));
}
///
diff --git a/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlers.cs b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlers.cs
index 3ba40cd9..5936993f 100644
--- a/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlers.cs
+++ b/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
}
}
+ ///
+ /// 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.
+ ///
+ public sealed class AttachNonDefaultResponseMode : IOpenIddictClientHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictClientHandlerDescriptor Descriptor { get; }
+ = OpenIddictClientHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .AddFilter()
+ .UseSingletonHandler()
+ .SetOrder(AttachResponseMode.Descriptor.Order - 500)
+ .Build();
+
+ ///
+ 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 { 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,
+ _ => 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;
+ }
+ }
+
///
/// Contains the logic responsible for creating a correlation cookie that serves as a
/// protection against state token injection, forged requests and session fixation attacks.
diff --git a/src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlers.cs b/src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlers.cs
index 5ebfda54..1e270ea0 100644
--- a/src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlers.cs
+++ b/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
}
}
+ ///
+ /// 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.
+ ///
+ public sealed class AttachNonDefaultResponseMode : IOpenIddictClientHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictClientHandlerDescriptor Descriptor { get; }
+ = OpenIddictClientHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .AddFilter()
+ .UseSingletonHandler()
+ .SetOrder(AttachResponseMode.Descriptor.Order - 500)
+ .Build();
+
+ ///
+ 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 { 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,
+ _ => 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;
+ }
+ }
+
///
/// Contains the logic responsible for creating a correlation cookie that serves as a
/// protection against state token injection, forged requests and session fixation attacks.
diff --git a/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlers.cs b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlers.cs
index 50b7889f..56a99bfd 100644
--- a/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlers.cs
+++ b/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(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(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()
// 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()
.UseSingletonHandler()
.SetOrder(AttachRedirectUri.Descriptor.Order + 500)
@@ -1713,6 +1757,162 @@ public static partial class OpenIddictClientSystemIntegrationHandlers
}
}
+ ///
+ /// Contains the logic responsible for attaching a non-default response mode to the challenge request, if applicable.
+ ///
+ public sealed class AttachNonDefaultResponseMode : IOpenIddictClientHandler
+ {
+ private readonly OpenIddictClientSystemIntegrationHttpListener _listener;
+ private readonly IOptionsMonitor _options;
+
+ public AttachNonDefaultResponseMode(
+ OpenIddictClientSystemIntegrationHttpListener listener,
+ IOptionsMonitor options)
+ {
+ _listener = listener ?? throw new ArgumentNullException(nameof(listener));
+ _options = options ?? throw new ArgumentNullException(nameof(options));
+ }
+
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictClientHandlerDescriptor Descriptor { get; }
+ = OpenIddictClientHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .AddFilter()
+ .UseSingletonHandler()
+ .SetOrder(AttachResponseMode.Descriptor.Order - 500)
+ .Build();
+
+ ///
+ 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 { 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,
+ _ => 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;
+ }
+ }
+ }
+
///
/// 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.
diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs
index 840643e5..1c1d4e6d 100644
--- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs
+++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs
@@ -1468,9 +1468,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers
= OpenIddictClientHandlerDescriptor.CreateBuilder()
.AddFilter()
.UseSingletonHandler()
- // 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();
diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.cs
index acdc7c34..d0642518 100644
--- a/src/OpenIddict.Client/OpenIddictClientHandlers.cs
+++ b/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
}
}
- ///
- /// Contains the logic responsible for attaching the response mode to the challenge request.
- ///
- public sealed class AttachResponseMode : IOpenIddictClientHandler
- {
- ///
- /// Gets the default descriptor definition assigned to this handler.
- ///
- public static OpenIddictClientHandlerDescriptor Descriptor { get; }
- = OpenIddictClientHandlerDescriptor.CreateBuilder()
- .AddFilter()
- .UseSingletonHandler()
- .SetOrder(AttachChallengeHostProperties.Descriptor.Order + 1_000)
- .Build();
-
- ///
- 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 { 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,
- _ => 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;
- }
- }
-
///
/// Contains the logic responsible for attaching the client identifier to the challenge request.
///
@@ -4876,7 +4773,7 @@ public static partial class OpenIddictClientHandlers
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder()
.UseSingletonHandler()
- .SetOrder(AttachResponseMode.Descriptor.Order + 1_000)
+ .SetOrder(AttachChallengeHostProperties.Descriptor.Order + 1_000)
.Build();
///
@@ -5176,6 +5073,72 @@ public static partial class OpenIddictClientHandlers
}
}
+ ///
+ /// Contains the logic responsible for attaching the response mode to the challenge request.
+ ///
+ public sealed class AttachResponseMode : IOpenIddictClientHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictClientHandlerDescriptor Descriptor { get; }
+ = OpenIddictClientHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .UseSingletonHandler()
+ .SetOrder(AttachCodeChallengeParameters.Descriptor.Order + 1_000)
+ .Build();
+
+ ///
+ 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,
+ _ => 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;
+ }
+ }
+
///
/// 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()
.AddFilter()
.UseSingletonHandler()
- .SetOrder(AttachCodeChallengeParameters.Descriptor.Order + 1_000)
+ .SetOrder(AttachResponseMode.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();