Skip to content
Merged
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 @@ -870,7 +870,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 @@ -1122,7 +1122,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 @@ -1308,6 +1308,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
21 changes: 21 additions & 0 deletions src/Microsoft.IdentityModel.Tokens/AppContextSwitches.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// 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.UseCaseSensitiveClaimsIdentity";

internal static bool UseCaseSensitiveClaimsIdentityType() => (AppContext.TryGetSwitch(UseCaseSensitiveClaimsIdentityTypeSwitch, out bool useCaseSensitiveClaimsIdentityType) && useCaseSensitiveClaimsIdentityType);
}
}
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);
}
}
}
5 changes: 2 additions & 3 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, MarkAsNonPII(value)))) : value;
}

/// <summary>
Expand All @@ -54,7 +54,6 @@ public int TokenLifetimeInMinutes
}

#region methods

/// <summary>
/// Validates a token.
/// On a validation failure, no exception will be thrown; instead, the exception will be set in the returned TokenValidationResult.Exception property.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ public virtual ClaimsIdentity CreateClaimsIdentity(SecurityToken securityToken,
if (LogHelper.IsEnabled(EventLogLevel.Informational))
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,74 @@
// 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
{
[Fact]
public void CreateClaimsIdentity_ReturnsClaimsIdentity_ByDefault()
{
var handler = new DerivedJsonWebTokenHandler();
var jsonWebToken = new JsonWebToken(Default.Jwt(Default.SecurityTokenDescriptor()));
var tokenValidationParameters = new TokenValidationParameters();

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

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

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

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

[Fact]
public void CreateClaimsIdentity_ReturnsCaseSensitiveClaimsIdentity_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);
}

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);
}
}
}
Loading