Skip to content

Commit afb9c59

Browse files
pmaytakmsbw2Keegan Caruso
authored
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]>
1 parent 863ba0f commit afb9c59

File tree

33 files changed

+801
-115
lines changed

33 files changed

+801
-115
lines changed

src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ private ClaimsIdentity CreateClaimsIdentityWithMapping(JsonWebToken jwtToken, To
212212
{
213213
_ = validationParameters ?? throw LogHelper.LogArgumentNullException(nameof(validationParameters));
214214

215-
ClaimsIdentity identity = validationParameters.CreateClaimsIdentity(jwtToken, issuer);
215+
ClaimsIdentity identity = ClaimsIdentityFactory.Create(jwtToken, validationParameters, issuer);
216216
foreach (Claim jwtClaim in jwtToken.Claims)
217217
{
218218
bool wasMapped = _inboundClaimTypeMap.TryGetValue(jwtClaim.Type, out string claimType);
@@ -281,7 +281,7 @@ private ClaimsIdentity CreateClaimsIdentityPrivate(JsonWebToken jwtToken, TokenV
281281
{
282282
_ = validationParameters ?? throw LogHelper.LogArgumentNullException(nameof(validationParameters));
283283

284-
ClaimsIdentity identity = validationParameters.CreateClaimsIdentity(jwtToken, issuer);
284+
ClaimsIdentity identity = ClaimsIdentityFactory.Create(jwtToken, validationParameters, issuer);
285285
foreach (Claim jwtClaim in jwtToken.Claims)
286286
{
287287
string claimType = jwtClaim.Type;

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: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -677,7 +677,7 @@ protected virtual IEnumerable<ClaimsIdentity> ProcessStatements(SamlSecurityToke
677677

678678
if (!identityDict.TryGetValue(statement.Subject, out ClaimsIdentity identity))
679679
{
680-
identity = validationParameters.CreateClaimsIdentity(samlToken, issuer);
680+
identity = ClaimsIdentityFactory.Create(samlToken, validationParameters, issuer);
681681
ProcessSubject(statement.Subject, identity, issuer);
682682
identityDict.Add(statement.Subject, identity);
683683
}
@@ -898,7 +898,7 @@ protected virtual void SetDelegateFromAttribute(SamlAttribute attribute, ClaimsI
898898
}
899899
}
900900

901-
subject.Actor = new ClaimsIdentity(claims, "Federation");
901+
subject.Actor = ClaimsIdentityFactory.Create(claims, "Federation");
902902
SetDelegateFromAttribute(actingAsAttribute, subject.Actor, issuer);
903903
}
904904

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1129,7 +1129,7 @@ protected virtual void SetClaimsIdentityActorFromAttribute(Saml2Attribute attrib
11291129
}
11301130
}
11311131

1132-
identity.Actor = new ClaimsIdentity(claims);
1132+
identity.Actor = ClaimsIdentityFactory.Create(claims);
11331133
SetClaimsIdentityActorFromAttribute(actorAttribute, identity.Actor, issuer);
11341134
}
11351135

@@ -1314,7 +1314,8 @@ protected virtual ClaimsIdentity CreateClaimsIdentity(Saml2SecurityToken samlTok
13141314
actualIssuer = ClaimsIdentity.DefaultIssuer;
13151315
}
13161316

1317-
var identity = validationParameters.CreateClaimsIdentity(samlToken, actualIssuer);
1317+
var identity = ClaimsIdentityFactory.Create(samlToken, validationParameters, issuer);
1318+
13181319
ProcessSubject(samlToken.Assertion.Subject, identity, actualIssuer);
13191320
ProcessStatements(samlToken.Assertion.Statements, identity, actualIssuer);
13201321

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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 fallback to the previous behavior of using <see cref="ClaimsIdentity"/> instead of <see cref="CaseSensitiveClaimsIdentity"/> globally.
16+
/// </summary>
17+
internal const string UseClaimsIdentityTypeSwitch = "Microsoft.IdentityModel.Tokens.UseClaimsIdentityType";
18+
19+
internal static bool UseClaimsIdentityType() => (AppContext.TryGetSwitch(UseClaimsIdentityTypeSwitch, out bool useClaimsIdentityType) && useClaimsIdentityType);
20+
}
21+
}
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: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
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.UseClaimsIdentityTypeSwitch"/>.
11+
/// </summary>
12+
internal static class ClaimsIdentityFactory
13+
{
14+
internal static ClaimsIdentity Create(IEnumerable<Claim> claims)
15+
{
16+
if (AppContextSwitches.UseClaimsIdentityType())
17+
return new ClaimsIdentity(claims);
18+
19+
return new CaseSensitiveClaimsIdentity(claims);
20+
}
21+
22+
internal static ClaimsIdentity Create(IEnumerable<Claim> claims, string authenticationType)
23+
{
24+
if (AppContextSwitches.UseClaimsIdentityType())
25+
return new ClaimsIdentity(claims, authenticationType);
26+
27+
return new CaseSensitiveClaimsIdentity(claims, authenticationType);
28+
}
29+
30+
internal static ClaimsIdentity Create(string authenticationType, string nameType, string roleType, SecurityToken securityToken)
31+
{
32+
if (AppContextSwitches.UseClaimsIdentityType())
33+
return new ClaimsIdentity(authenticationType: authenticationType, nameType: nameType, roleType: roleType);
34+
35+
return new CaseSensitiveClaimsIdentity(authenticationType: authenticationType, nameType: nameType, roleType: roleType)
36+
{
37+
SecurityToken = securityToken,
38+
};
39+
}
40+
41+
internal static ClaimsIdentity Create(SecurityToken securityToken, TokenValidationParameters validationParameters, string issuer)
42+
{
43+
ClaimsIdentity claimsIdentity = validationParameters.CreateClaimsIdentity(securityToken, issuer);
44+
45+
// Set the SecurityToken in cases where derived TokenValidationParameters created a CaseSensitiveClaimsIdentity.
46+
if (claimsIdentity is CaseSensitiveClaimsIdentity caseSensitiveClaimsIdentity && caseSensitiveClaimsIdentity.SecurityToken == null)
47+
{
48+
caseSensitiveClaimsIdentity.SecurityToken = securityToken;
49+
}
50+
else if (claimsIdentity is not CaseSensitiveClaimsIdentity && !AppContextSwitches.UseClaimsIdentityType())
51+
{
52+
claimsIdentity = new CaseSensitiveClaimsIdentity(claimsIdentity)
53+
{
54+
SecurityToken = securityToken,
55+
};
56+
}
57+
58+
return claimsIdentity;
59+
}
60+
61+
internal static ClaimsIdentity Create(TokenHandler tokenHandler, SecurityToken securityToken, TokenValidationParameters validationParameters, string issuer)
62+
{
63+
ClaimsIdentity claimsIdentity = tokenHandler.CreateClaimsIdentityInternal(securityToken, validationParameters, issuer);
64+
65+
// Set the SecurityToken in cases where derived TokenHandler created a CaseSensitiveClaimsIdentity.
66+
if (claimsIdentity is CaseSensitiveClaimsIdentity caseSensitiveClaimsIdentity && caseSensitiveClaimsIdentity.SecurityToken == null)
67+
{
68+
caseSensitiveClaimsIdentity.SecurityToken = securityToken;
69+
}
70+
else if (claimsIdentity is not CaseSensitiveClaimsIdentity && !AppContextSwitches.UseClaimsIdentityType())
71+
{
72+
claimsIdentity = new CaseSensitiveClaimsIdentity(claimsIdentity)
73+
{
74+
SecurityToken = securityToken,
75+
};
76+
}
77+
78+
return claimsIdentity;
79+
}
80+
}
81+
}

