diff --git a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.cs b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.cs index 5bbeccc3..c9c98db7 100644 --- a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.cs +++ b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.cs @@ -1429,46 +1429,92 @@ public static partial class OpenIddictClientSystemNetHttpHandlers return default; } - var parameters = new Dictionary(response.Headers.WwwAuthenticate.Count); + context.Transaction.Response = new OpenIddictResponse(response.Headers.WwwAuthenticate + .Where(static header => !string.IsNullOrEmpty(header.Parameter)) + .SelectMany(static header => ParseParameters(header.Parameter!))); - foreach (var header in response.Headers.WwwAuthenticate) + return default; + + static IEnumerable> ParseParameters(string parameter) { - if (string.IsNullOrEmpty(header.Parameter)) + var index = 0; + + while (index < parameter.Length) { - continue; - } + // Skip leading whitespaces and commas. + while (index < parameter.Length && (char.IsWhiteSpace(parameter[index]) || parameter[index] is ',')) + { + index++; + } - // Note: while initially not allowed by the core OAuth 2.0 specification, multiple - // parameters with the same name are used by derived drafts like the OAuth 2.0 - // token exchange specification. For consistency, multiple parameters with the - // same name are also supported when returned as part of WWW-Authentication headers. + // Parse the parameter key. + var start = index; + while (index < parameter.Length && parameter[index] is not ('=' or ',')) + { + index++; + } - foreach (var parameter in header.Parameter.Split(Separators.Comma, StringSplitOptions.RemoveEmptyEntries)) - { - var values = parameter.Split(Separators.EqualsSign, StringSplitOptions.RemoveEmptyEntries); - if (values.Length is not 2) + if (index >= parameter.Length || parameter[index] is ',') { - continue; + break; } - var (name, value) = ( - values[0]?.Trim(Separators.Space[0]), - values[1]?.Trim(Separators.Space[0], Separators.DoubleQuote[0])); + var key = parameter[start..index].Trim(); - if (string.IsNullOrEmpty(name)) + // Skip the equals sign. + index++; + while (index < parameter.Length && char.IsWhiteSpace(parameter[index])) { - continue; + index++; } - parameters[name] = parameters.ContainsKey(name) ? - StringValues.Concat(parameters[name], value?.Replace("\\\"", "\"")) : - new StringValues(value?.Replace("\\\"", "\"")); - } - } + // Parse the parameter value. + string value; + if (index < parameter.Length && parameter[index] is '"') + { + // Skip the opening quote. + index++; + + var builder = new StringBuilder(); + + while (index < parameter.Length) + { + if (parameter[index] is '\\' && index + 1 < parameter.Length) + { + builder.Append(parameter[index + 1]); + index += 2; + } + + else if (parameter[index] is '"') + { + // Skip the closing quote. + index++; + break; + } + + else + { + builder.Append(parameter[index++]); + } + } + + value = builder.ToString(); + } + + else + { + start = index; + while (index < parameter.Length && parameter[index] is not ',' && !char.IsWhiteSpace(parameter[index])) + { + index++; + } - context.Transaction.Response = new OpenIddictResponse(parameters); + value = parameter[start..index].Trim(); + } - return default; + yield return new KeyValuePair(key, value); + } + } } } diff --git a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.cs b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.cs index 6fc0d608..ceba563c 100644 --- a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.cs +++ b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.cs @@ -953,46 +953,92 @@ public static partial class OpenIddictValidationSystemNetHttpHandlers return default; } - var parameters = new Dictionary(response.Headers.WwwAuthenticate.Count); + context.Transaction.Response = new OpenIddictResponse(response.Headers.WwwAuthenticate + .Where(static header => !string.IsNullOrEmpty(header.Parameter)) + .SelectMany(static header => ParseParameters(header.Parameter!))); - foreach (var header in response.Headers.WwwAuthenticate) + return default; + + static IEnumerable> ParseParameters(string parameter) { - if (string.IsNullOrEmpty(header.Parameter)) + var index = 0; + + while (index < parameter.Length) { - continue; - } + // Skip leading whitespaces and commas. + while (index < parameter.Length && (char.IsWhiteSpace(parameter[index]) || parameter[index] is ',')) + { + index++; + } - // Note: while initially not allowed by the core OAuth 2.0 specification, multiple - // parameters with the same name are used by derived drafts like the OAuth 2.0 - // token exchange specification. For consistency, multiple parameters with the - // same name are also supported when returned as part of WWW-Authentication headers. + // Parse the parameter key. + var start = index; + while (index < parameter.Length && parameter[index] is not ('=' or ',')) + { + index++; + } - foreach (var parameter in header.Parameter.Split(Separators.Comma, StringSplitOptions.RemoveEmptyEntries)) - { - var values = parameter.Split(Separators.EqualsSign, StringSplitOptions.RemoveEmptyEntries); - if (values.Length is not 2) + if (index >= parameter.Length || parameter[index] is ',') { - continue; + break; } - var (name, value) = ( - values[0]?.Trim(Separators.Space[0]), - values[1]?.Trim(Separators.Space[0], Separators.DoubleQuote[0])); + var key = parameter[start..index].Trim(); - if (string.IsNullOrEmpty(name)) + // Skip the equals sign. + index++; + while (index < parameter.Length && char.IsWhiteSpace(parameter[index])) { - continue; + index++; } - parameters[name] = parameters.ContainsKey(name) ? - StringValues.Concat(parameters[name], value?.Replace("\\\"", "\"")) : - new StringValues(value?.Replace("\\\"", "\"")); - } - } + // Parse the parameter value. + string value; + if (index < parameter.Length && parameter[index] is '"') + { + // Skip the opening quote. + index++; + + var builder = new StringBuilder(); + + while (index < parameter.Length) + { + if (parameter[index] is '\\' && index + 1 < parameter.Length) + { + builder.Append(parameter[index + 1]); + index += 2; + } + + else if (parameter[index] is '"') + { + // Skip the closing quote. + index++; + break; + } + + else + { + builder.Append(parameter[index++]); + } + } + + value = builder.ToString(); + } + + else + { + start = index; + while (index < parameter.Length && parameter[index] is not ',' && !char.IsWhiteSpace(parameter[index])) + { + index++; + } - context.Transaction.Response = new OpenIddictResponse(parameters); + value = parameter[start..index].Trim(); + } - return default; + yield return new KeyValuePair(key, value); + } + } } } diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTestClient.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTestClient.cs index 06f90f64..2ed2d36f 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTestClient.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTestClient.cs @@ -6,6 +6,7 @@ using System.Net.Http; using System.Net.Http.Json; +using System.Text; using AngleSharp.Html.Parser; using Microsoft.Extensions.Primitives; using OpenIddict.Extensions; @@ -266,44 +267,90 @@ public class OpenIddictServerIntegrationTestClient : IAsyncDisposable { if (message.Headers.WwwAuthenticate.Count is not 0) { - var parameters = new Dictionary(message.Headers.WwwAuthenticate.Count); + return new OpenIddictResponse(message.Headers.WwwAuthenticate + .Where(static header => !string.IsNullOrEmpty(header.Parameter)) + .SelectMany(static header => ParseParameters(header.Parameter!))); - foreach (var header in message.Headers.WwwAuthenticate) + static IEnumerable> ParseParameters(string parameter) { - if (string.IsNullOrEmpty(header.Parameter)) + var index = 0; + + while (index < parameter.Length) { - continue; - } + // Skip leading whitespaces and commas. + while (index < parameter.Length && (char.IsWhiteSpace(parameter[index]) || parameter[index] is ',')) + { + index++; + } - // Note: while initially not allowed by the core OAuth 2.0 specification, multiple - // parameters with the same name are used by derived drafts like the OAuth 2.0 - // token exchange specification. For consistency, multiple parameters with the - // same name are also supported when returned as part of WWW-Authentication headers. + // Parse the parameter key. + var start = index; + while (index < parameter.Length && parameter[index] is not ('=' or ',')) + { + index++; + } - foreach (var parameter in header.Parameter.Split(Separators.Comma, StringSplitOptions.RemoveEmptyEntries)) - { - var values = parameter.Split(Separators.EqualsSign, StringSplitOptions.RemoveEmptyEntries); - if (values.Length is not 2) + if (index >= parameter.Length || parameter[index] is ',') { - continue; + break; } - var (name, value) = ( - values[0]?.Trim(Separators.Space[0]), - values[1]?.Trim(Separators.Space[0], Separators.DoubleQuote[0])); + var key = parameter[start..index].Trim(); - if (string.IsNullOrEmpty(name)) + // Skip the equals sign. + index++; + while (index < parameter.Length && char.IsWhiteSpace(parameter[index])) { - continue; + index++; } - parameters[name] = parameters.ContainsKey(name) ? - StringValues.Concat(parameters[name], value?.Replace("\\\"", "\"")) : - new StringValues(value?.Replace("\\\"", "\"")); + // Parse the parameter value. + string value; + if (index < parameter.Length && parameter[index] is '"') + { + // Skip the opening quote. + index++; + + var builder = new StringBuilder(); + + while (index < parameter.Length) + { + if (parameter[index] is '\\' && index + 1 < parameter.Length) + { + builder.Append(parameter[index + 1]); + index += 2; + } + + else if (parameter[index] is '"') + { + // Skip the closing quote. + index++; + break; + } + + else + { + builder.Append(parameter[index++]); + } + } + + value = builder.ToString(); + } + + else + { + start = index; + while (index < parameter.Length && parameter[index] is not ',' && !char.IsWhiteSpace(parameter[index])) + { + index++; + } + + value = parameter[start..index].Trim(); + } + + yield return new KeyValuePair(key, value); } } - - return new OpenIddictResponse(parameters); } else if (message.Headers.Location is not null) diff --git a/test/OpenIddict.Validation.IntegrationTests/OpenIddictValidationIntegrationTestClient.cs b/test/OpenIddict.Validation.IntegrationTests/OpenIddictValidationIntegrationTestClient.cs index b7ace6fb..c9159bff 100644 --- a/test/OpenIddict.Validation.IntegrationTests/OpenIddictValidationIntegrationTestClient.cs +++ b/test/OpenIddict.Validation.IntegrationTests/OpenIddictValidationIntegrationTestClient.cs @@ -6,6 +6,7 @@ using System.Net.Http; using System.Net.Http.Json; +using System.Text; using AngleSharp.Html.Parser; using Microsoft.Extensions.Primitives; using OpenIddict.Extensions; @@ -266,44 +267,90 @@ public class OpenIddictValidationIntegrationTestClient : IAsyncDisposable { if (message.Headers.WwwAuthenticate.Count is not 0) { - var parameters = new Dictionary(message.Headers.WwwAuthenticate.Count); + return new OpenIddictResponse(message.Headers.WwwAuthenticate + .Where(static header => !string.IsNullOrEmpty(header.Parameter)) + .SelectMany(static header => ParseParameters(header.Parameter!))); - foreach (var header in message.Headers.WwwAuthenticate) + static IEnumerable> ParseParameters(string parameter) { - if (string.IsNullOrEmpty(header.Parameter)) + var index = 0; + + while (index < parameter.Length) { - continue; - } + // Skip leading whitespaces and commas. + while (index < parameter.Length && (char.IsWhiteSpace(parameter[index]) || parameter[index] is ',')) + { + index++; + } - // Note: while initially not allowed by the core OAuth 2.0 specification, multiple - // parameters with the same name are used by derived drafts like the OAuth 2.0 - // token exchange specification. For consistency, multiple parameters with the - // same name are also supported when returned as part of WWW-Authentication headers. + // Parse the parameter key. + var start = index; + while (index < parameter.Length && parameter[index] is not ('=' or ',')) + { + index++; + } - foreach (var parameter in header.Parameter.Split(Separators.Comma, StringSplitOptions.RemoveEmptyEntries)) - { - var values = parameter.Split(Separators.EqualsSign, StringSplitOptions.RemoveEmptyEntries); - if (values.Length is not 2) + if (index >= parameter.Length || parameter[index] is ',') { - continue; + break; } - var (name, value) = ( - values[0]?.Trim(Separators.Space[0]), - values[1]?.Trim(Separators.Space[0], Separators.DoubleQuote[0])); + var key = parameter[start..index].Trim(); - if (string.IsNullOrEmpty(name)) + // Skip the equals sign. + index++; + while (index < parameter.Length && char.IsWhiteSpace(parameter[index])) { - continue; + index++; } - parameters[name] = parameters.ContainsKey(name) ? - StringValues.Concat(parameters[name], value?.Replace("\\\"", "\"")) : - new StringValues(value?.Replace("\\\"", "\"")); + // Parse the parameter value. + string value; + if (index < parameter.Length && parameter[index] is '"') + { + // Skip the opening quote. + index++; + + var builder = new StringBuilder(); + + while (index < parameter.Length) + { + if (parameter[index] is '\\' && index + 1 < parameter.Length) + { + builder.Append(parameter[index + 1]); + index += 2; + } + + else if (parameter[index] is '"') + { + // Skip the closing quote. + index++; + break; + } + + else + { + builder.Append(parameter[index++]); + } + } + + value = builder.ToString(); + } + + else + { + start = index; + while (index < parameter.Length && parameter[index] is not ',' && !char.IsWhiteSpace(parameter[index])) + { + index++; + } + + value = parameter[start..index].Trim(); + } + + yield return new KeyValuePair(key, value); } } - - return new OpenIddictResponse(parameters); } else if (message.Headers.Location is not null)