Browse Source

Improve the WWW-Authenticate parsing logic

Kévin Chalet 8 months ago
parent
commit
07728ed60e
  1. 98
      src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.cs
  2. 98
      src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.cs
  3. 95
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTestClient.cs
  4. 95
      test/OpenIddict.Validation.IntegrationTests/OpenIddictValidationIntegrationTestClient.cs

98
src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.cs

@ -1429,46 +1429,92 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
return default;
}
var parameters = new Dictionary<string, StringValues>(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<KeyValuePair<string, string?>> 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<string, string?>(key, value);
}
}
}
}

98
src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.cs

@ -953,46 +953,92 @@ public static partial class OpenIddictValidationSystemNetHttpHandlers
return default;
}
var parameters = new Dictionary<string, StringValues>(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<KeyValuePair<string, string?>> 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<string, string?>(key, value);
}
}
}
}

95
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<string, StringValues>(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<KeyValuePair<string, string?>> 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<string, string?>(key, value);
}
}
return new OpenIddictResponse(parameters);
}
else if (message.Headers.Location is not null)

95
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<string, StringValues>(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<KeyValuePair<string, string?>> 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<string, string?>(key, value);
}
}
return new OpenIddictResponse(parameters);
}
else if (message.Headers.Location is not null)

Loading…
Cancel
Save