Skip to content

[API Proposal]: ClaimsIdentity to perform case-sensitive comparison #113562

Open
@pmaytak

Description

@pmaytak

Changes to Approved API

We previously approved this but missed a few constructor overloads. These are the suggested changes to the approved API.

namespace System.Security.Claims
{
    public partial class ClaimsIdentity
    {
        public ClaimsIdentity(
            IIdentity? identity = null,
            IEnumerable<Claim>? claims = null,
            string? authenticationType = null,
            string? nameType = null,
            string? roleType = null,
            StringComparison stringComparison = StringComparison.OrdinalIgnoreCase);

+       public ClaimsIdentity(BinaryReader reader, StringComparison stringComparison);
+       public ClaimsIdentity(ClaimsIdentity other, StringComparison stringComparison);
    }
}

Original Proposal

Background and motivation

Certain means of representing and handling of claims, like JWT, treat claim names (i.e. types) in a case-sensitive manner. Current ClaimsIdentity class in .NET compares claim names in a case-insensitive way. The issue could arise, if some application creates a claims identity and multiple claims with names that differ in casing. When retrieving this claim, the user may end up getting either of those claims and inconsistent with their expectations. For example, if there are two claims, "ID, 1" and "Id, 2"; a request to get "Id" claim will return the first one, not the second one.

The goals of these proposed changes is to make sure the users do the right and secure thing; to ensure secure behavior by making case sensitivity an intentional choice when creating a ClaimsIdentity instance; to avoid breaking existing functionality by not changing the default behavior, while still offering a secure alternative.

An earlier issue #83128 was created with a similar proposal.

API Proposal

Updates ClaimsIdentity class.

  • Adds StringComparison parameter to all relevant constructors.
    • This StringComparison value is defaulted to StringComparison.OrdinalIgnoreCase.
    • This StringComparison value is used in FindAll, FindFirst, HasClaim methods.
  • Adds overloads for FindAll, FindFirst, HasClaim methods which accepts an additional StringComparison parameter.
  • Adds Roslyn analyzer rule to suggest to use one of the above case-sensitive overloads.

Some examples of the above proposals:

// Use for claim name comparison only (not claim value)
private readonly StringComparison _stringComparison = StringComparison.OrdinalIgnoreCase;

/// <summary>
/// Initializes an instance of <see cref="ClaimsIdentity"/>.
/// </summary>
/// <param name="stringComparison">The <see cref="StringComparison"/> to use when comparing claim names.</param>
public ClaimsIdentity(StringComparison stringComparison)
    : this((IIdentity?)null, (IEnumerable<Claim>?)null, (string?)null, (string?)null, (string?)null)
{
    _stringComparison = stringComparison;
}

public virtual Claim? FindFirst(string type)
{
    ArgumentNullException.ThrowIfNull(type);

    foreach (Claim claim in Claims)
    {
        if (claim != null)
        {
            if (string.Equals(claim.Type, type, _stringComparison))
            {
                return claim;
            }
        }
    }

    return null;
}

public virtual IEnumerable<Claim> FindAll(string type)
{
    ArgumentNullException.ThrowIfNull(type);
    return Core(type);

    IEnumerable<Claim> Core(string type)
    {
        foreach (Claim claim in Claims)
        {
            if (claim != null)
            {
                if (string.Equals(claim.Type, type, _stringComparison))
                {
                    yield return claim;
                }
            }
        }
    }
}

public virtual bool HasClaim(string type, string value)
{
    ArgumentNullException.ThrowIfNull(type);
    ArgumentNullException.ThrowIfNull(value);

    foreach (Claim claim in Claims)
    {
        if (claim != null
                && string.Equals(claim.Type, type, _stringComparison)
                && string.Equals(claim.Value, value, StringComparison.Ordinal))
        {
            return true;
        }
    }

    return false;
}

API Usage

var caseSensitiveClaimsIdentity = new ClaimsIdentity(StringComparison.Ordinal);
var claim = caseSensitiveClaimsIdentity.FindFirst("claimName"); // Case-sensitive search

var caseInsensitiveClaimsIdentity = new ClaimsIdentity();
var claim = caseInsensitiveClaimsIdentity .FindFirst("claimName", StringComparison.Ordinal); // Case-sensitive search

Alternative Designs

  • Make the current ClaimsIdentity class case-sensitive by default.
    • This is the most secure way (in light of certain case-sensitive protocols); however, it is a breaking change and is impractical due to case-insensitive classes derived from the current ClaimsIdentity and with a ClaimsIdentity being a more general purpose type in concept
  • Let users use the existing FindAll, FindFirst, HasClaim methods with predicate overloads which can be customized to do a case-sensitive match.
    • This assumes the users are aware of the case-sensitivity issue and they know these overloads exist. The library should guide the users towards the best practices and optimal solutions, if possible, and not leave it to the user to figure out.
    • An analyzer suggestion can be added to use predicate overloads; however, it would require the users to add that bit of repetitive code everywhere.

Risks

  • These changes increase the ClaimsIdentity API surface area, which could make the class more complex to use.
  • It could add confusion if these case-sensitivity flags affect the add and remove operations (which currently operate by Claim object reference). Although, the same difference exists between find and add and remove operations today.

Metadata

Metadata

Assignees

Labels

api-ready-for-reviewAPI is ready for review, it is NOT ready for implementationarea-System.Securitypartner-impactThis issue impacts a partner who needs to be kept updated

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions