Skip to content

Commit 06c3106

Browse files
pmaytakmsbw2Keegan CarusobrentschmaltzHP712
authored
Cherry pick CaseSensitiveClaimsIdentity to 6x. (#2700) (#2710)
* 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 <[email protected]> * 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 <[email protected]> Co-authored-by: Keegan Caruso <[email protected]> * 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 <[email protected]> * Update src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.cs --------- Co-authored-by: msbw2 <[email protected]> Co-authored-by: Keegan Caruso <[email protected]> Co-authored-by: BrentSchmaltz <[email protected]> Co-authored-by: id4s <[email protected]>
1 parent d621d23 commit 06c3106

File tree

12 files changed

+663
-6
lines changed

12 files changed

+663
-6
lines changed

src/Microsoft.IdentityModel.TestExtensions/TestTokenCreator.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,7 @@ public SecurityTokenDescriptor CreateTokenDescriptorWithInstanceOverrides()
357357
{
358358
var securityTokenDescriptor = new SecurityTokenDescriptor()
359359
{
360-
Subject = new ClaimsIdentity(_payloadClaims),
360+
Subject = ClaimsIdentityFactory.Create(_payloadClaims),
361361
};
362362

363363
if (!string.IsNullOrEmpty(Issuer))

src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -868,7 +868,7 @@ protected virtual void SetDelegateFromAttribute(SamlAttribute attribute, ClaimsI
868868
}
869869
}
870870

871-
subject.Actor = new ClaimsIdentity(claims, "Federation");
871+
subject.Actor = ClaimsIdentityFactory.Create(claims, "Federation");
872872
SetDelegateFromAttribute(actingAsAttribute, subject.Actor, issuer);
873873
}
874874

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1110,7 +1110,7 @@ protected virtual void SetClaimsIdentityActorFromAttribute(Saml2Attribute attrib
11101110
}
11111111
}
11121112

1113-
identity.Actor = new ClaimsIdentity(claims);
1113+
identity.Actor = ClaimsIdentityFactory.Create(claims);
11141114
SetClaimsIdentityActorFromAttribute(actorAttribute, identity.Actor, issuer);
11151115
}
11161116

@@ -1294,6 +1294,7 @@ protected virtual ClaimsIdentity CreateClaimsIdentity(Saml2SecurityToken samlTok
12941294
}
12951295