src/Microsoft.IdentityModel.Tokens/TokenHandler.cs

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

3737
/// <summary>
@@ -53,7 +53,6 @@ public int TokenLifetimeInMinutes
5353
}
5454

5555
#region methods
56-
5756
/// <summary>
5857
/// Validates a token.
5958
/// On a validation failure, no exception will be thrown; instead, the exception will be set in the returned TokenValidationResult.Exception property.

src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ public virtual ClaimsIdentity CreateClaimsIdentity(SecurityToken securityToken,
240240
if (LogHelper.IsEnabled(EventLogLevel.Informational))
241241
LogHelper.LogInformation(LogMessages.IDX10245, securityToken);
242242

243-
return new ClaimsIdentity(authenticationType: AuthenticationType ?? DefaultAuthenticationType, nameType: nameClaimType ?? ClaimsIdentity.DefaultNameClaimType, roleType: roleClaimType ?? ClaimsIdentity.DefaultRoleClaimType);
243+
return ClaimsIdentityFactory.Create(authenticationType: AuthenticationType ?? DefaultAuthenticationType, nameType: nameClaimType ?? ClaimsIdentity.DefaultNameClaimType, roleType: roleClaimType ?? ClaimsIdentity.DefaultRoleClaimType, securityToken);
244244
}
245245

246246
/// <summary>

src/Microsoft.IdentityModel.Tokens/Validation/TokenValidationResult.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ internal ClaimsIdentity ClaimsIdentityNoLocking
135135

136136
if (_validationParameters != null && SecurityToken != null && _tokenHandler != null && Issuer != null)
137137
{
138-
_claimsIdentity = _tokenHandler.CreateClaimsIdentityInternal(SecurityToken, _validationParameters, Issuer);
138+
_claimsIdentity = ClaimsIdentityFactory.Create(_tokenHandler, SecurityToken, _validationParameters, Issuer);
139139
}
140140

141141
_claimsIdentityInitialized = true;

0 commit comments

Comments
 (0)