You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
422 lines
16 KiB
422 lines
16 KiB
/*
|
|
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
|
|
* See https://github.com/openiddict/openiddict-core for more information concerning
|
|
* the license and the contributors participating to this project.
|
|
*/
|
|
|
|
using System.Collections.Immutable;
|
|
using System.Collections.ObjectModel;
|
|
using System.Collections.Specialized;
|
|
using System.Diagnostics;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
using System.Text.Encodings.Web;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Nodes;
|
|
using System.Text.Json.Serialization;
|
|
using Microsoft.Extensions.Primitives;
|
|
|
|
namespace OpenIddict.Abstractions;
|
|
|
|
/// <summary>
|
|
/// Represents an abstract OpenIddict message.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Security notice: developers instantiating this type are responsible for ensuring that the
|
|
/// imported parameters are safe and won't cause the resulting message to grow abnormally,
|
|
/// which may result in an excessive memory consumption and a potential denial of service.
|
|
/// </remarks>
|
|
[DebuggerDisplay("Parameters: {Parameters.Count}")]
|
|
[JsonConverter(typeof(OpenIddictConverter))]
|
|
public class OpenIddictMessage
|
|
{
|
|
/// <summary>
|
|
/// Initializes a new OpenIddict message.
|
|
/// </summary>
|
|
public OpenIddictMessage()
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new OpenIddict message.
|
|
/// </summary>
|
|
/// <param name="parameters">The message parameters.</param>
|
|
/// <remarks>Parameters with a null or empty key are always ignored.</remarks>
|
|
public OpenIddictMessage(JsonElement parameters)
|
|
{
|
|
if (parameters.ValueKind is not JsonValueKind.Object)
|
|
{
|
|
throw new ArgumentException(SR.GetResourceString(SR.ID0189), nameof(parameters));
|
|
}
|
|
|
|
foreach (var parameter in parameters.EnumerateObject())
|
|
{
|
|
// Ignore parameters whose name is null or empty.
|
|
if (string.IsNullOrEmpty(parameter.Name))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// While generally discouraged, JSON objects can contain multiple properties with
|
|
// the same name. In this case, the last occurrence replaces the previous ones.
|
|
Parameters[parameter.Name] = parameter.Value;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new OpenIddict message.
|
|
/// </summary>
|
|
/// <param name="parameters">The message parameters.</param>
|
|
/// <remarks>Parameters with a null or empty key are always ignored.</remarks>
|
|
public OpenIddictMessage(JsonObject parameters)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(parameters);
|
|
|
|
foreach (var parameter in parameters)
|
|
{
|
|
// Ignore parameters whose name is null or empty.
|
|
if (string.IsNullOrEmpty(parameter.Key))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// While generally discouraged, JSON objects can contain multiple properties with
|
|
// the same name. In this case, the last occurrence replaces the previous ones.
|
|
Parameters[parameter.Key] = parameter.Value;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new OpenIddict message.
|
|
/// </summary>
|
|
/// <param name="parameters">The message parameters.</param>
|
|
/// <remarks>Parameters with a null or empty key are always ignored.</remarks>
|
|
public OpenIddictMessage(IEnumerable<KeyValuePair<string, OpenIddictParameter>> parameters)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(parameters);
|
|
|
|
foreach (var parameter in parameters)
|
|
{
|
|
// Ignore parameters whose name is null or empty.
|
|
if (string.IsNullOrEmpty(parameter.Key))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (Parameters.ContainsKey(parameter.Key))
|
|
{
|
|
throw new ArgumentException(SR.GetResourceString(SR.ID0191), nameof(parameters));
|
|
}
|
|
|
|
Parameters.Add(parameter.Key, parameter.Value);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new OpenIddict message.
|
|
/// </summary>
|
|
/// <param name="parameters">The message parameters.</param>
|
|
/// <remarks>Parameters with a null or empty key are always ignored.</remarks>
|
|
public OpenIddictMessage(IEnumerable<KeyValuePair<string, string?>> parameters)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(parameters);
|
|
|
|
foreach (var parameter in parameters.GroupBy(parameter => parameter.Key, StringComparer.Ordinal))
|
|
{
|
|
// Ignore parameters whose name is null or empty.
|
|
if (string.IsNullOrEmpty(parameter.Key))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Note: the core OAuth 2.0 specification requires that request parameters
|
|
// not be present more than once but derived specifications like the
|
|
// token exchange specification deliberately allow specifying multiple
|
|
// parameters with the same name to represent a multi-valued parameter.
|
|
Parameters.Add(parameter.Key, parameter.Select(parameter => parameter.Value).ToArray() switch
|
|
{
|
|
[] => default,
|
|
[string value] => new OpenIddictParameter(value),
|
|
[..] values => new(ImmutableCollectionsMarshal.AsImmutableArray(values))
|
|
});
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new OpenIddict message.
|
|
/// </summary>
|
|
/// <param name="parameters">The message parameters.</param>
|
|
/// <remarks>Parameters with a null or empty key are always ignored.</remarks>
|
|
public OpenIddictMessage(IEnumerable<KeyValuePair<string, ImmutableArray<string?>>> parameters)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(parameters);
|
|
|
|
foreach (var parameter in parameters)
|
|
{
|
|
// Ignore parameters whose name is null or empty.
|
|
if (string.IsNullOrEmpty(parameter.Key))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Note: the core OAuth 2.0 specification requires that request parameters
|
|
// not be present more than once but derived specifications like the
|
|
// token exchange specification deliberately allow specifying multiple
|
|
// parameters with the same name to represent a multi-valued parameter.
|
|
Parameters.Add(parameter.Key, parameter.Value switch
|
|
{
|
|
{ IsDefaultOrEmpty: true } => default,
|
|
[string value] => new OpenIddictParameter(value),
|
|
[..] values => new OpenIddictParameter(values)
|
|
});
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new OpenIddict message.
|
|
/// </summary>
|
|
/// <param name="parameters">The message parameters.</param>
|
|
/// <remarks>Parameters with a null or empty key are always ignored.</remarks>
|
|
public OpenIddictMessage(IEnumerable<KeyValuePair<string, StringValues>> parameters)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(parameters);
|
|
|
|
foreach (var parameter in parameters)
|
|
{
|
|
// Ignore parameters whose name is null or empty.
|
|
if (string.IsNullOrEmpty(parameter.Key))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Note: the core OAuth 2.0 specification requires that request parameters
|
|
// not be present more than once but derived specifications like the
|
|
// token exchange specification deliberately allow specifying multiple
|
|
// parameters with the same name to represent a multi-valued parameter.
|
|
Parameters.Add(parameter.Key, parameter.Value switch
|
|
{
|
|
[] => default,
|
|
[string value] => new OpenIddictParameter(value),
|
|
[..] values => new(ImmutableCollectionsMarshal.AsImmutableArray(values.ToArray()))
|
|
});
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new OpenIddict message.
|
|
/// </summary>
|
|
/// <param name="parameters">The message parameters.</param>
|
|
/// <remarks>Parameters with a null or empty key are always ignored.</remarks>
|
|
public OpenIddictMessage(NameValueCollection parameters)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(parameters);
|
|
|
|
for (var index = 0; index < parameters.AllKeys.Length; index++)
|
|
{
|
|
// Ignore parameters whose name is null or empty.
|
|
var name = parameters.AllKeys[index];
|
|
if (string.IsNullOrEmpty(name))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Note: the core OAuth 2.0 specification requires that request parameters
|
|
// not be present more than once but derived specifications like the
|
|
// token exchange specification deliberately allow specifying multiple
|
|
// parameters with the same name to represent a multi-valued parameter.
|
|
Parameters.Add(name, parameters.GetValues(name) switch
|
|
{
|
|
null or [] => default,
|
|
[string value] => new OpenIddictParameter(value),
|
|
[..] values => new(ImmutableCollectionsMarshal.AsImmutableArray<string?>(values))
|
|
});
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets a parameter.
|
|
/// </summary>
|
|
/// <param name="name">The parameter name.</param>
|
|
/// <returns>The parameter value.</returns>
|
|
public OpenIddictParameter? this[string name]
|
|
{
|
|
get => GetParameter(name);
|
|
set => SetParameter(name, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the number of parameters contained in the current message.
|
|
/// </summary>
|
|
public int Count => Parameters.Count;
|
|
|
|
/// <summary>
|
|
/// Gets the dictionary containing the parameters.
|
|
/// </summary>
|
|
protected Dictionary<string, OpenIddictParameter> Parameters { get; }
|
|
= new Dictionary<string, OpenIddictParameter>(StringComparer.Ordinal);
|
|
|
|
/// <summary>
|
|
/// Adds a parameter. Note: an exception is thrown if a parameter with the same name was already added.
|
|
/// </summary>
|
|
/// <param name="name">The parameter name.</param>
|
|
/// <param name="value">The parameter value.</param>
|
|
/// <returns>The current instance, which allows chaining calls.</returns>
|
|
public OpenIddictMessage AddParameter(string name, OpenIddictParameter value)
|
|
{
|
|
ArgumentException.ThrowIfNullOrEmpty(name);
|
|
|
|
if (Parameters.ContainsKey(name))
|
|
{
|
|
throw new ArgumentException(SR.GetResourceString(SR.ID0191), nameof(name));
|
|
}
|
|
|
|
Parameters.Add(name, value);
|
|
|
|
return this;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the value corresponding to a given parameter.
|
|
/// </summary>
|
|
/// <param name="name">The parameter name.</param>
|
|
/// <returns>The parameter value, or <see langword="null"/> if it cannot be found.</returns>
|
|
public OpenIddictParameter? GetParameter(string name)
|
|
=> TryGetParameter(name, out var parameter) ? parameter : (OpenIddictParameter?) null;
|
|
|
|
/// <summary>
|
|
/// Gets all the parameters associated with this instance.
|
|
/// </summary>
|
|
/// <returns>The parameters associated with this instance.</returns>
|
|
public IReadOnlyDictionary<string, OpenIddictParameter> GetParameters()
|
|
=> new ReadOnlyDictionary<string, OpenIddictParameter>(Parameters);
|
|
|
|
/// <summary>
|
|
/// Determines whether the current message contains the specified parameter.
|
|
/// </summary>
|
|
/// <param name="name">The parameter name.</param>
|
|
/// <returns><see langword="true"/> if the parameter is present, <see langword="false"/> otherwise.</returns>
|
|
public bool HasParameter(string name)
|
|
{
|
|
ArgumentException.ThrowIfNullOrEmpty(name);
|
|
|
|
return Parameters.ContainsKey(name);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes a parameter.
|
|
/// </summary>
|
|
/// <param name="name">The parameter name.</param>
|
|
/// <returns>The current instance, which allows chaining calls.</returns>
|
|
public OpenIddictMessage RemoveParameter(string name)
|
|
{
|
|
ArgumentException.ThrowIfNullOrEmpty(name);
|
|
|
|
Parameters.Remove(name);
|
|
|
|
return this;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds, replaces or removes a parameter.
|
|
/// Note: this method automatically removes empty parameters.
|
|
/// </summary>
|
|
/// <param name="name">The parameter name.</param>
|
|
/// <param name="value">The parameter value.</param>
|
|
/// <returns>The current instance, which allows chaining calls.</returns>
|
|
public OpenIddictMessage SetParameter(string name, OpenIddictParameter? value)
|
|
{
|
|
ArgumentException.ThrowIfNullOrEmpty(name);
|
|
|
|
// If the parameter value is null or empty, remove the corresponding entry from the collection.
|
|
if (value is null || OpenIddictParameter.IsNullOrEmpty(value.GetValueOrDefault()))
|
|
{
|
|
Parameters.Remove(name);
|
|
}
|
|
|
|
else
|
|
{
|
|
Parameters[name] = value.GetValueOrDefault();
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tries to get the value corresponding to a given parameter.
|
|
/// </summary>
|
|
/// <param name="name">The parameter name.</param>
|
|
/// <param name="value">The parameter value.</param>
|
|
/// <returns><see langword="true"/> if the parameter could be found, <see langword="false"/> otherwise.</returns>
|
|
public bool TryGetParameter(string name, out OpenIddictParameter value)
|
|
{
|
|
ArgumentException.ThrowIfNullOrEmpty(name);
|
|
|
|
return Parameters.TryGetValue(name, out value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a <see cref="string"/> representation of the current instance that can be used in logs.
|
|
/// Note: sensitive parameters like client secrets are automatically removed for security reasons.
|
|
/// </summary>
|
|
/// <returns>The indented JSON representation corresponding to this message.</returns>
|
|
public override string ToString()
|
|
{
|
|
using var stream = new MemoryStream();
|
|
using var writer = new Utf8JsonWriter(stream, new JsonWriterOptions
|
|
{
|
|
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
|
Indented = true
|
|
});
|
|
|
|
writer.WriteStartObject();
|
|
|
|
foreach (var parameter in Parameters)
|
|
{
|
|
writer.WritePropertyName(parameter.Key);
|
|
|
|
// Remove sensitive parameters from the generated payload.
|
|
switch (parameter.Key)
|
|
{
|
|
case OpenIddictConstants.Parameters.AccessToken:
|
|
case OpenIddictConstants.Parameters.Assertion:
|
|
case OpenIddictConstants.Parameters.ClientAssertion:
|
|
case OpenIddictConstants.Parameters.ClientSecret:
|
|
case OpenIddictConstants.Parameters.Code:
|
|
case OpenIddictConstants.Parameters.IdToken:
|
|
case OpenIddictConstants.Parameters.IdTokenHint:
|
|
case OpenIddictConstants.Parameters.Password:
|
|
case OpenIddictConstants.Parameters.RefreshToken:
|
|
case OpenIddictConstants.Parameters.Token:
|
|
case { Length: > 6 } name when name.EndsWith("_token", StringComparison.OrdinalIgnoreCase):
|
|
writer.WriteStringValue("[redacted]");
|
|
continue;
|
|
}
|
|
|
|
parameter.Value.WriteTo(writer);
|
|
}
|
|
|
|
writer.WriteEndObject();
|
|
writer.Flush();
|
|
|
|
return Encoding.UTF8.GetString(stream.ToArray());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes the message to the specified JSON writer.
|
|
/// </summary>
|
|
/// <param name="writer">The UTF-8 JSON writer.</param>
|
|
public void WriteTo(Utf8JsonWriter writer)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(writer);
|
|
|
|
writer.WriteStartObject();
|
|
|
|
foreach (var parameter in Parameters)
|
|
{
|
|
writer.WritePropertyName(parameter.Key);
|
|
parameter.Value.WriteTo(writer);
|
|
}
|
|
|
|
writer.WriteEndObject();
|
|
}
|
|
}
|
|
|