12961296
var identity = validationParameters.CreateClaimsIdentity(samlToken, actualIssuer);
1297+
12971298
ProcessSubject(samlToken.Assertion.Subject, identity, actualIssuer);
12981299
ProcessStatements(samlToken.Assertion.Statements, identity, actualIssuer);
12991300

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Security.Claims;
6+
7+
namespace Microsoft.IdentityModel.Tokens
8+
{
9+
/// <summary>
10+
/// AppContext switches for Microsoft.IdentityModel.Tokens and referencing packages.
11+
/// </summary>
12+
internal static class AppContextSwitches
13+
{
14+
/// <summary>
15+
/// Enables a new behavior of using <see cref="CaseSensitiveClaimsIdentity"/> instead of <see cref="ClaimsIdentity"/> globally.
16+
/// </summary>
17+
internal const string UseCaseSensitiveClaimsIdentityTypeSwitch = "Microsoft.IdentityModel.Tokens.UseCaseSensitiveClaimsIdentityType";
18+
19+
#if NET46_OR_GREATER || NETCOREAPP || NETSTANDARD
20+
internal static bool UseCaseSensitiveClaimsIdentityType() => AppContext.TryGetSwitch(UseCaseSensitiveClaimsIdentityTypeSwitch, out bool useCaseSensitiveClaimsIdentityType) && useCaseSensitiveClaimsIdentityType;
21+
22+
#else
23+
// .NET 4.5 does not support AppContext switches. Always use ClaimsIdentity.
24+
internal static bool UseCaseSensitiveClaimsIdentityType() => false;
25+
#endif
26+
}
27+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Security.Claims;
7+
8+
namespace Microsoft.IdentityModel.Tokens
9+
{
10+
/// <summary>
11+
/// 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"/>.
12+
/// </summary>
13+
public class CaseSensitiveClaimsIdentity : ClaimsIdentity
14+
{
15+
/// <summary>
16+
/// Gets the <see cref="SecurityToken"/> associated with this claims identity.
17+
/// </summary>
18+
public SecurityToken SecurityToken { get; internal set; }
19+
20+
/// <summary>
21+
/// Initializes an instance of <see cref="CaseSensitiveClaimsIdentity"/>.
22+
/// </summary>
23+
public CaseSensitiveClaimsIdentity() : base()
24+
{
25+
}
26+
27+
/// <summary>
28+
/// Initializes an instance of <see cref="CaseSensitiveClaimsIdentity"/>.
29+
/// </summary>
30+
/// <param name="authenticationType">The authentication method used to establish this identity.</param>
31+
public CaseSensitiveClaimsIdentity(string authenticationType) : base(authenticationType)
32+
{
33+
}
34+
35+
/// <summary>
36+
/// Initializes an instance of <see cref="CaseSensitiveClaimsIdentity"/>.
37+
/// </summary>
38+
/// <param name="claimsIdentity"><see cref="ClaimsIdentity"/> to copy.</param>
39+
public CaseSensitiveClaimsIdentity(ClaimsIdentity claimsIdentity) : base(claimsIdentity)
40+
{
41+
}
42+
43+
/// <summary>
44+
/// Initializes an instance of <see cref="CaseSensitiveClaimsIdentity"/>.
45+
/// </summary>
46+
/// <param name="claims"><see cref="IEnumerable{Claim}"/> associated with this instance.</param>
47+
public CaseSensitiveClaimsIdentity(IEnumerable<Claim> claims) : base(claims)
48+
{
49+
}
50+
51+
/// <summary>
52+
/// Initializes an instance of <see cref="CaseSensitiveClaimsIdentity"/>.
53+
/// </summary>
54+
/// <param name="claims"><see cref="IEnumerable{Claim}"/> associated with this instance.</param>
55+
/// <param name="authenticationType">The authentication method used to establish this identity.</param>
56+
public CaseSensitiveClaimsIdentity(IEnumerable<Claim> claims, string authenticationType) : base(claims, authenticationType)
57+
{
58+
}
59+
60+
/// <summary>
61+
/// Initializes an instance of <see cref="CaseSensitiveClaimsIdentity"/>.
62+
/// </summary>
63+
/// <param name="claims"><see cref="IEnumerable{Claim}"/> associated with this instance.</param>
64+
/// <param name="authenticationType">The authentication method used to establish this identity.</param>
65+
/// <param name="nameType">The <see cref="Claim.Type"/> used when obtaining the value of <see cref="ClaimsIdentity.Name"/>.</param>
66+
/// <param name="roleType">The <see cref="Claim.Type"/> used when performing logic for <see cref="ClaimsPrincipal.IsInRole"/>.</param>
67+
public CaseSensitiveClaimsIdentity(IEnumerable<Claim> claims, string authenticationType, string nameType, string roleType) :
68+
base(claims, authenticationType, nameType, roleType)
69+
{
70+
}
71+
72+
/// <summary>
73+
/// Initializes an instance of <see cref="CaseSensitiveClaimsIdentity"/>.
74+
/// </summary>
75+
/// <param name="authenticationType">The authentication method used to establish this identity.</param>
76+
/// <param name="nameType">The <see cref="Claim.Type"/> used when obtaining the value of <see cref="ClaimsIdentity.Name"/>.</param>
77+
/// <param name="roleType">The <see cref="Claim.Type"/> used when performing logic for <see cref="ClaimsPrincipal.IsInRole"/>.</param>
78+
public CaseSensitiveClaimsIdentity(string authenticationType, string nameType, string roleType) :
79+
base(authenticationType, nameType, roleType)
80+
{
81+
}
82+
83+
/// <summary>
84+
/// Retrieves a <see cref="IEnumerable{Claim}"/> where each <see cref="Claim.Type"/> equals <paramref name="type"/>.
85+
/// </summary>
86+
/// <param name="type">The type of the claim to match.</param>
87+
/// <returns>A <see cref="IEnumerable{Claim}"/> of matched claims.</returns>
88+
/// <remarks>Comparison is <see cref="StringComparison.Ordinal"/>.</remarks>
89+
/// <exception cref="ArgumentNullException">if <paramref name="type"/> is null.</exception>
90+
public override IEnumerable<Claim> FindAll(string type)
91+
{
92+
return base.FindAll(claim => claim?.Type.Equals(type, StringComparison.Ordinal) == true);
93+
}
94+
95+
/// <summary>
96+
/// Retrieves the first <see cref="Claim"/> where <see cref="Claim.Type"/> equals <paramref name="type"/>.
97+
/// </summary>
98+
/// <param name="type">The type of the claim to match.</param>
99+
/// <returns>A <see cref="Claim"/>, <see langword="null"/> if nothing matches.</returns>
100+
/// <remarks>Comparison is <see cref="StringComparison.Ordinal"/>.</remarks>
101+
/// <exception cref="ArgumentNullException">if <paramref name="type"/> is null.</exception>
102+
public override Claim FindFirst(string type)
103+
{
104+
return base.FindFirst(claim => claim?.Type.Equals(type, StringComparison.Ordinal) == true);
105+
}
106+
107+
/// <summary>
108+
/// Determines if a claim with type AND value is contained within this claims identity.
109+
/// </summary>
110+
/// <param name="type">The type of the claim to match.</param>
111+
/// <param name="value">The value of the claim to match.</param>
112+
/// <returns><c>true</c> if a claim is matched, <c>false</c> otherwise.</returns>
113+
/// <remarks>Comparison is <see cref="StringComparison.Ordinal"/> for <see cref="Claim.Type"/> and <see cref="Claim.Value"/>.</remarks>
114+
/// <exception cref="ArgumentNullException">if <paramref name="type"/> is null.</exception>
115+
/// <exception cref="ArgumentNullException">if <paramref name="value"/> is null.</exception>
116+
public override bool HasClaim(string type, string value)
117+
{
118+
return base.HasClaim(claim => claim?.Type.Equals(type, StringComparison.Ordinal) == true
119+
&& claim?.Value.Equals(value, StringComparison.Ordinal) == true);
120+
}
121+
}
122+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System.Collections.Generic;
5+
using System.Security.Claims;
6+
7+
namespace Microsoft.IdentityModel.Tokens
8+
{
9+
/// <summary>
10+
/// Facilitates the creation of <see cref="ClaimsIdentity"/> and <see cref="CaseSensitiveClaimsIdentity"/> instances based on the <see cref="AppContextSwitches.UseCaseSensitiveClaimsIdentityTypeSwitch"/>.
11+
/// </summary>
12+
internal static class ClaimsIdentityFactory
13+
{
14+
internal static ClaimsIdentity Create(IEnumerable<Claim> claims)
15+
{
16+
if (AppContextSwitches.UseCaseSensitiveClaimsIdentityType())
17+
return new CaseSensitiveClaimsIdentity(claims);
18+
19+
return new ClaimsIdentity(claims);
20+
}
21+
22+
internal static ClaimsIdentity Create(IEnumerable<Claim> claims, string authenticationType)
23+
{
24+
if (AppContextSwitches.UseCaseSensitiveClaimsIdentityType())
25+
return new CaseSensitiveClaimsIdentity(claims, authenticationType);
26+
27+
return new ClaimsIdentity(claims, authenticationType);
28+
}
29+
30+
internal static ClaimsIdentity Create(string authenticationType, string nameType, string roleType, SecurityToken securityToken)
31+
{
32+
if (AppContextSwitches.UseCaseSensitiveClaimsIdentityType())
33+
return new CaseSensitiveClaimsIdentity(authenticationType: authenticationType, nameType: nameType, roleType: roleType)
34+
{
35+
SecurityToken = securityToken,
36+
};
37+
38+
return new ClaimsIdentity(authenticationType: authenticationType, nameType: nameType, roleType: roleType);
39+
}
40+
}
41+
}

src/Microsoft.IdentityModel.Tokens/TokenHandler.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ public abstract class TokenHandler
3131
/// <exception cref="ArgumentOutOfRangeException">'value' less than 1.</exception>
3232
public virtual int MaximumTokenSizeInBytes
3333
{
34-
get => _maximumTokenSizeInBytes;
35-
set => _maximumTokenSizeInBytes = (value < 1) ? throw LogExceptionMessage(new ArgumentOutOfRangeException(nameof(value), FormatInvariant(LogMessages.IDX10101, LogHelper.MarkAsNonPII(value)))) : value;
34+
get => _maximumTokenSizeInBytes;
35+
set => _maximumTokenSizeInBytes = (value < 1) ? throw LogExceptionMessage(new ArgumentOutOfRangeException(nameof(value), FormatInvariant(LogMessages.IDX10101, LogHelper.MarkAsNonPII(value)))) : value;
3636
}
3737

3838
/// <summary>

src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -422,7 +422,7 @@ public virtual ClaimsIdentity CreateClaimsIdentity(SecurityToken securityToken,
422422
}
423423

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

428428
/// <summary>
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Security.Claims;
6+
using Microsoft.IdentityModel.TestUtils;
7+
using Microsoft.IdentityModel.Tokens;
8+
using Xunit;
9+
10+
namespace Microsoft.IdentityModel.JsonWebTokens.Tests
11+
{
12+
[Collection(nameof(JsonWebTokenHandlerClaimsIdentityTests))]
13+
public class JsonWebTokenHandlerClaimsIdentityTests
14+
{
15+
16+
#if NET46_OR_GREATER || NETCOREAPP || NETSTANDARD
17+
[Fact]
18+
public void CreateClaimsIdentity_ReturnsCaseSensitveClaimsIdentity_WithAppContextSwitch()
19+
{
20+
AppContext.SetSwitch(AppContextSwitches.UseCaseSensitiveClaimsIdentityTypeSwitch, true);
21+
22+
var handler = new DerivedJsonWebTokenHandler();
23+
var jsonWebToken = new JsonWebToken(Default.Jwt(Default.SecurityTokenDescriptor()));
24+
var tokenValidationParameters = new TokenValidationParameters();
25+
26+
var actualClaimsIdentity = handler.CreateClaimsIdentity(jsonWebToken, tokenValidationParameters);
27+
Assert.IsType<CaseSensitiveClaimsIdentity>(actualClaimsIdentity);
28+
Assert.NotNull(((CaseSensitiveClaimsIdentity)actualClaimsIdentity).SecurityToken);
29+
30+
actualClaimsIdentity = handler.CreateClaimsIdentity(jsonWebToken, tokenValidationParameters, Default.Issuer);
31+
Assert.IsType<CaseSensitiveClaimsIdentity>(actualClaimsIdentity);
32+
Assert.NotNull(((CaseSensitiveClaimsIdentity)actualClaimsIdentity).SecurityToken);
33+
34+
actualClaimsIdentity = handler.CreateClaimsIdentityInternal(jsonWebToken, tokenValidationParameters, Default.Issuer);
35+
Assert.IsType<CaseSensitiveClaimsIdentity>(actualClaimsIdentity);
36+
Assert.NotNull(((CaseSensitiveClaimsIdentity)actualClaimsIdentity).SecurityToken);
37+
38+
// This will also test mapped claims flow.
39+
handler.MapInboundClaims = true;
40+
actualClaimsIdentity = handler.CreateClaimsIdentityInternal(jsonWebToken, tokenValidationParameters, Default.Issuer);
41+
Assert.IsType<CaseSensitiveClaimsIdentity>(actualClaimsIdentity);
42+
Assert.NotNull(((CaseSensitiveClaimsIdentity)actualClaimsIdentity).SecurityToken);
43+
44+
AppContext.SetSwitch(AppContextSwitches.UseCaseSensitiveClaimsIdentityTypeSwitch, false);
45+
}
46+
#endif
47+
48+
[Fact]
49+
public void CreateClaimsIdentity_ReturnsClaimsIdentity_ByDefault()
50+
{
51+
var handler = new DerivedJsonWebTokenHandler();
52+
var jsonWebToken = new JsonWebToken(Default.Jwt(Default.SecurityTokenDescriptor()));
53+
var tokenValidationParameters = new TokenValidationParameters();
54+
55+
Assert.IsType<ClaimsIdentity>(handler.CreateClaimsIdentity(jsonWebToken, tokenValidationParameters));
56+
Assert.IsType<ClaimsIdentity>(handler.CreateClaimsIdentity(jsonWebToken, tokenValidationParameters, Default.Issuer));
57+
Assert.IsType<ClaimsIdentity>(handler.CreateClaimsIdentityInternal(jsonWebToken, tokenValidationParameters, Default.Issuer));
58+
// This will also test mapped claims flow.
59+
handler.MapInboundClaims = true;
60+
Assert.IsType<ClaimsIdentity>(handler.CreateClaimsIdentityInternal(jsonWebToken, tokenValidationParameters, Default.Issuer));
61+
}
62+
63+
private class DerivedJsonWebTokenHandler : JsonWebTokenHandler
64+
{
65+
public new ClaimsIdentity CreateClaimsIdentity(JsonWebToken jwtToken, TokenValidationParameters validationParameters) => base.CreateClaimsIdentity(jwtToken, validationParameters);
66+
public new ClaimsIdentity CreateClaimsIdentity(JsonWebToken jwtToken, TokenValidationParameters validationParameters, string issuer) => base.CreateClaimsIdentity(jwtToken, validationParameters, issuer);
67+
public new ClaimsIdentity CreateClaimsIdentityInternal(SecurityToken securityToken, TokenValidationParameters tokenValidationParameters, string issuer) => base.CreateClaimsIdentityInternal(securityToken, tokenValidationParameters, issuer);
68+
}
69+
}
70+
}

test/Microsoft.IdentityModel.TestUtils/IdentityComparer.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ public class IdentityComparer
4444
{ typeof(Dictionary<string, object>.ValueCollection).ToString(), AreValueCollectionsEqual },
4545
{ typeof(IEnumerable<Claim>).ToString(), AreClaimsEnumsEqual },
4646
{ typeof(IEnumerable<ClaimsIdentity>).ToString(), AreClaimsIdentitiesEnumsEqual },
47+
{ typeof(IEnumerable<CaseSensitiveClaimsIdentity>).ToString(), AreClaimsIdentitiesEnumsEqual },
4748
{ typeof(IEnumerable<object>).ToString(), AreObjectEnumsEqual },
4849
{ typeof(IEnumerable<SecurityKey>).ToString(), AreSecurityKeyEnumsEqual },
4950
{ typeof(IEnumerable<string>).ToString(), AreStringEnumsEqual },
@@ -67,6 +68,7 @@ public class IdentityComparer
6768
{ typeof(byte[]).ToString(), AreBytesEqual },
6869
{ typeof(Claim).ToString(), CompareAllPublicProperties },
6970
{ typeof(ClaimsIdentity).ToString(), CompareAllPublicProperties },
71+
{ typeof(CaseSensitiveClaimsIdentity).ToString(), CompareAllPublicProperties },
7072
{ typeof(ClaimsPrincipal).ToString(), CompareAllPublicProperties },
7173
{ typeof(ExclusiveCanonicalizationTransform).ToString(), CompareAllPublicProperties },
7274
{ typeof(CanonicalizingTransfrom).ToString(), CompareAllPublicProperties },
@@ -1050,6 +1052,12 @@ public static bool CompareAllPublicProperties(object obj1, object obj2, CompareC
10501052
continue;
10511053
}
10521054

1055+
if (type == typeof(CaseSensitiveClaimsIdentity))
1056+
{
1057+
if (propertyInfo.Name == "SecurityToken")
1058+
continue;
1059+
}
1060+
10531061
if (propertyInfo.GetMethod != null)
10541062
{
10551063
object val1 = propertyInfo.GetValue(obj1, null);

0 commit comments

Comments
 (0)