using System.Security.Claims; using System.Text; using Microsoft.Extensions.Primitives; using Microsoft.IdentityModel.Tokens; namespace OpenIddict.Extensions; /// /// Exposes common helpers used by the OpenIddict assemblies. /// internal static class OpenIddictHelpers { /// /// Finds the first base type that matches the specified generic type definition. /// /// The type to introspect. /// The generic type definition. /// A instance if the base type was found, otherwise. public static Type? FindGenericBaseType(Type type, Type definition) => FindGenericBaseTypes(type, definition).FirstOrDefault(); /// /// Finds all the base types that matches the specified generic type definition. /// /// The type to introspect. /// The generic type definition. /// A instance if the base type was found, otherwise. public static IEnumerable FindGenericBaseTypes(Type type, Type definition) { if (type is null) { throw new ArgumentNullException(nameof(type)); } if (definition is null) { throw new ArgumentNullException(nameof(definition)); } if (!definition.IsGenericTypeDefinition) { throw new ArgumentException(SR.GetResourceString(SR.ID0263), nameof(definition)); } if (definition.IsInterface) { foreach (var contract in type.GetInterfaces()) { if (!contract.IsGenericType && !contract.IsConstructedGenericType) { continue; } if (contract.GetGenericTypeDefinition() == definition) { yield return contract; } } } else { for (var candidate = type; candidate is not null; candidate = candidate.BaseType) { if (!candidate.IsGenericType && !candidate.IsConstructedGenericType) { continue; } if (candidate.GetGenericTypeDefinition() == definition) { yield return candidate; } } } } /// /// Adds a query string parameter to the specified . /// /// The address, to which the query string parameter will be appended. /// The name of the query string parameter to append. /// The value of the query string parameter to append. /// The final instance, with the specified parameter appended. public static Uri AddQueryStringParameter(Uri address, string name, string? value) { if (address is null) { throw new ArgumentNullException(nameof(address)); } var builder = new StringBuilder(address.Query); if (builder.Length > 0) { builder.Append('&'); } builder.Append(Uri.EscapeDataString(name)); if (!string.IsNullOrEmpty(value)) { builder.Append('='); builder.Append(Uri.EscapeDataString(value)); } return new UriBuilder(address) { Query = builder.ToString() }.Uri; } /// /// Adds query string parameters to the specified . /// /// The address, to which the query string parameters will be appended. /// The query string parameters to append. /// The final instance, with the specified parameters appended. /// is . /// is . public static Uri AddQueryStringParameters(Uri address, IReadOnlyDictionary parameters) { if (address is null) { throw new ArgumentNullException(nameof(address)); } if (parameters is null) { throw new ArgumentNullException(nameof(parameters)); } if (parameters.Count is 0) { return address; } var builder = new StringBuilder(address.Query); foreach (var parameter in parameters) { // If the parameter doesn't include any string value, // only append the parameter key to the query string. if (parameter.Value.Count is 0) { if (builder.Length > 0) { builder.Append('&'); } builder.Append(Uri.EscapeDataString(parameter.Key)); } // Otherwise, iterate the string values and create // a new "name=value" pair for each iterated value. else { foreach (var value in parameter.Value) { if (builder.Length > 0) { builder.Append('&'); } builder.Append(Uri.EscapeDataString(parameter.Key)); if (!string.IsNullOrEmpty(value)) { builder.Append('='); builder.Append(Uri.EscapeDataString(value)); } } } } return new UriBuilder(address) { Query = builder.ToString() }.Uri; } /// /// Extracts the parameters from the specified query string. /// /// The query string, which may start with a '?'. /// The parameters extracted from the specified query string. /// is . public static IReadOnlyDictionary ParseQuery(string query) { if (query is null) { throw new ArgumentNullException(nameof(query)); } 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 => ( 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())); } /// /// Creates a merged principal based on the specified principals. /// /// The collection of principals to merge. /// The merged principal. public static ClaimsPrincipal CreateMergedPrincipal(params ClaimsPrincipal?[] principals) { // Note: components like the client handler can be used as a pure OAuth 2.0 stack for // delegation scenarios where the identity of the user is not needed. In this case, // since no principal can be resolved from a token or a userinfo response to construct // a user identity, a fake one containing an "unauthenticated" identity (i.e with its // AuthenticationType property deliberately left to null) is used to allow the host // to return a "successful" authentication result for these delegation-only scenarios. if (!principals.Any(principal => principal?.Identity is ClaimsIdentity { IsAuthenticated: true })) { return new ClaimsPrincipal(new ClaimsIdentity()); } // Create a new composite identity containing the claims of all the principals. var identity = new ClaimsIdentity(TokenValidationParameters.DefaultAuthenticationType); foreach (var principal in principals) { // Note: the principal may be null if no value was extracted from the corresponding token. if (principal is null) { continue; } foreach (var claim in principal.Claims) { // If a claim with the same type and the same value already exist, skip it. if (identity.HasClaim(claim.Type, claim.Value)) { continue; } identity.AddClaim(claim); } } return new ClaimsPrincipal(identity); } }