Skip to content

Commit

Permalink
Cherry pick CaseSensitiveClaimsIdentity to 6x. (#2700) (#2710)
Browse files Browse the repository at this point in the history
* Add CaseSensitiveClaimsIdentity type. (#2700)

* Add CaseSensitiveClaimsIdentity. Update JsonWebTokenHandler.

* Move switch to a separate class. Update claims identity creation code.

* Add test.

* Update AppContextSwitches

* Update test/Microsoft.IdentityModel.Tokens.Tests/CaseSensitiveClaimsIdentityTests.cs

Co-authored-by: msbw2 <brettwhite@microsoft.com>

* Update comments.

* Update ClaimsIdentity code creation in src.

* Add tests.

* Update tests to use correct types.

* Add SecurityToken property to CsClaimsIdentity.

* Update tests to use CsClaimsIdentity.

* Refactor code into ClaimsIdentityFactory.

* Update tests.

* Update ClaimsIdentityFactory.

* Fix tests.

* Update tests for CaseSensitiveClaimsIdentity

* ignore SecurityToken in IdentityComparer

* Set security token in ClaimsIdentityFactory. Add tests.

* Apply suggestions from code review

* Update test.

---------

Co-authored-by: msbw2 <brettwhite@microsoft.com>
Co-authored-by: Keegan Caruso <keegancaruso@microsoft.com>

* Update.

* Update.

* Update.

* Update.

* Call TVP.CreateClaimsIdentity to support users that have overloaded. (#2716)

* Call TVP.CreateClaimsIdentity to support users that have overloaded.

* picked up SAML changes and TokenValidationResult

* updated JwtSecurityTokenHandler, reverted tests and removed method.

* touched up tests

---------

Co-authored-by: id4s <user@contoso.com>

* Update src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.cs

---------

Co-authored-by: msbw2 <brettwhite@microsoft.com>
Co-authored-by: Keegan Caruso <keegancaruso@microsoft.com>
Co-authored-by: BrentSchmaltz <brentschmaltz@hotmail.com>
Co-authored-by: id4s <user@contoso.com>
  • Loading branch information
5 people authored Jul 16, 2024
1 parent d621d23 commit 06c3106
Show file tree
Hide file tree
Showing 12 changed files with 663 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ public SecurityTokenDescriptor CreateTokenDescriptorWithInstanceOverrides()
{
var securityTokenDescriptor = new SecurityTokenDescriptor()
{
Subject = new ClaimsIdentity(_payloadClaims),
Subject = ClaimsIdentityFactory.Create(_payloadClaims),
};

if (!string.IsNullOrEmpty(Issuer))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -868,7 +868,7 @@ protected virtual void SetDelegateFromAttribute(SamlAttribute attribute, ClaimsI
}
}

subject.Actor = new ClaimsIdentity(claims, "Federation");
subject.Actor = ClaimsIdentityFactory.Create(claims, "Federation");
SetDelegateFromAttribute(actingAsAttribute, subject.Actor, issuer);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1110,7 +1110,7 @@ protected virtual void SetClaimsIdentityActorFromAttribute(Saml2Attribute attrib
}
}

identity.Actor = new ClaimsIdentity(claims);
identity.Actor = ClaimsIdentityFactory.Create(claims);
SetClaimsIdentityActorFromAttribute(actorAttribute, identity.Actor, issuer);
}

Expand Down Expand Up @@ -1294,6 +1294,7 @@ protected virtual ClaimsIdentity CreateClaimsIdentity(Saml2SecurityToken samlTok
}

var identity = validationParameters.CreateClaimsIdentity(samlToken, actualIssuer);

ProcessSubject(samlToken.Assertion.Subject, identity, actualIssuer);
ProcessStatements(samlToken.Assertion.Statements, identity, actualIssuer);

Expand Down
27 changes: 27 additions & 0 deletions src/Microsoft.IdentityModel.Tokens/AppContextSwitches.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Security.Claims;

namespace Microsoft.IdentityModel.Tokens
{
/// <summary>
/// AppContext switches for Microsoft.IdentityModel.Tokens and referencing packages.
/// </summary>
internal static class AppContextSwitches
{
/// <summary>
/// Enables a new behavior of using <see cref="CaseSensitiveClaimsIdentity"/> instead of <see cref="ClaimsIdentity"/> globally.
/// </summary>
internal const string UseCaseSensitiveClaimsIdentityTypeSwitch = "Microsoft.IdentityModel.Tokens.UseCaseSensitiveClaimsIdentityType";

#if NET46_OR_GREATER || NETCOREAPP || NETSTANDARD
internal static bool UseCaseSensitiveClaimsIdentityType() => AppContext.TryGetSwitch(UseCaseSensitiveClaimsIdentityTypeSwitch, out bool useCaseSensitiveClaimsIdentityType) && useCaseSensitiveClaimsIdentityType;

#else
// .NET 4.5 does not support AppContext switches. Always use ClaimsIdentity.
internal static bool UseCaseSensitiveClaimsIdentityType() => false;
#endif
}
}
122 changes: 122 additions & 0 deletions src/Microsoft.IdentityModel.Tokens/CaseSensitiveClaimsIdentity.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Security.Claims;

namespace Microsoft.IdentityModel.Tokens
{
/// <summary>
/// A derived <see cref="ClaimsIdentity"/> where claim retrieval is case-sensitive. The current <see cref="ClaimsIdentity"/> retrieves claims in a case-insensitive manner which is different than querying the underlying <see cref="SecurityToken"/>. The <see cref="CaseSensitiveClaimsIdentity"/> provides consistent retrieval logic between the <see cref="SecurityToken"/> and <see cref="ClaimsIdentity"/>.
/// </summary>
public class CaseSensitiveClaimsIdentity : ClaimsIdentity
{
/// <summary>
/// Gets the <see cref="SecurityToken"/> associated with this claims identity.
/// </summary>
public SecurityToken SecurityToken { get; internal set; }

/// <summary>
/// Initializes an instance of <see cref="CaseSensitiveClaimsIdentity"/>.
/// </summary>
public CaseSensitiveClaimsIdentity() : base()
{
}

/// <summary>
/// Initializes an instance of <see cref="CaseSensitiveClaimsIdentity"/>.
/// </summary>
/// <param name="authenticationType">The authentication method used to establish this identity.</param>
public CaseSensitiveClaimsIdentity(string authenticationType) : base(authenticationType)
{
}

/// <summary>
/// Initializes an instance of <see cref="CaseSensitiveClaimsIdentity"/>.
/// </summary>
/// <param name="claimsIdentity"><see cref="ClaimsIdentity"/> to copy.</param>
public CaseSensitiveClaimsIdentity(ClaimsIdentity claimsIdentity) : base(claimsIdentity)
{
}

/// <summary>
/// Initializes an instance of <see cref="CaseSensitiveClaimsIdentity"/>.
/// </summary>
/// <param name="claims"><see cref="IEnumerable{Claim}"/> associated with this instance.</param>
public CaseSensitiveClaimsIdentity(IEnumerable<Claim> claims) : base(claims)
{
}

/// <summary>
/// Initializes an instance of <see cref="CaseSensitiveClaimsIdentity"/>.
/// </summary>
/// <param name="claims"><see cref="IEnumerable{Claim}"/> associated with this instance.</param>
/// <param name="authenticationType">The authentication method used to establish this identity.</param>
public CaseSensitiveClaimsIdentity(IEnumerable<Claim> claims, string authenticationType) : base(claims, authenticationType)
{
}

/// <summary>
/// Initializes an instance of <see cref="CaseSensitiveClaimsIdentity"/>.
/// </summary>
/// <param name="claims"><see cref="IEnumerable{Claim}"/> associated with this instance.</param>
/// <param name="authenticationType">The authentication method used to establish this identity.</param>
/// <param name="nameType">The <see cref="Claim.Type"/> used when obtaining the value of <see cref="ClaimsIdentity.Name"/>.</param>
/// <param name="roleType">The <see cref="Claim.Type"/> used when performing logic for <see cref="ClaimsPrincipal.IsInRole"/>.</param>
public CaseSensitiveClaimsIdentity(IEnumerable<Claim> claims, string authenticationType, string nameType, string roleType) :
base(claims, authenticationType, nameType, roleType)
{
}

/// <summary>
/// Initializes an instance of <see cref="CaseSensitiveClaimsIdentity"/>.
/// </summary>
/// <param name="authenticationType">The authentication method used to establish this identity.</param>
/// <param name="nameType">The <see cref="Claim.Type"/> used when obtaining the value of <see cref="ClaimsIdentity.Name"/>.</param>
/// <param name="roleType">The <see cref="Claim.Type"/> used when performing logic for <see cref="ClaimsPrincipal.IsInRole"/>.</param>
public CaseSensitiveClaimsIdentity(string authenticationType, string nameType, string roleType) :
base(authenticationType, nameType, roleType)
{
}

/// <summary>
/// Retrieves a <see cref="IEnumerable{Claim}"/> where each <see cref="Claim.Type"/> equals <paramref name="type"/>.
/// </summary>
/// <param name="type">The type of the claim to match.</param>
/// <returns>A <see cref="IEnumerable{Claim}"/> of matched claims.</returns>
/// <remarks>Comparison is <see cref="StringComparison.Ordinal"/>.</remarks>
/// <exception cref="ArgumentNullException">if <paramref name="type"/> is null.</exception>
public override IEnumerable<Claim> FindAll(string type)
{
return base.FindAll(claim => claim?.Type.Equals(type, StringComparison.Ordinal) == true);
}

/// <summary>
/// Retrieves the first <see cref="Claim"/> where <see cref="Claim.Type"/> equals <paramref name="type"/>.
/// </summary>
/// <param name="type">The type of the claim to match.</param>
/// <returns>A <see cref="Claim"/>, <see langword="null"/> if nothing matches.</returns>
/// <remarks>Comparison is <see cref="StringComparison.Ordinal"/>.</remarks>
/// <exception cref="ArgumentNullException">if <paramref name="type"/> is null.</exception>
public override Claim FindFirst(string type)
{
return base.FindFirst(claim => claim?.Type.Equals(type, StringComparison.Ordinal) == true);
}

/// <summary>
/// Determines if a claim with type AND value is contained within this claims identity.
/// </summary>
/// <param name="type">The type of the claim to match.</param>
/// <param name="value">The value of the claim to match.</param>
/// <returns><c>true</c> if a claim is matched, <c>false</c> otherwise.</returns>
/// <remarks>Comparison is <see cref="StringComparison.Ordinal"/> for <see cref="Claim.Type"/> and <see cref="Claim.Value"/>.</remarks>
/// <exception cref="ArgumentNullException">if <paramref name="type"/> is null.</exception>
/// <exception cref="ArgumentNullException">if <paramref name="value"/> is null.</exception>
public override bool HasClaim(string type, string value)
{
return base.HasClaim(claim => claim?.Type.Equals(type, StringComparison.Ordinal) == true
&& claim?.Value.Equals(value, StringComparison.Ordinal) == true);
}
}
}
41 changes: 41 additions & 0 deletions src/Microsoft.IdentityModel.Tokens/ClaimsIdentityFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Collections.Generic;
using System.Security.Claims;

namespace Microsoft.IdentityModel.Tokens
{
/// <summary>
/// Facilitates the creation of <see cref="ClaimsIdentity"/> and <see cref="CaseSensitiveClaimsIdentity"/> instances based on the <see cref="AppContextSwitches.UseCaseSensitiveClaimsIdentityTypeSwitch"/>.
/// </summary>
internal static class ClaimsIdentityFactory
{
internal static ClaimsIdentity Create(IEnumerable<Claim> claims)
{
if (AppContextSwitches.UseCaseSensitiveClaimsIdentityType())
return new CaseSensitiveClaimsIdentity(claims);

return new ClaimsIdentity(claims);
}

internal static ClaimsIdentity Create(IEnumerable<Claim> claims, string authenticationType)
{
if (AppContextSwitches.UseCaseSensitiveClaimsIdentityType())
return new CaseSensitiveClaimsIdentity(claims, authenticationType);

return new ClaimsIdentity(claims, authenticationType);
}

internal static ClaimsIdentity Create(string authenticationType, string nameType, string roleType, SecurityToken securityToken)
{
if (AppContextSwitches.UseCaseSensitiveClaimsIdentityType())
return new CaseSensitiveClaimsIdentity(authenticationType: authenticationType, nameType: nameType, roleType: roleType)
{
SecurityToken = securityToken,
};

return new ClaimsIdentity(authenticationType: authenticationType, nameType: nameType, roleType: roleType);
}
}
}
4 changes: 2 additions & 2 deletions src/Microsoft.IdentityModel.Tokens/TokenHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ public abstract class TokenHandler
/// <exception cref="ArgumentOutOfRangeException">'value' less than 1.</exception>
public virtual int MaximumTokenSizeInBytes
{
get => _maximumTokenSizeInBytes;
set => _maximumTokenSizeInBytes = (value < 1) ? throw LogExceptionMessage(new ArgumentOutOfRangeException(nameof(value), FormatInvariant(LogMessages.IDX10101, LogHelper.MarkAsNonPII(value)))) : value;
get => _maximumTokenSizeInBytes;
set => _maximumTokenSizeInBytes = (value < 1) ? throw LogExceptionMessage(new ArgumentOutOfRangeException(nameof(value), FormatInvariant(LogMessages.IDX10101, LogHelper.MarkAsNonPII(value)))) : value;
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,7 @@ public virtual ClaimsIdentity CreateClaimsIdentity(SecurityToken securityToken,
}

LogHelper.LogInformation(LogMessages.IDX10245, securityToken);
return new ClaimsIdentity(authenticationType: AuthenticationType ?? DefaultAuthenticationType, nameType: nameClaimType ?? ClaimsIdentity.DefaultNameClaimType, roleType: roleClaimType ?? ClaimsIdentity.DefaultRoleClaimType);
return ClaimsIdentityFactory.Create(authenticationType: AuthenticationType ?? DefaultAuthenticationType, nameType: nameClaimType ?? ClaimsIdentity.DefaultNameClaimType, roleType: roleClaimType ?? ClaimsIdentity.DefaultRoleClaimType, securityToken);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Security.Claims;
using Microsoft.IdentityModel.TestUtils;
using Microsoft.IdentityModel.Tokens;
using Xunit;

namespace Microsoft.IdentityModel.JsonWebTokens.Tests
{
[Collection(nameof(JsonWebTokenHandlerClaimsIdentityTests))]
public class JsonWebTokenHandlerClaimsIdentityTests
{

#if NET46_OR_GREATER || NETCOREAPP || NETSTANDARD
[Fact]
public void CreateClaimsIdentity_ReturnsCaseSensitveClaimsIdentity_WithAppContextSwitch()
{
AppContext.SetSwitch(AppContextSwitches.UseCaseSensitiveClaimsIdentityTypeSwitch, true);

var handler = new DerivedJsonWebTokenHandler();
var jsonWebToken = new JsonWebToken(Default.Jwt(Default.SecurityTokenDescriptor()));
var tokenValidationParameters = new TokenValidationParameters();

var actualClaimsIdentity = handler.CreateClaimsIdentity(jsonWebToken, tokenValidationParameters);
Assert.IsType<CaseSensitiveClaimsIdentity>(actualClaimsIdentity);
Assert.NotNull(((CaseSensitiveClaimsIdentity)actualClaimsIdentity).SecurityToken);

actualClaimsIdentity = handler.CreateClaimsIdentity(jsonWebToken, tokenValidationParameters, Default.Issuer);
Assert.IsType<CaseSensitiveClaimsIdentity>(actualClaimsIdentity);
Assert.NotNull(((CaseSensitiveClaimsIdentity)actualClaimsIdentity).SecurityToken);

actualClaimsIdentity = handler.CreateClaimsIdentityInternal(jsonWebToken, tokenValidationParameters, Default.Issuer);
Assert.IsType<CaseSensitiveClaimsIdentity>(actualClaimsIdentity);
Assert.NotNull(((CaseSensitiveClaimsIdentity)actualClaimsIdentity).SecurityToken);

// This will also test mapped claims flow.
handler.MapInboundClaims = true;
actualClaimsIdentity = handler.CreateClaimsIdentityInternal(jsonWebToken, tokenValidationParameters, Default.Issuer);
Assert.IsType<CaseSensitiveClaimsIdentity>(actualClaimsIdentity);
Assert.NotNull(((CaseSensitiveClaimsIdentity)actualClaimsIdentity).SecurityToken);

AppContext.SetSwitch(AppContextSwitches.UseCaseSensitiveClaimsIdentityTypeSwitch, false);
}
#endif

[Fact]
public void CreateClaimsIdentity_ReturnsClaimsIdentity_ByDefault()
{
var handler = new DerivedJsonWebTokenHandler();
var jsonWebToken = new JsonWebToken(Default.Jwt(Default.SecurityTokenDescriptor()));
var tokenValidationParameters = new TokenValidationParameters();

Assert.IsType<ClaimsIdentity>(handler.CreateClaimsIdentity(jsonWebToken, tokenValidationParameters));
Assert.IsType<ClaimsIdentity>(handler.CreateClaimsIdentity(jsonWebToken, tokenValidationParameters, Default.Issuer));
Assert.IsType<ClaimsIdentity>(handler.CreateClaimsIdentityInternal(jsonWebToken, tokenValidationParameters, Default.Issuer));
// This will also test mapped claims flow.
handler.MapInboundClaims = true;
Assert.IsType<ClaimsIdentity>(handler.CreateClaimsIdentityInternal(jsonWebToken, tokenValidationParameters, Default.Issuer));
}

private class DerivedJsonWebTokenHandler : JsonWebTokenHandler
{
public new ClaimsIdentity CreateClaimsIdentity(JsonWebToken jwtToken, TokenValidationParameters validationParameters) => base.CreateClaimsIdentity(jwtToken, validationParameters);
public new ClaimsIdentity CreateClaimsIdentity(JsonWebToken jwtToken, TokenValidationParameters validationParameters, string issuer) => base.CreateClaimsIdentity(jwtToken, validationParameters, issuer);
public new ClaimsIdentity CreateClaimsIdentityInternal(SecurityToken securityToken, TokenValidationParameters tokenValidationParameters, string issuer) => base.CreateClaimsIdentityInternal(securityToken, tokenValidationParameters, issuer);
}
}
}
8 changes: 8 additions & 0 deletions test/Microsoft.IdentityModel.TestUtils/IdentityComparer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public class IdentityComparer
{ typeof(Dictionary<string, object>.ValueCollection).ToString(), AreValueCollectionsEqual },
{ typeof(IEnumerable<Claim>).ToString(), AreClaimsEnumsEqual },
{ typeof(IEnumerable<ClaimsIdentity>).ToString(), AreClaimsIdentitiesEnumsEqual },
{ typeof(IEnumerable<CaseSensitiveClaimsIdentity>).ToString(), AreClaimsIdentitiesEnumsEqual },
{ typeof(IEnumerable<object>).ToString(), AreObjectEnumsEqual },
{ typeof(IEnumerable<SecurityKey>).ToString(), AreSecurityKeyEnumsEqual },
{ typeof(IEnumerable<string>).ToString(), AreStringEnumsEqual },
Expand All @@ -67,6 +68,7 @@ public class IdentityComparer
{ typeof(byte[]).ToString(), AreBytesEqual },
{ typeof(Claim).ToString(), CompareAllPublicProperties },
{ typeof(ClaimsIdentity).ToString(), CompareAllPublicProperties },
{ typeof(CaseSensitiveClaimsIdentity).ToString(), CompareAllPublicProperties },
{ typeof(ClaimsPrincipal).ToString(), CompareAllPublicProperties },
{ typeof(ExclusiveCanonicalizationTransform).ToString(), CompareAllPublicProperties },
{ typeof(CanonicalizingTransfrom).ToString(), CompareAllPublicProperties },
Expand Down Expand Up @@ -1050,6 +1052,12 @@ public static bool CompareAllPublicProperties(object obj1, object obj2, CompareC
continue;
}

if (type == typeof(CaseSensitiveClaimsIdentity))
{
if (propertyInfo.Name == "SecurityToken")
continue;
}

if (propertyInfo.GetMethod != null)
{
object val1 = propertyInfo.GetValue(obj1, null);
Expand Down
Loading

0 comments on commit 06c3106

Please sign in to comment.