From 105e5136fe19c81e162f0af28bbd6ea14b8d8c24 Mon Sep 17 00:00:00 2001 From: saurabhsathe-ms Date: Thu, 1 May 2025 14:42:02 -0700 Subject: [PATCH 01/52] Added a test case to test presence of Signature in JsonWebToken. This update also involves serializing logic for Subject Actors --- .../JsonWebTokenHandler.CreateToken.cs | 23 ++- .../ActorClaimsTests.cs | 188 ++++++++++++++++++ 2 files changed, 210 insertions(+), 1 deletion(-) create mode 100644 test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs index 79613d3c0a..ed4f241ba6 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs @@ -133,7 +133,7 @@ public virtual string CreateToken(SecurityTokenDescriptor tokenDescriptor) LogMessages.IDX14116, LogHelper.MarkAsNonPII(nameof(tokenDescriptor.AdditionalInnerHeaderClaims)), LogHelper.MarkAsNonPII(string.Join(", ", JwtTokenUtilities.DefaultHeaderParameters))))); - + Console.WriteLine("create token 2"); return CreateToken( tokenDescriptor, SetDefaultTimesOnTokenCreation, @@ -804,7 +804,24 @@ internal static void AddSubjectClaims( var payload = new Dictionary(); bool checkClaims = tokenDescriptor.Claims != null && tokenDescriptor.Claims.Count > 0; + // Handle Actor claim specifically + if (tokenDescriptor.Subject.Actor != null) + { + // Create a nested JWT for the Actor + var actorTokenDescriptor = new SecurityTokenDescriptor + { + Subject = tokenDescriptor.Subject.Actor, + // Copy any signing credentials from the parent token + SigningCredentials = tokenDescriptor.SigningCredentials + }; + + // Create a JWT from the Actor claims identity + string actorToken = CreateToken(actorTokenDescriptor, false, 0); + // Add the Actor token as a claim + writer.WritePropertyName(JwtRegisteredClaimNames.Actort); + writer.WriteStringValue(actorToken); + } foreach (Claim claim in tokenDescriptor.Subject.Claims) { if (claim == null) @@ -820,6 +837,10 @@ internal static void AddSubjectClaims( if (issuerSet && claim.Type.Equals(JwtRegisteredClaimNames.Iss, StringComparison.Ordinal)) continue; + // Skip the actor claim as we've already processed it above + if (claim.Type.Equals(ClaimTypes.Actor, StringComparison.Ordinal)) + continue; + if (claim.Type.Equals(JwtRegisteredClaimNames.Exp, StringComparison.Ordinal)) { if (expSet) diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs new file mode 100644 index 0000000000..ac4a614bfb --- /dev/null +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs @@ -0,0 +1,188 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Security.Claims; +//using System.IdentityModel.Tokens.Jwt; +using Microsoft.IdentityModel.TestUtils; +using Microsoft.IdentityModel.Tokens; +using Xunit; +using Microsoft.IdentityModel.JsonWebTokens; + +#pragma warning disable CS3016 // Arrays as attribute arguments is not CLS-compliant + +namespace Microsoft.IdentityModel.Tests +{ + public class ActorClaimsTests + { + [Fact] + public void ActorClaimsShouldBeSerializedInTokens() + { + var context = new CompareContext($"{this}.ActorClaimsShouldBeSerializedInTokens"); + + try + { + // Create actor claims identity + var actorIdentity = new CaseSensitiveClaimsIdentity("ActorAuth"); // Updated to CaseSensitiveClaimsIdentity + actorIdentity.AddClaim(new Claim("sub", "actor-subject-id")); + actorIdentity.AddClaim(new Claim("name", "Actor Name")); + + var actorIdentityInner = new CaseSensitiveClaimsIdentity("ActorAuthInner"); // Updated to CaseSensitiveClaimsIdentity + actorIdentityInner.AddClaim(new Claim("sub", "actor-subject-id_inner")); + actorIdentityInner.AddClaim(new Claim("name", "Actor Name Inner")); + // Add a claim with destinations + var claim = new Claim("role", "admin"); + claim.Properties["destination"] = "accesstoken id_token"; + actorIdentity.AddClaim(claim); + actorIdentity.Actor = actorIdentityInner; + + // Create the main identity + var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); // Updated to CaseSensitiveClaimsIdentity + mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); + mainIdentity.AddClaim(new Claim("name", "Main User")); + + // Set the actor + mainIdentity.Actor = actorIdentity; + + // Create a ClaimsPrincipal + var principal = new ClaimsPrincipal(mainIdentity); + + // Create a token with JsonWebTokenHandler + var tokenHandler = new JsonWebTokenHandler(); + SecurityTokenDescriptor tokenDescriptor = new SecurityTokenDescriptor + { + Subject = principal.Identity as ClaimsIdentity, + Issuer = "https://example.com", + Audience = "https://api.example.com", + Expires = DateTime.UtcNow.AddHours(1), + SigningCredentials = Default.AsymmetricSigningCredentials + }; + //Console.WriteLine($"Token Descriptor: {tokenDescriptor.Subject.Actor.Claims.IsNullOrEmpty()}"); + var token = tokenHandler.CreateToken(tokenDescriptor); + + // Encode the token + var encodedToken = token; + JsonWebToken decodedToken = tokenHandler.ReadJsonWebToken(encodedToken); + Console.WriteLine($"jsonWeb Token: {token}"); + Console.WriteLine($"Decoded Token Subject Actor: {decodedToken.Actor}"); + // Verify actor claim exists in the token + Assert.True(decodedToken.Payload.HasClaim("actort"), "JWT token should contain 'act' claim"); + //decodedToken.Payload.TryGetValue("actort", out object actorClaimStr); + //Console.WriteLine($"Decoded Actor Token: {tokenHandler.ReadJwtToken(actorClaimStr as string)}"); + //// Parse actor token and verify its claims + var actorClaim = decodedToken.Payload.GetValue>("actort"); + Assert.NotNull(actorClaim); + Assert.Equal("actor-subject-id", actorClaim["sub"]); + Assert.Equal("Actor Name", actorClaim["name"]); + + //// Verify the destination property was maintained for the role claim + //if (actorClaim.TryGetValue("role", out object roleValue)) + //{ + // // Further verification could be done here for the destination properties + // Assert.Equal("admin", roleValue); + //} + //else + //{ + // context.Diffs.Add("Actor claim should contain 'role' claim with destination properties"); + //} + } + catch (Exception ex) + { + context.Diffs.Add($"Exception: {ex.ToString()}"); + } + + TestUtilities.AssertFailIfErrors(context); + } + + [Fact] + public void ActorClaimsWithDestinationsShouldBePreserved() + { + var context = new CompareContext($"{this}.ActorClaimsWithDestinationsShouldBePreserved"); + + try + { + // Create actor claims identity + var actorIdentity = new CaseSensitiveClaimsIdentity("ActorAuth"); // Updated to CaseSensitiveClaimsIdentity + + // Add claims with destinations + var claim1 = new Claim("role", "admin"); + claim1.Properties["destination"] = "accesstoken"; + actorIdentity.AddClaim(claim1); + + var claim2 = new Claim("email", "actor@example.com"); + claim2.Properties["destination"] = "id_token"; + actorIdentity.AddClaim(claim2); + + // Create the main identity + var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); // Updated to CaseSensitiveClaimsIdentity + mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); + + // Set the actor + mainIdentity.Actor = actorIdentity; + + // Create a ClaimsPrincipal + var principal = new ClaimsPrincipal(mainIdentity); + + // Create a token with JsonWebTokenHandler to test its handling + var handler = new JsonWebTokenHandler(); + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = principal.Identity as ClaimsIdentity, + Issuer = "https://example.com", + Audience = "https://api.example.com", + Expires = DateTime.UtcNow.AddHours(1), + SigningCredentials = Default.AsymmetricSigningCredentials + }; + + var token = handler.CreateToken(tokenDescriptor); + + // Read back the token + var validationParameters = new TokenValidationParameters + { + ValidIssuer = "https://example.com", + ValidAudience = "https://api.example.com", + IssuerSigningKey = Default.AsymmetricSigningCredentials.Key + }; + + var result = handler.ValidateToken(token, validationParameters); + Assert.True(result.IsValid); + + // Check if actor claim is preserved with correct identity + var claimsIdentity = result.ClaimsIdentity; + Assert.NotNull(claimsIdentity.Actor); + + // Check destination properties on actor claims + bool foundRoleClaim = false; + bool foundEmailClaim = false; + + foreach (var claim in claimsIdentity.Actor.Claims) + { + if (claim.Type == "role") + { + foundRoleClaim = true; + Assert.Equal("accesstoken", claim.Properties["destination"]); + } + else if (claim.Type == "email") + { + foundEmailClaim = true; + Assert.Equal("id_token", claim.Properties["destination"]); + } + } + + if (!foundRoleClaim || !foundEmailClaim) + { + context.Diffs.Add("Actor identity should preserve claims with destination properties"); + } + } + catch (Exception ex) + { + context.Diffs.Add($"Exception: {ex.ToString()}"); + } + + TestUtilities.AssertFailIfErrors(context); + } + } +} + +#pragma warning restore CS3016 // Arrays as attribute arguments is not CLS-compliant From b5c02e62a2c819da276d1b713a96155e210cc2ef Mon Sep 17 00:00:00 2001 From: saurabhsathe-ms Date: Thu, 1 May 2025 22:34:16 -0700 Subject: [PATCH 02/52] Included reference and implemented serialization logic --- .../JsonWebTokenHandler.CreateToken.cs | 50 +++++++++++-------- ...crosoft.IdentityModel.JsonWebTokens.csproj | 1 + 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs index ed4f241ba6..bfbd5de99f 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs @@ -13,6 +13,7 @@ using Microsoft.IdentityModel.Abstractions; using Microsoft.IdentityModel.Logging; using Microsoft.IdentityModel.Tokens; +using System.IdentityModel.Tokens.Jwt; using JsonPrimitives = Microsoft.IdentityModel.Tokens.Json.JsonSerializerPrimitives; using TokenLogMessages = Microsoft.IdentityModel.Tokens.LogMessages; @@ -784,6 +785,14 @@ internal static void WriteJwsPayload( writer.WriteEndObject(); writer.Flush(); } + private static string GetClaimValueAsUnsignedJWT(string jsonClaimsString) + { + JwtPayload payload = JwtPayload.Deserialize(jsonClaimsString); + string header = Base64UrlEncoder.Encode("{\"alg\":\"none\"}"); + string encodedPayload = Base64UrlEncoder.Encode(jsonClaimsString); + string jwtString = $"{header}.{encodedPayload}."; + return jwtString; + } internal static void AddSubjectClaims( ref Utf8JsonWriter writer, @@ -804,24 +813,7 @@ internal static void AddSubjectClaims( var payload = new Dictionary(); bool checkClaims = tokenDescriptor.Claims != null && tokenDescriptor.Claims.Count > 0; - // Handle Actor claim specifically - if (tokenDescriptor.Subject.Actor != null) - { - // Create a nested JWT for the Actor - var actorTokenDescriptor = new SecurityTokenDescriptor - { - Subject = tokenDescriptor.Subject.Actor, - // Copy any signing credentials from the parent token - SigningCredentials = tokenDescriptor.SigningCredentials - }; - - // Create a JWT from the Actor claims identity - string actorToken = CreateToken(actorTokenDescriptor, false, 0); - - // Add the Actor token as a claim - writer.WritePropertyName(JwtRegisteredClaimNames.Actort); - writer.WriteStringValue(actorToken); - } + bool isActorTokenSet = false; foreach (Claim claim in tokenDescriptor.Subject.Claims) { if (claim == null) @@ -838,9 +830,13 @@ internal static void AddSubjectClaims( continue; // Skip the actor claim as we've already processed it above - if (claim.Type.Equals(ClaimTypes.Actor, StringComparison.Ordinal)) - continue; - + if (!isActorTokenSet && claim.Type.Equals(ClaimTypes.Actor, StringComparison.Ordinal)) + { + isActorTokenSet = true; + string claimJwtString = GetClaimValueAsUnsignedJWT(claim.Value); + writer.WritePropertyName(JwtRegisteredClaimNames.Actort); + writer.WriteStringValue(claimJwtString); + } if (claim.Type.Equals(JwtRegisteredClaimNames.Exp, StringComparison.Ordinal)) { if (expSet) @@ -890,7 +886,19 @@ internal static void AddSubjectClaims( payload[claim.Type] = jsonClaimValue; } } + if (!isActorTokenSet && tokenDescriptor.Subject.Actor != null) + { + var actorTokenDescriptor = new SecurityTokenDescriptor + { + Subject = tokenDescriptor.Subject.Actor, + SigningCredentials = tokenDescriptor.SigningCredentials + }; + string actorToken = CreateToken(actorTokenDescriptor, false, 0); + writer.WritePropertyName(JwtRegisteredClaimNames.Actort); + writer.WriteStringValue(actorToken); + isActorTokenSet = true; + } foreach (KeyValuePair kvp in payload) JsonPrimitives.WriteObject(ref writer, kvp.Key, kvp.Value); diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/Microsoft.IdentityModel.JsonWebTokens.csproj b/src/Microsoft.IdentityModel.JsonWebTokens/Microsoft.IdentityModel.JsonWebTokens.csproj index b6f7239a7a..df759e11d6 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/Microsoft.IdentityModel.JsonWebTokens.csproj +++ b/src/Microsoft.IdentityModel.JsonWebTokens/Microsoft.IdentityModel.JsonWebTokens.csproj @@ -26,6 +26,7 @@ + From 90d5667b3e5501c127085084cf6ae5b4b7a1b8e0 Mon Sep 17 00:00:00 2001 From: saurabhsathe-ms Date: Thu, 1 May 2025 23:04:34 -0700 Subject: [PATCH 03/52] Added a json parser that can parse the stringified claim without breaking aot functionality or back compatibility --- .../JsonWebTokenHandler.CreateToken.cs | 10 +++++++--- .../Microsoft.IdentityModel.JsonWebTokens.csproj | 3 +-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs index bfbd5de99f..d2838e4e66 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs @@ -13,7 +13,6 @@ using Microsoft.IdentityModel.Abstractions; using Microsoft.IdentityModel.Logging; using Microsoft.IdentityModel.Tokens; -using System.IdentityModel.Tokens.Jwt; using JsonPrimitives = Microsoft.IdentityModel.Tokens.Json.JsonSerializerPrimitives; using TokenLogMessages = Microsoft.IdentityModel.Tokens.LogMessages; @@ -787,7 +786,13 @@ internal static void WriteJwsPayload( } private static string GetClaimValueAsUnsignedJWT(string jsonClaimsString) { - JwtPayload payload = JwtPayload.Deserialize(jsonClaimsString); + using JsonDocument doc = JsonDocument.Parse(jsonClaimsString); + JsonElement root = doc.RootElement; + var claimsDictionary = new Dictionary(); + foreach (JsonProperty property in root.EnumerateObject()) + { + claimsDictionary[property.Name] = property.Value.ToString(); + } string header = Base64UrlEncoder.Encode("{\"alg\":\"none\"}"); string encodedPayload = Base64UrlEncoder.Encode(jsonClaimsString); string jwtString = $"{header}.{encodedPayload}."; @@ -829,7 +834,6 @@ internal static void AddSubjectClaims( if (issuerSet && claim.Type.Equals(JwtRegisteredClaimNames.Iss, StringComparison.Ordinal)) continue; - // Skip the actor claim as we've already processed it above if (!isActorTokenSet && claim.Type.Equals(ClaimTypes.Actor, StringComparison.Ordinal)) { isActorTokenSet = true; diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/Microsoft.IdentityModel.JsonWebTokens.csproj b/src/Microsoft.IdentityModel.JsonWebTokens/Microsoft.IdentityModel.JsonWebTokens.csproj index df759e11d6..2b92c71616 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/Microsoft.IdentityModel.JsonWebTokens.csproj +++ b/src/Microsoft.IdentityModel.JsonWebTokens/Microsoft.IdentityModel.JsonWebTokens.csproj @@ -1,4 +1,4 @@ - + @@ -26,7 +26,6 @@ - From 2a0c1b17b0f18ce64f063897727ef375a568c4fd Mon Sep 17 00:00:00 2001 From: saurabhsathe-ms Date: Fri, 2 May 2025 09:27:14 -0700 Subject: [PATCH 04/52] Found an ovverriden create token method for our requirement. Removing the method we created --- .../JsonWebTokenHandler.CreateToken.cs | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs index d2838e4e66..f04e13ca92 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs @@ -784,21 +784,6 @@ internal static void WriteJwsPayload( writer.WriteEndObject(); writer.Flush(); } - private static string GetClaimValueAsUnsignedJWT(string jsonClaimsString) - { - using JsonDocument doc = JsonDocument.Parse(jsonClaimsString); - JsonElement root = doc.RootElement; - var claimsDictionary = new Dictionary(); - foreach (JsonProperty property in root.EnumerateObject()) - { - claimsDictionary[property.Name] = property.Value.ToString(); - } - string header = Base64UrlEncoder.Encode("{\"alg\":\"none\"}"); - string encodedPayload = Base64UrlEncoder.Encode(jsonClaimsString); - string jwtString = $"{header}.{encodedPayload}."; - return jwtString; - } - internal static void AddSubjectClaims( ref Utf8JsonWriter writer, SecurityTokenDescriptor tokenDescriptor, @@ -819,6 +804,7 @@ internal static void AddSubjectClaims( bool checkClaims = tokenDescriptor.Claims != null && tokenDescriptor.Claims.Count > 0; bool isActorTokenSet = false; + JsonWebTokenHandler jsonWebTokenHandler = new JsonWebTokenHandler(); foreach (Claim claim in tokenDescriptor.Subject.Claims) { if (claim == null) @@ -837,7 +823,7 @@ internal static void AddSubjectClaims( if (!isActorTokenSet && claim.Type.Equals(ClaimTypes.Actor, StringComparison.Ordinal)) { isActorTokenSet = true; - string claimJwtString = GetClaimValueAsUnsignedJWT(claim.Value); + string claimJwtString = jsonWebTokenHandler.CreateToken(claim.Value); writer.WritePropertyName(JwtRegisteredClaimNames.Actort); writer.WriteStringValue(claimJwtString); } From 9b0c1bd7e93530501b4231698a7d5e3f820f16d0 Mon Sep 17 00:00:00 2001 From: saurabhsathe-ms Date: Fri, 2 May 2025 09:49:44 -0700 Subject: [PATCH 05/52] Added a check to ensure that the claimvalue is a valid json --- .../JsonWebTokenHandler.CreateToken.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs index f04e13ca92..e7cca8b6e6 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs @@ -822,10 +822,13 @@ internal static void AddSubjectClaims( if (!isActorTokenSet && claim.Type.Equals(ClaimTypes.Actor, StringComparison.Ordinal)) { - isActorTokenSet = true; - string claimJwtString = jsonWebTokenHandler.CreateToken(claim.Value); - writer.WritePropertyName(JwtRegisteredClaimNames.Actort); - writer.WriteStringValue(claimJwtString); + using (JsonDocument.Parse(claim.Value)) + { + isActorTokenSet = true; + string claimJwtString = jsonWebTokenHandler.CreateToken(claim.Value); + writer.WritePropertyName(JwtRegisteredClaimNames.Actort); + writer.WriteStringValue(claimJwtString); + } } if (claim.Type.Equals(JwtRegisteredClaimNames.Exp, StringComparison.Ordinal)) { @@ -881,7 +884,6 @@ internal static void AddSubjectClaims( var actorTokenDescriptor = new SecurityTokenDescriptor { Subject = tokenDescriptor.Subject.Actor, - SigningCredentials = tokenDescriptor.SigningCredentials }; string actorToken = CreateToken(actorTokenDescriptor, false, 0); From b7a22801e7f7c9f47d0a6b2815a79b974980d3cb Mon Sep 17 00:00:00 2001 From: saurabhsathe-ms Date: Fri, 2 May 2025 17:27:04 -0700 Subject: [PATCH 06/52] Added logic to handle actor token from claims dictionary. Updated testcase. Testcases need more modifications --- .../JsonWebTokenHandler.CreateToken.cs | 59 +-- .../ActorClaimsTests.cs | 339 +++++++++++++----- 2 files changed, 285 insertions(+), 113 deletions(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs index e7cca8b6e6..99a0755eab 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs @@ -618,6 +618,7 @@ internal static void WriteJwsPayload( bool iatSet = false; bool descriptorClaimsNbfChecked = false; bool nbfSet = false; + bool isActorTokenSet = false; writer.WriteStartObject(); @@ -753,9 +754,26 @@ internal static void WriteJwsPayload( JsonPrimitives.WriteObject(ref writer, kvp.Key, kvp.Value); } + if (tokenDescriptor.Claims.ContainsKey(JwtRegisteredClaimNames.Actort)) + { + if (isActorTokenSet) + { + if (LogHelper.IsEnabled(EventLogLevel.Informational)) + LogHelper.LogInformation(LogHelper.FormatInvariant(LogMessages.IDX14113, LogHelper.MarkAsNonPII(nameof(tokenDescriptor.Expires)))); + + } + ClaimsIdentity actor = tokenDescriptor.Claims[JwtRegisteredClaimNames.Actort] as ClaimsIdentity; + var actorTokenDescriptor = new SecurityTokenDescriptor + { + Subject = actor, + }; + string actorToken = CreateToken(actorTokenDescriptor, false, 0); + JsonPrimitives.WriteObject(ref writer, JwtRegisteredClaimNames.Actort, actorToken); + isActorTokenSet = true; + } } - AddSubjectClaims(ref writer, tokenDescriptor, audienceSet, issuerSet, ref expSet, ref iatSet, ref nbfSet); + AddSubjectClaims(ref writer, tokenDescriptor, audienceSet, issuerSet, ref expSet, ref iatSet, ref nbfSet, ref isActorTokenSet); // By default we set these three properties only if they haven't been detected before. if (setDefaultTimesOnTokenCreation && !(expSet && iatSet && nbfSet)) @@ -791,7 +809,8 @@ internal static void AddSubjectClaims( bool issuerSet, ref bool expSet, ref bool iatSet, - ref bool nbfSet) + ref bool nbfSet, + ref bool isActorTokenSet) { if (tokenDescriptor.Subject == null) return; @@ -803,8 +822,20 @@ internal static void AddSubjectClaims( var payload = new Dictionary(); bool checkClaims = tokenDescriptor.Claims != null && tokenDescriptor.Claims.Count > 0; - bool isActorTokenSet = false; JsonWebTokenHandler jsonWebTokenHandler = new JsonWebTokenHandler(); + + if (!isActorTokenSet && tokenDescriptor.Subject.Actor != null) + { + var actorTokenDescriptor = new SecurityTokenDescriptor + { + Subject = tokenDescriptor.Subject.Actor, + }; + + string actorToken = CreateToken(actorTokenDescriptor, false, 0); + writer.WritePropertyName(JwtRegisteredClaimNames.Actort); + writer.WriteStringValue(actorToken); + isActorTokenSet = true; + } foreach (Claim claim in tokenDescriptor.Subject.Claims) { if (claim == null) @@ -820,16 +851,6 @@ internal static void AddSubjectClaims( if (issuerSet && claim.Type.Equals(JwtRegisteredClaimNames.Iss, StringComparison.Ordinal)) continue; - if (!isActorTokenSet && claim.Type.Equals(ClaimTypes.Actor, StringComparison.Ordinal)) - { - using (JsonDocument.Parse(claim.Value)) - { - isActorTokenSet = true; - string claimJwtString = jsonWebTokenHandler.CreateToken(claim.Value); - writer.WritePropertyName(JwtRegisteredClaimNames.Actort); - writer.WriteStringValue(claimJwtString); - } - } if (claim.Type.Equals(JwtRegisteredClaimNames.Exp, StringComparison.Ordinal)) { if (expSet) @@ -879,18 +900,6 @@ internal static void AddSubjectClaims( payload[claim.Type] = jsonClaimValue; } } - if (!isActorTokenSet && tokenDescriptor.Subject.Actor != null) - { - var actorTokenDescriptor = new SecurityTokenDescriptor - { - Subject = tokenDescriptor.Subject.Actor, - }; - - string actorToken = CreateToken(actorTokenDescriptor, false, 0); - writer.WritePropertyName(JwtRegisteredClaimNames.Actort); - writer.WriteStringValue(actorToken); - isActorTokenSet = true; - } foreach (KeyValuePair kvp in payload) JsonPrimitives.WriteObject(ref writer, kvp.Key, kvp.Value); diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs index ac4a614bfb..28505a265e 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs @@ -2,13 +2,12 @@ // Licensed under the MIT License. using System; -using System.Collections.Generic; using System.Security.Claims; -//using System.IdentityModel.Tokens.Jwt; using Microsoft.IdentityModel.TestUtils; using Microsoft.IdentityModel.Tokens; using Xunit; using Microsoft.IdentityModel.JsonWebTokens; +using Newtonsoft.Json; #pragma warning disable CS3016 // Arrays as attribute arguments is not CLS-compliant @@ -17,35 +16,98 @@ namespace Microsoft.IdentityModel.Tests public class ActorClaimsTests { [Fact] - public void ActorClaimsShouldBeSerializedInTokens() + public void ActorTokenAsClaimShouldBeSerialized() { - var context = new CompareContext($"{this}.ActorClaimsShouldBeSerializedInTokens"); + var context = new CompareContext($"{this}.ActorTokenAsClaimShouldBeSerialized"); + + try + { + // Create the main identity + var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); + mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); + mainIdentity.AddClaim(new Claim("name", "Main User")); + + // Create an actor claim with a JSON string payload + var actorJson = JsonConvert.SerializeObject(new + { + sub = "actor-subject-id", + name = "Actor Name", + role = "admin" + }); + + mainIdentity.AddClaim(new Claim(ClaimTypes.Actor, actorJson)); + + var principal = new ClaimsPrincipal(mainIdentity); + + // Create a token with JsonWebTokenHandler + var tokenHandler = new JsonWebTokenHandler(); + SecurityTokenDescriptor tokenDescriptor = new SecurityTokenDescriptor + { + Subject = principal.Identity as ClaimsIdentity, + Issuer = "https://example.com", + Audience = "https://api.example.com", + Expires = DateTime.UtcNow.AddHours(1), + SigningCredentials = Default.AsymmetricSigningCredentials + }; + + var token = tokenHandler.CreateToken(tokenDescriptor); + + // Verify token was created successfully + JsonWebToken decodedToken = tokenHandler.ReadJsonWebToken(token); + + // Verify actor claim exists in the token + Assert.True(decodedToken.Payload.HasClaim("actort"), "JWT token should contain 'actort' claim"); + + // Verify actor token can be parsed + var actorToken = decodedToken.Actor; + Assert.NotNull(actorToken); + + // Try to deserialize the actor token string into a JSON object + var actorTokenString = decodedToken.Actor; + JsonWebToken actorJwt = tokenHandler.ReadJsonWebToken(actorTokenString); + + // Verify actor claims + Assert.Equal("actor-subject-id", actorJwt.Payload.GetValue("sub")); + Assert.Equal("Actor Name", actorJwt.Payload.GetValue("name")); + Assert.Equal("admin", actorJwt.Payload.GetValue("role")); + } + catch (Exception ex) + { + context.Diffs.Add($"Exception: {ex}"); + } + + TestUtilities.AssertFailIfErrors(context); + } + + [Fact] + public void ActorTokenAsClaimsIdentityShouldBeSerialized() + { + var context = new CompareContext($"{this}.ActorTokenAsClaimsIdentityShouldBeSerialized"); try { // Create actor claims identity - var actorIdentity = new CaseSensitiveClaimsIdentity("ActorAuth"); // Updated to CaseSensitiveClaimsIdentity + var actorIdentity = new CaseSensitiveClaimsIdentity("ActorAuth"); actorIdentity.AddClaim(new Claim("sub", "actor-subject-id")); actorIdentity.AddClaim(new Claim("name", "Actor Name")); + actorIdentity.AddClaim(new Claim("role", "admin")); + + // Create nested actor + var nestedActorIdentity = new CaseSensitiveClaimsIdentity("NestedActorAuth"); + nestedActorIdentity.AddClaim(new Claim("sub", "nested-actor-id")); + nestedActorIdentity.AddClaim(new Claim("name", "Nested Actor")); - var actorIdentityInner = new CaseSensitiveClaimsIdentity("ActorAuthInner"); // Updated to CaseSensitiveClaimsIdentity - actorIdentityInner.AddClaim(new Claim("sub", "actor-subject-id_inner")); - actorIdentityInner.AddClaim(new Claim("name", "Actor Name Inner")); - // Add a claim with destinations - var claim = new Claim("role", "admin"); - claim.Properties["destination"] = "accesstoken id_token"; - actorIdentity.AddClaim(claim); - actorIdentity.Actor = actorIdentityInner; + // Set nested actor + actorIdentity.Actor = nestedActorIdentity; // Create the main identity - var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); // Updated to CaseSensitiveClaimsIdentity + var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); mainIdentity.AddClaim(new Claim("name", "Main User")); // Set the actor mainIdentity.Actor = actorIdentity; - // Create a ClaimsPrincipal var principal = new ClaimsPrincipal(mainIdentity); // Create a token with JsonWebTokenHandler @@ -58,75 +120,128 @@ public void ActorClaimsShouldBeSerializedInTokens() Expires = DateTime.UtcNow.AddHours(1), SigningCredentials = Default.AsymmetricSigningCredentials }; - //Console.WriteLine($"Token Descriptor: {tokenDescriptor.Subject.Actor.Claims.IsNullOrEmpty()}"); + var token = tokenHandler.CreateToken(tokenDescriptor); + JsonWebToken decodedToken = tokenHandler.ReadJsonWebToken(token); - // Encode the token - var encodedToken = token; - JsonWebToken decodedToken = tokenHandler.ReadJsonWebToken(encodedToken); - Console.WriteLine($"jsonWeb Token: {token}"); - Console.WriteLine($"Decoded Token Subject Actor: {decodedToken.Actor}"); - // Verify actor claim exists in the token - Assert.True(decodedToken.Payload.HasClaim("actort"), "JWT token should contain 'act' claim"); - //decodedToken.Payload.TryGetValue("actort", out object actorClaimStr); - //Console.WriteLine($"Decoded Actor Token: {tokenHandler.ReadJwtToken(actorClaimStr as string)}"); - //// Parse actor token and verify its claims - var actorClaim = decodedToken.Payload.GetValue>("actort"); - Assert.NotNull(actorClaim); - Assert.Equal("actor-subject-id", actorClaim["sub"]); - Assert.Equal("Actor Name", actorClaim["name"]); - - //// Verify the destination property was maintained for the role claim - //if (actorClaim.TryGetValue("role", out object roleValue)) - //{ - // // Further verification could be done here for the destination properties - // Assert.Equal("admin", roleValue); - //} - //else - //{ - // context.Diffs.Add("Actor claim should contain 'role' claim with destination properties"); - //} + // Verify actor claim exists + Assert.True(decodedToken.Payload.HasClaim("actort"), "JWT token should contain 'actort' claim"); + + // Validate actor token and its claims + var result = tokenHandler.ValidateToken(token, new TokenValidationParameters + { + ValidateActor = true, + ValidIssuer = "https://example.com", + ValidAudience = "https://api.example.com", + IssuerSigningKey = Default.AsymmetricSigningCredentials.Key + }); + + Assert.True(result.IsValid); + + // Verify actor identity structure + Assert.NotNull(result.ClaimsIdentity.Actor); + Assert.Equal("actor-subject-id", result.ClaimsIdentity.Actor.FindFirst("sub")?.Value); + Assert.Equal("Actor Name", result.ClaimsIdentity.Actor.FindFirst("name")?.Value); + Assert.Equal("admin", result.ClaimsIdentity.Actor.FindFirst("role")?.Value); + + // Verify nested actor + Assert.NotNull(result.ClaimsIdentity.Actor.Actor); + Assert.Equal("nested-actor-id", result.ClaimsIdentity.Actor.Actor.FindFirst("sub")?.Value); + Assert.Equal("Nested Actor", result.ClaimsIdentity.Actor.Actor.FindFirst("name")?.Value); } catch (Exception ex) { - context.Diffs.Add($"Exception: {ex.ToString()}"); + context.Diffs.Add($"Exception: {ex}"); } TestUtilities.AssertFailIfErrors(context); } [Fact] - public void ActorClaimsWithDestinationsShouldBePreserved() + public void ActorTokenInBothClaimAndIdentityShouldPreferClaim() { - var context = new CompareContext($"{this}.ActorClaimsWithDestinationsShouldBePreserved"); + var context = new CompareContext($"{this}.ActorTokenInBothClaimAndIdentityShouldPreferClaim"); try { - // Create actor claims identity - var actorIdentity = new CaseSensitiveClaimsIdentity("ActorAuth"); // Updated to CaseSensitiveClaimsIdentity + // Create actor claims identity that should NOT be used + var actorIdentity = new CaseSensitiveClaimsIdentity("ActorAuth"); + actorIdentity.AddClaim(new Claim("sub", "identity-actor-id")); + actorIdentity.AddClaim(new Claim("name", "Identity Actor")); - // Add claims with destinations - var claim1 = new Claim("role", "admin"); - claim1.Properties["destination"] = "accesstoken"; - actorIdentity.AddClaim(claim1); + // Create the main identity with Actor set + var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); + mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); + mainIdentity.AddClaim(new Claim("name", "Main User")); + mainIdentity.Actor = actorIdentity; - var claim2 = new Claim("email", "actor@example.com"); - claim2.Properties["destination"] = "id_token"; - actorIdentity.AddClaim(claim2); + // Add an actor claim that should take precedence + var actorJson = JsonConvert.SerializeObject(new + { + sub = "claim-actor-id", + name = "Claim Actor" + }); + + mainIdentity.AddClaim(new Claim(ClaimTypes.Actor, actorJson)); + + var principal = new ClaimsPrincipal(mainIdentity); + // Create a token with JsonWebTokenHandler + var tokenHandler = new JsonWebTokenHandler(); + SecurityTokenDescriptor tokenDescriptor = new SecurityTokenDescriptor + { + Subject = principal.Identity as ClaimsIdentity, + Issuer = "https://example.com", + Audience = "https://api.example.com", + Expires = DateTime.UtcNow.AddHours(1), + SigningCredentials = Default.AsymmetricSigningCredentials + }; + + var token = tokenHandler.CreateToken(tokenDescriptor); + + // Verify token was created successfully + JsonWebToken decodedToken = tokenHandler.ReadJsonWebToken(token); + + // Verify actor claim exists in the token + Assert.True(decodedToken.Payload.HasClaim("actort"), "JWT token should contain 'actort' claim"); + + // Verify the actor token contains the claim data, not the identity data + var actorTokenString = decodedToken.Actor; + JsonWebToken actorJwt = tokenHandler.ReadJsonWebToken(actorTokenString); + + Assert.Equal("claim-actor-id", actorJwt.Payload.GetValue("sub")); + Assert.Equal("Claim Actor", actorJwt.Payload.GetValue("name")); + Assert.NotEqual("identity-actor-id", actorJwt.Payload.GetValue("sub")); + Assert.NotEqual("Identity Actor", actorJwt.Payload.GetValue("name")); + } + catch (Exception ex) + { + context.Diffs.Add($"Exception: {ex}"); + } + + TestUtilities.AssertFailIfErrors(context); + } + + [Fact] + public void InvalidActorTokenJsonShouldNotParse() + { + var context = new CompareContext($"{this}.InvalidActorTokenJsonShouldNotParse"); + + try + { // Create the main identity - var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); // Updated to CaseSensitiveClaimsIdentity + var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); - // Set the actor - mainIdentity.Actor = actorIdentity; + // Create an actor claim with an invalid JSON string + var invalidJson = "{ invalid json format }"; + mainIdentity.AddClaim(new Claim(ClaimTypes.Actor, invalidJson)); - // Create a ClaimsPrincipal var principal = new ClaimsPrincipal(mainIdentity); - // Create a token with JsonWebTokenHandler to test its handling - var handler = new JsonWebTokenHandler(); - var tokenDescriptor = new SecurityTokenDescriptor + // Create a token with JsonWebTokenHandler + var tokenHandler = new JsonWebTokenHandler(); + SecurityTokenDescriptor tokenDescriptor = new SecurityTokenDescriptor { Subject = principal.Identity as ClaimsIdentity, Issuer = "https://example.com", @@ -135,49 +250,97 @@ public void ActorClaimsWithDestinationsShouldBePreserved() SigningCredentials = Default.AsymmetricSigningCredentials }; - var token = handler.CreateToken(tokenDescriptor); + var token = tokenHandler.CreateToken(tokenDescriptor); + + // Verify token was created successfully + JsonWebToken decodedToken = tokenHandler.ReadJsonWebToken(token); + + // The actort claim should exist but with the raw value + Assert.True(decodedToken.Payload.HasClaim("actort"), "JWT token should contain 'actort' claim"); - // Read back the token - var validationParameters = new TokenValidationParameters + // Verify the actor token is not parseable as a JWT + var actorTokenString = decodedToken.Actor; + + // This should not be parseable as a JWT + var exception = Record.Exception(() => tokenHandler.ReadJsonWebToken(actorTokenString)); + Assert.NotNull(exception); + + // The actor claim should be the raw invalid JSON + Assert.Equal(invalidJson, decodedToken.Payload.GetValue("actort")); + + // When validating a token with an invalid actor, validation should still pass if not validating actor + var result = tokenHandler.ValidateToken(token, new TokenValidationParameters { + ValidateActor = false, ValidIssuer = "https://example.com", ValidAudience = "https://api.example.com", IssuerSigningKey = Default.AsymmetricSigningCredentials.Key - }; + }); - var result = handler.ValidateToken(token, validationParameters); Assert.True(result.IsValid); + } + catch (Exception ex) + { + context.Diffs.Add($"Exception: {ex}"); + } - // Check if actor claim is preserved with correct identity - var claimsIdentity = result.ClaimsIdentity; - Assert.NotNull(claimsIdentity.Actor); + TestUtilities.AssertFailIfErrors(context); + } - // Check destination properties on actor claims - bool foundRoleClaim = false; - bool foundEmailClaim = false; + [Fact] + public void ClaimPropertiesShouldBePreservedInActorToken() + { + var context = new CompareContext($"{this}.ClaimPropertiesShouldBePreservedInActorToken"); - foreach (var claim in claimsIdentity.Actor.Claims) + try + { + // Create actor claims identity with properties + // Add a claim with properties + var actorIdentityInner = new CaseSensitiveClaimsIdentity("ActorAuth"); // Updated to CaseSensitiveClaimsIdentity + actorIdentityInner.AddClaim(new Claim("destination", "accesstoken id_token")); + actorIdentityInner.AddClaim(new Claim("custom_property", "custom_value")); + + // Create the main identity + var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); + mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); + mainIdentity.Actor = actorIdentityInner; + + var principal = new ClaimsPrincipal(mainIdentity); + + // Create a token with JsonWebTokenHandler + var tokenHandler = new JsonWebTokenHandler(); + SecurityTokenDescriptor tokenDescriptor = new SecurityTokenDescriptor { - if (claim.Type == "role") - { - foundRoleClaim = true; - Assert.Equal("accesstoken", claim.Properties["destination"]); - } - else if (claim.Type == "email") - { - foundEmailClaim = true; - Assert.Equal("id_token", claim.Properties["destination"]); - } - } - - if (!foundRoleClaim || !foundEmailClaim) + Subject = principal.Identity as ClaimsIdentity, + Issuer = "https://example.com", + Audience = "https://api.example.com", + Expires = DateTime.UtcNow.AddHours(1), + SigningCredentials = Default.AsymmetricSigningCredentials + }; + + var token = tokenHandler.CreateToken(tokenDescriptor); + Console.WriteLine($"Here is the token {token}"); + // Validate and check if properties were preserved + var result = tokenHandler.ValidateToken(token, new TokenValidationParameters { - context.Diffs.Add("Actor identity should preserve claims with destination properties"); - } + ValidIssuer = "https://example.com", + ValidAudience = "https://api.example.com", + IssuerSigningKey = Default.AsymmetricSigningCredentials.Key + }); + + Assert.True(result.IsValid); + Assert.NotNull(result.ClaimsIdentity.Actor); + + // Check if property was preserved in actor claim + var roleClaim = result.ClaimsIdentity.Actor.FindFirst("role"); + Assert.NotNull(roleClaim); + Assert.Equal("admin", roleClaim.Value); + Assert.Equal("accesstoken id_token", roleClaim.Properties["destination"]); + Assert.Equal("custom_value", roleClaim.Properties["custom_property"]); } catch (Exception ex) { - context.Diffs.Add($"Exception: {ex.ToString()}"); + context.Diffs.Add($"Exception: {ex}"); } TestUtilities.AssertFailIfErrors(context); From 7b65a01559ce44447a8f937fd155bceb4eb06dbe Mon Sep 17 00:00:00 2001 From: saurabhsathe-ms Date: Sun, 4 May 2025 12:28:40 -0700 Subject: [PATCH 07/52] Updated JsonWebTokenHandlerCreateToken.cs to accomodate new changes. Added new test case to test serialization precedence --- .../JsonWebTokenHandler.CreateToken.cs | 6 +- .../ActorClaimsTests.cs | 315 ++++++++---------- 2 files changed, 150 insertions(+), 171 deletions(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs index 99a0755eab..39851c937f 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs @@ -133,7 +133,6 @@ public virtual string CreateToken(SecurityTokenDescriptor tokenDescriptor) LogMessages.IDX14116, LogHelper.MarkAsNonPII(nameof(tokenDescriptor.AdditionalInnerHeaderClaims)), LogHelper.MarkAsNonPII(string.Join(", ", JwtTokenUtilities.DefaultHeaderParameters))))); - Console.WriteLine("create token 2"); return CreateToken( tokenDescriptor, SetDefaultTimesOnTokenCreation, @@ -674,6 +673,10 @@ internal static void WriteJwsPayload( { foreach (KeyValuePair kvp in tokenDescriptor.Claims) { + if (kvp.Key.Equals(JwtRegisteredClaimNames.Actort, StringComparison.Ordinal)) + { + continue; + } if (!descriptorClaimsAudienceChecked && kvp.Key.Equals(JwtRegisteredClaimNames.Aud, StringComparison.Ordinal)) { descriptorClaimsAudienceChecked = true; @@ -751,7 +754,6 @@ internal static void WriteJwsPayload( nbfSet = true; } - JsonPrimitives.WriteObject(ref writer, kvp.Key, kvp.Value); } if (tokenDescriptor.Claims.ContainsKey(JwtRegisteredClaimNames.Actort)) diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs index 28505a265e..515227e887 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs @@ -1,72 +1,58 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - using System; +using System.Collections.Generic; using System.Security.Claims; using Microsoft.IdentityModel.TestUtils; using Microsoft.IdentityModel.Tokens; -using Xunit; using Microsoft.IdentityModel.JsonWebTokens; -using Newtonsoft.Json; - -#pragma warning disable CS3016 // Arrays as attribute arguments is not CLS-compliant +using Xunit; namespace Microsoft.IdentityModel.Tests { - public class ActorClaimsTests + public class ActorTokenSerializationTests { [Fact] - public void ActorTokenAsClaimShouldBeSerialized() + public void ActorTokenInClaimsDictionaryShouldBeProperlySerialized() { - var context = new CompareContext($"{this}.ActorTokenAsClaimShouldBeSerialized"); + var context = new CompareContext($"{this}.ActorTokenInClaimsDictionaryShouldBeProperlySerialized"); try { + // Create a ClaimsIdentity for the actor + var actorIdentity = new CaseSensitiveClaimsIdentity("ActorAuth"); + actorIdentity.AddClaim(new Claim("sub", "actor-subject-id")); + actorIdentity.AddClaim(new Claim("name", "Actor Name")); + actorIdentity.AddClaim(new Claim("role", "admin")); + // Create the main identity var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); mainIdentity.AddClaim(new Claim("name", "Main User")); - // Create an actor claim with a JSON string payload - var actorJson = JsonConvert.SerializeObject(new - { - sub = "actor-subject-id", - name = "Actor Name", - role = "admin" - }); - - mainIdentity.AddClaim(new Claim(ClaimTypes.Actor, actorJson)); - - var principal = new ClaimsPrincipal(mainIdentity); - - // Create a token with JsonWebTokenHandler + // Create a token with JsonWebTokenHandler where actor is in Claims dictionary var tokenHandler = new JsonWebTokenHandler(); - SecurityTokenDescriptor tokenDescriptor = new SecurityTokenDescriptor + var tokenDescriptor = new SecurityTokenDescriptor { - Subject = principal.Identity as ClaimsIdentity, + Subject = mainIdentity, Issuer = "https://example.com", Audience = "https://api.example.com", Expires = DateTime.UtcNow.AddHours(1), - SigningCredentials = Default.AsymmetricSigningCredentials + SigningCredentials = Default.AsymmetricSigningCredentials, + Claims = new Dictionary + { + { JwtRegisteredClaimNames.Actort, actorIdentity } + } }; var token = tokenHandler.CreateToken(tokenDescriptor); - - // Verify token was created successfully JsonWebToken decodedToken = tokenHandler.ReadJsonWebToken(token); // Verify actor claim exists in the token Assert.True(decodedToken.Payload.HasClaim("actort"), "JWT token should contain 'actort' claim"); - - // Verify actor token can be parsed - var actorToken = decodedToken.Actor; - Assert.NotNull(actorToken); - - // Try to deserialize the actor token string into a JSON object + // Get the actor token and verify it contains the expected claims var actorTokenString = decodedToken.Actor; - JsonWebToken actorJwt = tokenHandler.ReadJsonWebToken(actorTokenString); + Assert.NotNull(actorTokenString); - // Verify actor claims + JsonWebToken actorJwt = tokenHandler.ReadJsonWebToken(actorTokenString); Assert.Equal("actor-subject-id", actorJwt.Payload.GetValue("sub")); Assert.Equal("Actor Name", actorJwt.Payload.GetValue("name")); Assert.Equal("admin", actorJwt.Payload.GetValue("role")); @@ -80,41 +66,29 @@ public void ActorTokenAsClaimShouldBeSerialized() } [Fact] - public void ActorTokenAsClaimsIdentityShouldBeSerialized() + public void ActorTokenAsSubjectShouldBeProperlySerialized() { - var context = new CompareContext($"{this}.ActorTokenAsClaimsIdentityShouldBeSerialized"); + var context = new CompareContext($"{this}.ActorTokenAsSubjectShouldBeProperlySerialized"); try { - // Create actor claims identity + // Create actor identity var actorIdentity = new CaseSensitiveClaimsIdentity("ActorAuth"); actorIdentity.AddClaim(new Claim("sub", "actor-subject-id")); actorIdentity.AddClaim(new Claim("name", "Actor Name")); actorIdentity.AddClaim(new Claim("role", "admin")); - // Create nested actor - var nestedActorIdentity = new CaseSensitiveClaimsIdentity("NestedActorAuth"); - nestedActorIdentity.AddClaim(new Claim("sub", "nested-actor-id")); - nestedActorIdentity.AddClaim(new Claim("name", "Nested Actor")); - - // Set nested actor - actorIdentity.Actor = nestedActorIdentity; - - // Create the main identity + // Create the main identity with Actor set via Identity.Actor var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); mainIdentity.AddClaim(new Claim("name", "Main User")); - - // Set the actor mainIdentity.Actor = actorIdentity; - var principal = new ClaimsPrincipal(mainIdentity); - // Create a token with JsonWebTokenHandler var tokenHandler = new JsonWebTokenHandler(); - SecurityTokenDescriptor tokenDescriptor = new SecurityTokenDescriptor + var tokenDescriptor = new SecurityTokenDescriptor { - Subject = principal.Identity as ClaimsIdentity, + Subject = mainIdentity, Issuer = "https://example.com", Audience = "https://api.example.com", Expires = DateTime.UtcNow.AddHours(1), @@ -124,30 +98,17 @@ public void ActorTokenAsClaimsIdentityShouldBeSerialized() var token = tokenHandler.CreateToken(tokenDescriptor); JsonWebToken decodedToken = tokenHandler.ReadJsonWebToken(token); - // Verify actor claim exists + // Verify actor claim exists in the token Assert.True(decodedToken.Payload.HasClaim("actort"), "JWT token should contain 'actort' claim"); - // Validate actor token and its claims - var result = tokenHandler.ValidateToken(token, new TokenValidationParameters - { - ValidateActor = true, - ValidIssuer = "https://example.com", - ValidAudience = "https://api.example.com", - IssuerSigningKey = Default.AsymmetricSigningCredentials.Key - }); - - Assert.True(result.IsValid); - - // Verify actor identity structure - Assert.NotNull(result.ClaimsIdentity.Actor); - Assert.Equal("actor-subject-id", result.ClaimsIdentity.Actor.FindFirst("sub")?.Value); - Assert.Equal("Actor Name", result.ClaimsIdentity.Actor.FindFirst("name")?.Value); - Assert.Equal("admin", result.ClaimsIdentity.Actor.FindFirst("role")?.Value); - - // Verify nested actor - Assert.NotNull(result.ClaimsIdentity.Actor.Actor); - Assert.Equal("nested-actor-id", result.ClaimsIdentity.Actor.Actor.FindFirst("sub")?.Value); - Assert.Equal("Nested Actor", result.ClaimsIdentity.Actor.Actor.FindFirst("name")?.Value); + // Get the actor token and verify it contains the expected claims + var actorTokenString = decodedToken.Actor; + Assert.NotNull(actorTokenString); + + JsonWebToken actorJwt = tokenHandler.ReadJsonWebToken(actorTokenString); + Assert.Equal("actor-subject-id", actorJwt.Payload.GetValue("sub")); + Assert.Equal("Actor Name", actorJwt.Payload.GetValue("name")); + Assert.Equal("admin", actorJwt.Payload.GetValue("role")); } catch (Exception ex) { @@ -158,61 +119,60 @@ public void ActorTokenAsClaimsIdentityShouldBeSerialized() } [Fact] - public void ActorTokenInBothClaimAndIdentityShouldPreferClaim() + public void ActorTokenInBothClaimsAndSubjectShouldPreferClaimsValue() { - var context = new CompareContext($"{this}.ActorTokenInBothClaimAndIdentityShouldPreferClaim"); + var context = new CompareContext($"{this}.ActorTokenInBothClaimsAndSubjectShouldPreferClaimsValue"); try { - // Create actor claims identity that should NOT be used - var actorIdentity = new CaseSensitiveClaimsIdentity("ActorAuth"); - actorIdentity.AddClaim(new Claim("sub", "identity-actor-id")); - actorIdentity.AddClaim(new Claim("name", "Identity Actor")); + // Create actor identity for Subject.Actor (should be ignored) + var subjectActorIdentity = new CaseSensitiveClaimsIdentity("ActorAuth"); + subjectActorIdentity.AddClaim(new Claim("sub", "subject-actor-id")); + subjectActorIdentity.AddClaim(new Claim("name", "Subject Actor")); - // Create the main identity with Actor set + // Create actor identity for Claims dictionary (should be used) + var claimsActorIdentity = new CaseSensitiveClaimsIdentity("ClaimsActorAuth"); + claimsActorIdentity.AddClaim(new Claim("sub", "claims-actor-id")); + claimsActorIdentity.AddClaim(new Claim("name", "Claims Actor")); + + // Create the main identity var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); mainIdentity.AddClaim(new Claim("name", "Main User")); - mainIdentity.Actor = actorIdentity; - - // Add an actor claim that should take precedence - var actorJson = JsonConvert.SerializeObject(new - { - sub = "claim-actor-id", - name = "Claim Actor" - }); - - mainIdentity.AddClaim(new Claim(ClaimTypes.Actor, actorJson)); - - var principal = new ClaimsPrincipal(mainIdentity); + mainIdentity.Actor = subjectActorIdentity; // Set the actor that should be ignored // Create a token with JsonWebTokenHandler var tokenHandler = new JsonWebTokenHandler(); - SecurityTokenDescriptor tokenDescriptor = new SecurityTokenDescriptor + var tokenDescriptor = new SecurityTokenDescriptor { - Subject = principal.Identity as ClaimsIdentity, + Subject = mainIdentity, Issuer = "https://example.com", Audience = "https://api.example.com", Expires = DateTime.UtcNow.AddHours(1), - SigningCredentials = Default.AsymmetricSigningCredentials + SigningCredentials = Default.AsymmetricSigningCredentials, + // Add Claims actor that should take precedence + Claims = new Dictionary + { + { JwtRegisteredClaimNames.Actort, claimsActorIdentity } + } }; var token = tokenHandler.CreateToken(tokenDescriptor); - - // Verify token was created successfully JsonWebToken decodedToken = tokenHandler.ReadJsonWebToken(token); - // Verify actor claim exists in the token + // Verify actor claim exists Assert.True(decodedToken.Payload.HasClaim("actort"), "JWT token should contain 'actort' claim"); - // Verify the actor token contains the claim data, not the identity data + // Get the actor token and verify it contains the expected claims var actorTokenString = decodedToken.Actor; + Assert.NotNull(actorTokenString); + JsonWebToken actorJwt = tokenHandler.ReadJsonWebToken(actorTokenString); - Assert.Equal("claim-actor-id", actorJwt.Payload.GetValue("sub")); - Assert.Equal("Claim Actor", actorJwt.Payload.GetValue("name")); - Assert.NotEqual("identity-actor-id", actorJwt.Payload.GetValue("sub")); - Assert.NotEqual("Identity Actor", actorJwt.Payload.GetValue("name")); + // Verify the Claims dictionary actor was used, not the Subject.Actor + Assert.Equal("claims-actor-id", actorJwt.Payload.GetValue("sub")); + Assert.Equal("Claims Actor", actorJwt.Payload.GetValue("name")); + Assert.NotEqual("subject-actor-id", actorJwt.Payload.GetValue("sub")); } catch (Exception ex) { @@ -223,61 +183,69 @@ public void ActorTokenInBothClaimAndIdentityShouldPreferClaim() } [Fact] - public void InvalidActorTokenJsonShouldNotParse() + public void NestedActorTokenInClaimsDictionaryShouldBeProperlySerialized() { - var context = new CompareContext($"{this}.InvalidActorTokenJsonShouldNotParse"); + var context = new CompareContext($"{this}.NestedActorTokenInClaimsDictionaryShouldBeProperlySerialized"); try { + // Create nested actor identity + var nestedActorIdentity = new CaseSensitiveClaimsIdentity("NestedActorAuth"); + nestedActorIdentity.AddClaim(new Claim("sub", "nested-actor-id")); + nestedActorIdentity.AddClaim(new Claim("name", "Nested Actor")); + + // Create actor identity with nested actor + var actorIdentity = new CaseSensitiveClaimsIdentity("ActorAuth"); + actorIdentity.AddClaim(new Claim("sub", "actor-subject-id")); + actorIdentity.AddClaim(new Claim("name", "Actor Name")); + actorIdentity.Actor = nestedActorIdentity; // Set nested actor + // Create the main identity var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); + mainIdentity.AddClaim(new Claim("name", "Main User")); - // Create an actor claim with an invalid JSON string - var invalidJson = "{ invalid json format }"; - mainIdentity.AddClaim(new Claim(ClaimTypes.Actor, invalidJson)); - - var principal = new ClaimsPrincipal(mainIdentity); - - // Create a token with JsonWebTokenHandler + // Create a token with JsonWebTokenHandler where actor is in Claims dictionary var tokenHandler = new JsonWebTokenHandler(); - SecurityTokenDescriptor tokenDescriptor = new SecurityTokenDescriptor + var tokenDescriptor = new SecurityTokenDescriptor { - Subject = principal.Identity as ClaimsIdentity, + Subject = mainIdentity, Issuer = "https://example.com", Audience = "https://api.example.com", Expires = DateTime.UtcNow.AddHours(1), - SigningCredentials = Default.AsymmetricSigningCredentials + SigningCredentials = Default.AsymmetricSigningCredentials, + Claims = new Dictionary + { + { JwtRegisteredClaimNames.Actort, actorIdentity } + } }; var token = tokenHandler.CreateToken(tokenDescriptor); - - // Verify token was created successfully JsonWebToken decodedToken = tokenHandler.ReadJsonWebToken(token); - // The actort claim should exist but with the raw value + // Verify actor claim exists Assert.True(decodedToken.Payload.HasClaim("actort"), "JWT token should contain 'actort' claim"); - // Verify the actor token is not parseable as a JWT + // Read the main actor token var actorTokenString = decodedToken.Actor; + Assert.NotNull(actorTokenString); + JsonWebToken actorJwt = tokenHandler.ReadJsonWebToken(actorTokenString); - // This should not be parseable as a JWT - var exception = Record.Exception(() => tokenHandler.ReadJsonWebToken(actorTokenString)); - Assert.NotNull(exception); + // Verify main actor claims + Assert.Equal("actor-subject-id", actorJwt.Payload.GetValue("sub")); + Assert.Equal("Actor Name", actorJwt.Payload.GetValue("name")); - // The actor claim should be the raw invalid JSON - Assert.Equal(invalidJson, decodedToken.Payload.GetValue("actort")); + // Verify nested actor exists + Assert.True(actorJwt.Payload.HasClaim("actort"), "Actor token should contain nested 'actort' claim"); - // When validating a token with an invalid actor, validation should still pass if not validating actor - var result = tokenHandler.ValidateToken(token, new TokenValidationParameters - { - ValidateActor = false, - ValidIssuer = "https://example.com", - ValidAudience = "https://api.example.com", - IssuerSigningKey = Default.AsymmetricSigningCredentials.Key - }); + // Read the nested actor token + var nestedActorTokenString = actorJwt.Actor; + Assert.NotNull(nestedActorTokenString); + JsonWebToken nestedActorJwt = tokenHandler.ReadJsonWebToken(nestedActorTokenString); - Assert.True(result.IsValid); + // Verify nested actor claims + Assert.Equal("nested-actor-id", nestedActorJwt.Payload.GetValue("sub")); + Assert.Equal("Nested Actor", nestedActorJwt.Payload.GetValue("name")); } catch (Exception ex) { @@ -288,30 +256,34 @@ public void InvalidActorTokenJsonShouldNotParse() } [Fact] - public void ClaimPropertiesShouldBePreservedInActorToken() + public void NestedActorTokenAsSubjectShouldBeProperlySerialized() { - var context = new CompareContext($"{this}.ClaimPropertiesShouldBePreservedInActorToken"); + var context = new CompareContext($"{this}.NestedActorTokenAsSubjectShouldBeProperlySerialized"); try { - // Create actor claims identity with properties - // Add a claim with properties - var actorIdentityInner = new CaseSensitiveClaimsIdentity("ActorAuth"); // Updated to CaseSensitiveClaimsIdentity - actorIdentityInner.AddClaim(new Claim("destination", "accesstoken id_token")); - actorIdentityInner.AddClaim(new Claim("custom_property", "custom_value")); + // Create nested actor + var nestedActorIdentity = new CaseSensitiveClaimsIdentity("NestedActorAuth"); + nestedActorIdentity.AddClaim(new Claim("sub", "nested-actor-id")); + nestedActorIdentity.AddClaim(new Claim("name", "Nested Actor")); - // Create the main identity + // Create actor identity with nested actor + var actorIdentity = new CaseSensitiveClaimsIdentity("ActorAuth"); + actorIdentity.AddClaim(new Claim("sub", "actor-subject-id")); + actorIdentity.AddClaim(new Claim("name", "Actor Name")); + actorIdentity.Actor = nestedActorIdentity; + + // Create the main identity with Actor set var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); - mainIdentity.Actor = actorIdentityInner; - - var principal = new ClaimsPrincipal(mainIdentity); + mainIdentity.AddClaim(new Claim("name", "Main User")); + mainIdentity.Actor = actorIdentity; // Create a token with JsonWebTokenHandler var tokenHandler = new JsonWebTokenHandler(); - SecurityTokenDescriptor tokenDescriptor = new SecurityTokenDescriptor + var tokenDescriptor = new SecurityTokenDescriptor { - Subject = principal.Identity as ClaimsIdentity, + Subject = mainIdentity, Issuer = "https://example.com", Audience = "https://api.example.com", Expires = DateTime.UtcNow.AddHours(1), @@ -319,24 +291,31 @@ public void ClaimPropertiesShouldBePreservedInActorToken() }; var token = tokenHandler.CreateToken(tokenDescriptor); - Console.WriteLine($"Here is the token {token}"); - // Validate and check if properties were preserved - var result = tokenHandler.ValidateToken(token, new TokenValidationParameters - { - ValidIssuer = "https://example.com", - ValidAudience = "https://api.example.com", - IssuerSigningKey = Default.AsymmetricSigningCredentials.Key - }); - - Assert.True(result.IsValid); - Assert.NotNull(result.ClaimsIdentity.Actor); - - // Check if property was preserved in actor claim - var roleClaim = result.ClaimsIdentity.Actor.FindFirst("role"); - Assert.NotNull(roleClaim); - Assert.Equal("admin", roleClaim.Value); - Assert.Equal("accesstoken id_token", roleClaim.Properties["destination"]); - Assert.Equal("custom_value", roleClaim.Properties["custom_property"]); + JsonWebToken decodedToken = tokenHandler.ReadJsonWebToken(token); + + // Verify actor claim exists + Assert.True(decodedToken.Payload.HasClaim("actort"), "JWT token should contain 'actort' claim"); + + // Read the main actor token + var actorTokenString = decodedToken.Actor; + Assert.NotNull(actorTokenString); + JsonWebToken actorJwt = tokenHandler.ReadJsonWebToken(actorTokenString); + + // Verify main actor claims + Assert.Equal("actor-subject-id", actorJwt.Payload.GetValue("sub")); + Assert.Equal("Actor Name", actorJwt.Payload.GetValue("name")); + + // Verify nested actor exists + Assert.True(actorJwt.Payload.HasClaim("actort"), "Actor token should contain nested 'actort' claim"); + + // Read the nested actor token + var nestedActorTokenString = actorJwt.Actor; + Assert.NotNull(nestedActorTokenString); + JsonWebToken nestedActorJwt = tokenHandler.ReadJsonWebToken(nestedActorTokenString); + + // Verify nested actor claims + Assert.Equal("nested-actor-id", nestedActorJwt.Payload.GetValue("sub")); + Assert.Equal("Nested Actor", nestedActorJwt.Payload.GetValue("name")); } catch (Exception ex) { @@ -347,5 +326,3 @@ public void ClaimPropertiesShouldBePreservedInActorToken() } } } - -#pragma warning restore CS3016 // Arrays as attribute arguments is not CLS-compliant From cd264f5ffc34fcae6505a19c5f66719706caa6bf Mon Sep 17 00:00:00 2001 From: saurabhsathe-ms Date: Sun, 4 May 2025 13:36:43 -0700 Subject: [PATCH 08/52] Removed the print statements and added header --- .../ActorClaimsTests.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs index 515227e887..fe0d4ffcd8 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + using System; using System.Collections.Generic; using System.Security.Claims; From a7b578d5e9aa2c3aa1f04f2c517018b6ed349555 Mon Sep 17 00:00:00 2001 From: saurabhsathe-ms Date: Mon, 5 May 2025 17:28:51 -0700 Subject: [PATCH 09/52] "ActorTokenInClaimsDictionaryShouldBeProperlySerialized" now properly working after fixing a bug in how claims dictionary was processed --- .../JsonWebTokenHandler.CreateToken.cs | 49 +- .../LogMessages.cs | 1 + .../PublicAPI.Unshipped.txt | 2 + .../ActorClaimsTests.cs | 462 +++++++++++++++++- 4 files changed, 503 insertions(+), 11 deletions(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs index 39851c937f..1b7422717e 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs @@ -27,6 +27,12 @@ public partial class JsonWebTokenHandler : TokenHandler { private static readonly SecurityTokenDescriptor s_emptyTokenDescriptor = new(); + /// + /// Gets or sets the maximum depth allowed when processing nested actor tokens. + /// This prevents excessive recursion when handling deeply nested actor tokens. + /// Default value is 5. + /// + public static int MaxActorChainLength { get; set; } = 5; /// /// Creates an unsigned JSON Web Signature (JWS). /// @@ -142,7 +148,8 @@ public virtual string CreateToken(SecurityTokenDescriptor tokenDescriptor) internal static string CreateToken( SecurityTokenDescriptor tokenDescriptor, bool setdefaultTimesOnTokenCreation, - int tokenLifetimeInMinutes) + int tokenLifetimeInMinutes, + int actorChainDepth = 0) { // The form of a JWS is: Base64UrlEncoding(UTF8(Header)) | . | Base64UrlEncoding(Payload) | . | Base64UrlEncoding(Signature) // Where the Header is specifically the UTF8 bytes of the JSON, whereas the Payload encoding is not specified, but UTF8 is used by everyone. @@ -174,7 +181,8 @@ internal static string CreateToken( ref writer, tokenDescriptor, setdefaultTimesOnTokenCreation, - tokenLifetimeInMinutes); + tokenLifetimeInMinutes, + actorChainDepth); // mark end of payload int payloadEnd = (int)utf8ByteMemoryStream.Length; @@ -600,12 +608,14 @@ int sizeOfEncodedHeaderAndPayloadAsciiBytes /// The used to create the token. /// A boolean that controls if expiration, notbefore, issuedat should be added if missing. /// The default value for the token lifetime in minutes. + /// Controls the recursion length while parsing nested actor tokens /// A dictionary of claims. internal static void WriteJwsPayload( ref Utf8JsonWriter writer, SecurityTokenDescriptor tokenDescriptor, bool setDefaultTimesOnTokenCreation, - int tokenLifetimeInMinutes) + int tokenLifetimeInMinutes, + int actorChainDepth) { bool descriptorClaimsAudienceChecked = false; bool audienceSet = false; @@ -668,7 +678,7 @@ internal static void WriteJwsPayload( // Duplicates are resolved according to the following priority: // SecurityTokenDescriptor.{Audience/Audiences, Issuer, Expires, IssuedAt, NotBefore}, SecurityTokenDescriptor.Claims, SecurityTokenDescriptor.Subject.Claims // SecurityTokenDescriptor.Claims are KeyValuePairs, whereas SecurityTokenDescriptor.Subject.Claims are System.Security.Claims.Claim and are processed differently. - + Console.WriteLine($"Actor token claim depth :{actorChainDepth}"); if (tokenDescriptor.Claims != null && tokenDescriptor.Claims.Count > 0) { foreach (KeyValuePair kvp in tokenDescriptor.Claims) @@ -758,6 +768,15 @@ internal static void WriteJwsPayload( } if (tokenDescriptor.Claims.ContainsKey(JwtRegisteredClaimNames.Actort)) { + // Check for maximum actor chain depth + if (actorChainDepth >= MaxActorChainLength) + { + throw LogHelper.LogExceptionMessage( + new SecurityTokenException( + LogHelper.FormatInvariant( + LogMessages.IDX14313, + LogHelper.MarkAsNonPII(MaxActorChainLength)))); + } if (isActorTokenSet) { if (LogHelper.IsEnabled(EventLogLevel.Informational)) @@ -765,17 +784,19 @@ internal static void WriteJwsPayload( } ClaimsIdentity actor = tokenDescriptor.Claims[JwtRegisteredClaimNames.Actort] as ClaimsIdentity; + Console.WriteLine($"Claims Identity identifier :{actor.AuthenticationType} and main token identifier {tokenDescriptor.ToString()}"); var actorTokenDescriptor = new SecurityTokenDescriptor { - Subject = actor, + Subject = actor }; - string actorToken = CreateToken(actorTokenDescriptor, false, 0); + actorChainDepth = actorChainDepth + 1; + string actorToken = CreateToken(actorTokenDescriptor, false, 0, actorChainDepth); JsonPrimitives.WriteObject(ref writer, JwtRegisteredClaimNames.Actort, actorToken); isActorTokenSet = true; } } - AddSubjectClaims(ref writer, tokenDescriptor, audienceSet, issuerSet, ref expSet, ref iatSet, ref nbfSet, ref isActorTokenSet); + AddSubjectClaims(ref writer, tokenDescriptor, audienceSet, issuerSet, ref expSet, ref iatSet, ref nbfSet, ref isActorTokenSet, actorChainDepth); // By default we set these three properties only if they haven't been detected before. if (setDefaultTimesOnTokenCreation && !(expSet && iatSet && nbfSet)) @@ -812,7 +833,8 @@ internal static void AddSubjectClaims( ref bool expSet, ref bool iatSet, ref bool nbfSet, - ref bool isActorTokenSet) + ref bool isActorTokenSet, + int actorChainDepth = 0) { if (tokenDescriptor.Subject == null) return; @@ -824,16 +846,23 @@ internal static void AddSubjectClaims( var payload = new Dictionary(); bool checkClaims = tokenDescriptor.Claims != null && tokenDescriptor.Claims.Count > 0; - JsonWebTokenHandler jsonWebTokenHandler = new JsonWebTokenHandler(); if (!isActorTokenSet && tokenDescriptor.Subject.Actor != null) { + if (actorChainDepth >= MaxActorChainLength) + { + throw LogHelper.LogExceptionMessage( + new SecurityTokenException( + LogHelper.FormatInvariant( + LogMessages.IDX14313, + LogHelper.MarkAsNonPII(MaxActorChainLength)))); + } var actorTokenDescriptor = new SecurityTokenDescriptor { Subject = tokenDescriptor.Subject.Actor, }; - string actorToken = CreateToken(actorTokenDescriptor, false, 0); + string actorToken = CreateToken(actorTokenDescriptor, false, 0, actorChainDepth + 1); writer.WritePropertyName(JwtRegisteredClaimNames.Actort); writer.WriteStringValue(actorToken); isActorTokenSet = true; diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/LogMessages.cs b/src/Microsoft.IdentityModel.JsonWebTokens/LogMessages.cs index edd5cddd5b..b3d4fd6683 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/LogMessages.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/LogMessages.cs @@ -51,5 +51,6 @@ internal static class LogMessages internal const string IDX14310 = "IDX14310: JWE authentication tag is missing."; internal const string IDX14311 = "IDX14311: Unable to decode the authentication tag as a Base64Url encoded string."; internal const string IDX14312 = "IDX14312: Unable to decode the cipher text as a Base64Url encoded string."; + internal const string IDX14313 = "IDX14313: Unable to serialize actor token. Actor token chain exceeded maximum depth of {0}"; } } diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.JsonWebTokens/PublicAPI.Unshipped.txt index e69de29bb2..20ef11973e 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.JsonWebTokens/PublicAPI.Unshipped.txt @@ -0,0 +1,2 @@ +static Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.MaxActorChainLength.get -> int +static Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.MaxActorChainLength.set -> void \ No newline at end of file diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs index fe0d4ffcd8..56e63e1e53 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs @@ -11,7 +11,7 @@ namespace Microsoft.IdentityModel.Tests { - public class ActorTokenSerializationTests + public class ActorClaimsTests { [Fact] public void ActorTokenInClaimsDictionaryShouldBeProperlySerialized() @@ -20,6 +20,7 @@ public void ActorTokenInClaimsDictionaryShouldBeProperlySerialized() try { + AppContext.SetSwitch(AppContextSwitches.UseClaimsIdentityTypeSwitch, true); // Create a ClaimsIdentity for the actor var actorIdentity = new CaseSensitiveClaimsIdentity("ActorAuth"); actorIdentity.AddClaim(new Claim("sub", "actor-subject-id")); @@ -325,7 +326,466 @@ public void NestedActorTokenAsSubjectShouldBeProperlySerialized() context.Diffs.Add($"Exception: {ex}"); } + TestUtilities.AssertFailIfErrors(context); + } + [Fact] + public void MaxActorChainLength_DefaultValue_Is5() + { + // Arrange + var handler = new JsonWebTokenHandler(); + + // Assert + Assert.Equal(5, JsonWebTokenHandler.MaxActorChainLength); + } + + [Fact] + public void MaxActorChainLength_CanBeChanged() + { + // Arrange + var handler = new JsonWebTokenHandler(); + + // Act + JsonWebTokenHandler.MaxActorChainLength = 10; + + // Assert + Assert.Equal(10, JsonWebTokenHandler.MaxActorChainLength); + } + + [Fact] + public void NestedActorTokens_WithinMaxDepth_AreSerializedProperly() + { + // Arrange + var handler = new JsonWebTokenHandler(); + JsonWebTokenHandler.MaxActorChainLength = 3; // Allow 3 levels of nesting + + // Create nested actor identities (3 levels) + var level3Actor = new CaseSensitiveClaimsIdentity("Level3Auth"); + level3Actor.AddClaim(new Claim("sub", "level3-actor")); + level3Actor.AddClaim(new Claim("name", "Level 3 Actor")); + + var level2Actor = new CaseSensitiveClaimsIdentity("Level2Auth"); + level2Actor.AddClaim(new Claim("sub", "level2-actor")); + level2Actor.AddClaim(new Claim("name", "Level 2 Actor")); + level2Actor.Actor = level3Actor; + + var level1Actor = new CaseSensitiveClaimsIdentity("Level1Auth"); + level1Actor.AddClaim(new Claim("sub", "level1-actor")); + level1Actor.AddClaim(new Claim("name", "Level 1 Actor")); + level1Actor.Actor = level2Actor; + + // Create the main identity + var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); + mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); + mainIdentity.AddClaim(new Claim("name", "Main User")); + mainIdentity.Actor = level1Actor; + + // Create token descriptor + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = mainIdentity, + Issuer = "https://example.com", + Audience = "https://api.example.com", + SigningCredentials = Default.AsymmetricSigningCredentials + }; + + // Act + var token = handler.CreateToken(tokenDescriptor); + var jwtToken = handler.ReadJsonWebToken(token); + + // Assert + Assert.NotNull(jwtToken.Actor); + + // Check first level actor + var level1Token = handler.ReadJsonWebToken(jwtToken.Actor); + Assert.Equal("level1-actor", level1Token.Payload.GetValue("sub")); + Assert.Equal("Level 1 Actor", level1Token.Payload.GetValue("name")); + Assert.NotNull(level1Token.Actor); + + // Check second level actor + var level2Token = handler.ReadJsonWebToken(level1Token.Actor); + Assert.Equal("level2-actor", level2Token.Payload.GetValue("sub")); + Assert.Equal("Level 2 Actor", level2Token.Payload.GetValue("name")); + Assert.NotNull(level2Token.Actor); + + // Check third level actor + var level3Token = handler.ReadJsonWebToken(level2Token.Actor); + Assert.Equal("level3-actor", level3Token.Payload.GetValue("sub")); + Assert.Equal("Level 3 Actor", level3Token.Payload.GetValue("name")); + Assert.False(level3Token.Payload.HasClaim("actort"), "There should be no deeper actor at level 3"); + } + + [Fact] + public void NestedActorTokens_ExceedingMaxDepth_ThrowsException() + { + var context = new CompareContext($"{this}.NestedActorTokens_ExceedingMaxDepth_ThrowsException"); + + try + { + // Arrange + var handler = new JsonWebTokenHandler(); + JsonWebTokenHandler.MaxActorChainLength = 2; // Allow only 2 levels of nesting + + // Create nested actor identities (3 levels, but we'll set MaxActorChainLength to 2) + var level3Actor = new CaseSensitiveClaimsIdentity("Level3Auth"); + level3Actor.AddClaim(new Claim("sub", "level3-actor")); + level3Actor.AddClaim(new Claim("name", "Level 3 Actor")); + + var level2Actor = new CaseSensitiveClaimsIdentity("Level2Auth"); + level2Actor.AddClaim(new Claim("sub", "level2-actor")); + level2Actor.AddClaim(new Claim("name", "Level 2 Actor")); + level2Actor.Actor = level3Actor; // This will cause exception due to MaxActorChainLength=2 + + var level1Actor = new CaseSensitiveClaimsIdentity("Level1Auth"); + level1Actor.AddClaim(new Claim("sub", "level1-actor")); + level1Actor.AddClaim(new Claim("name", "Level 1 Actor")); + level1Actor.Actor = level2Actor; + + // Create the main identity + var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); + mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); + mainIdentity.AddClaim(new Claim("name", "Main User")); + mainIdentity.Actor = level1Actor; + + // Create token descriptor + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = mainIdentity, + Issuer = "https://example.com", + Audience = "https://api.example.com", + SigningCredentials = Default.AsymmetricSigningCredentials + }; + + // Act - This should throw a SecurityTokenException + var token = handler.CreateToken(tokenDescriptor); + context.Diffs.Add("Expected exception was not thrown."); + } + catch (SecurityTokenException ex) + { + // Assert - Verify the exception message contains the expected content + if (ex.Message.Contains("IDX14313")) + { + // Test passed - expected exception was thrown with the right message + } + else + { + context.Diffs.Add($"Exception message does not contain expected content. Message: {ex.Message}"); + } + } + catch (Exception ex) + { + // Unexpected exception type + context.Diffs.Add($"Unexpected exception type: {ex.GetType()}, Message: {ex.Message}"); + } + + TestUtilities.AssertFailIfErrors(context); + } + + + + [Fact] + public void NestedActorTokens_WithZeroMaxDepth_ThrowsException() + { + var context = new CompareContext($"{this}.NestedActorTokens_WithZeroMaxDepth_ThrowsException"); + + try + { + // Arrange + var handler = new JsonWebTokenHandler(); + JsonWebTokenHandler.MaxActorChainLength = 0; // Setting to 0 should now cause an exception + + // Create an actor identity + var actorIdentity = new CaseSensitiveClaimsIdentity("ActorAuth"); + actorIdentity.AddClaim(new Claim("sub", "actor-subject-id")); + actorIdentity.AddClaim(new Claim("name", "Actor Name")); + + // Create the main identity with Actor set + var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); + mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); + mainIdentity.AddClaim(new Claim("name", "Main User")); + mainIdentity.Actor = actorIdentity; + + // Create token descriptor + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = mainIdentity, + Issuer = "https://example.com", + Audience = "https://api.example.com", + SigningCredentials = Default.AsymmetricSigningCredentials + }; + + // Act - This should throw a SecurityTokenException + var token = handler.CreateToken(tokenDescriptor); + context.Diffs.Add("Expected exception was not thrown."); + } + catch (SecurityTokenException ex) + { + Console.WriteLine("Here is the exception message: " + ex.Message); + // Assert - Verify the exception message contains the expected content + if (ex.Message.Contains("IDX14313")) + { + // Test passed - expected exception was thrown with the right message + } + else + { + context.Diffs.Add($"Exception message does not contain expected content. Message: {ex.Message}"); + } + } + catch (Exception ex) + { + // Unexpected exception type + context.Diffs.Add($"Unexpected exception type: {ex.GetType()}, Message: {ex.Message}"); + } + + TestUtilities.AssertFailIfErrors(context); + } + + [Fact] + public void ActorTokenInClaimsDictionary_RespectsMaxActorChainLength() + { + // Arrange + var handler = new JsonWebTokenHandler(); + JsonWebTokenHandler.MaxActorChainLength = 1; // Allow only 1 level of nesting + + // Create nested actor identities + var nestedActorIdentity = new CaseSensitiveClaimsIdentity("NestedActorAuth"); + nestedActorIdentity.AddClaim(new Claim("sub", "nested-actor-id")); + nestedActorIdentity.AddClaim(new Claim("name", "Nested Actor")); + + // Create actor identity with nested actor + var actorIdentity = new CaseSensitiveClaimsIdentity("ActorAuth"); + actorIdentity.AddClaim(new Claim("sub", "actor-subject-id")); + actorIdentity.AddClaim(new Claim("name", "Actor Name")); + actorIdentity.Actor = nestedActorIdentity; // This should be ignored due to MaxActorChainLength + + // Create the main identity + var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); + mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); + mainIdentity.AddClaim(new Claim("name", "Main User")); + + // Create token with actor in Claims dictionary + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = mainIdentity, + Issuer = "https://example.com", + Audience = "https://api.example.com", + SigningCredentials = Default.AsymmetricSigningCredentials, + Claims = new Dictionary + { + { JwtRegisteredClaimNames.Actort, actorIdentity } + } + }; + + // Act + var token = handler.CreateToken(tokenDescriptor); + var jwtToken = handler.ReadJsonWebToken(token); + + // Assert + Assert.True(jwtToken.Payload.HasClaim("actort"), "JWT token should contain 'actort' claim"); + + // Read the actor token + var actorToken = handler.ReadJsonWebToken(jwtToken.Actor); + Assert.Equal("actor-subject-id", actorToken.Payload.GetValue("sub")); + Assert.Equal("Actor Name", actorToken.Payload.GetValue("name")); + + // The nested actor should not be included because MaxActorChainLength is 1 + Assert.False(actorToken.Payload.HasClaim("actort"), "There should be no nested actor claim due to MaxActorChainLength"); + } + + [Fact] + public void ActorTokens_MixedSourceRespectMaxActorChainLength() + { + // Arrange + var handler = new JsonWebTokenHandler(); + JsonWebTokenHandler.MaxActorChainLength = 2; // Allow 2 levels of nesting + + // Create level 2 actor (will be in claims dictionary) + var level2Actor = new CaseSensitiveClaimsIdentity("Level2Auth"); + level2Actor.AddClaim(new Claim("sub", "level2-actor")); + level2Actor.AddClaim(new Claim("name", "Level 2 Actor")); + + // Create nested actors that should be truncated + var level3Actor = new CaseSensitiveClaimsIdentity("Level3Auth"); + level3Actor.AddClaim(new Claim("sub", "level3-actor")); + level3Actor.AddClaim(new Claim("name", "Level 3 Actor")); + + // Create level 1 actor with nested actor + var level1Actor = new CaseSensitiveClaimsIdentity("Level1Auth"); + level1Actor.AddClaim(new Claim("sub", "level1-actor")); + level1Actor.AddClaim(new Claim("name", "Level 1 Actor")); + level1Actor.Actor = level3Actor; // This should be ignored due to MaxActorChainLength + + // Create the main identity + var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); + mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); + mainIdentity.AddClaim(new Claim("name", "Main User")); + mainIdentity.Actor = level1Actor; + + // Create a token with additional actor in Claims dictionary + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = mainIdentity, + Issuer = "https://example.com", + Audience = "https://api.example.com", + SigningCredentials = Default.AsymmetricSigningCredentials, + // Add level 2 actor in claims dictionary to replace level 1's actor + Claims = new Dictionary + { + { JwtRegisteredClaimNames.Actort, level2Actor } + } + }; + + // Act + var token = handler.CreateToken(tokenDescriptor); + var jwtToken = handler.ReadJsonWebToken(token); + + // Assert + Assert.True(jwtToken.Payload.HasClaim("actort"), "JWT token should contain 'actort' claim"); + + // Verify we get the actor from Claims dictionary (should be level2Actor) + var actorToken = handler.ReadJsonWebToken(jwtToken.Actor); + Assert.Equal("level2-actor", actorToken.Payload.GetValue("sub")); + Assert.Equal("Level 2 Actor", actorToken.Payload.GetValue("name")); + + // There should be no nested actor because we're already at max depth + Assert.False(actorToken.Payload.HasClaim("actort"), "There should be no nested actor claim due to MaxActorChainLength"); + } + + [Fact] + public void CreateToken_ExceedingMaxActorChainDepth_ThrowsException() + { + var context = new CompareContext($"{this}.CreateToken_ExceedingMaxActorChainDepth_ThrowsException"); + + try + { + // Arrange + var handler = new JsonWebTokenHandler(); + JsonWebTokenHandler.MaxActorChainLength = 2; // Set max depth to 2 for the test + + // Create nested actor identities (3 levels, exceeding max depth) + var level3Actor = new CaseSensitiveClaimsIdentity("Level3Auth"); + level3Actor.AddClaim(new Claim("sub", "level3-actor")); + level3Actor.AddClaim(new Claim("name", "Level 3 Actor")); + + var level2Actor = new CaseSensitiveClaimsIdentity("Level2Auth"); + level2Actor.AddClaim(new Claim("sub", "level2-actor")); + level2Actor.AddClaim(new Claim("name", "Level 2 Actor")); + level2Actor.Actor = level3Actor; // This will cause exception due to MaxActorChainLength=2 + + var level1Actor = new CaseSensitiveClaimsIdentity("Level1Auth"); + level1Actor.AddClaim(new Claim("sub", "level1-actor")); + level1Actor.AddClaim(new Claim("name", "Level 1 Actor")); + level1Actor.Actor = level2Actor; + + // Create the main identity with Actor set + var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); + mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); + mainIdentity.AddClaim(new Claim("name", "Main User")); + mainIdentity.Actor = level1Actor; + + // Create token descriptor + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = mainIdentity, + Issuer = "https://example.com", + Audience = "https://api.example.com", + SigningCredentials = Default.AsymmetricSigningCredentials + }; + + // Act - This should throw a SecurityTokenException + var token = handler.CreateToken(tokenDescriptor); + context.Diffs.Add("Expected exception was not thrown."); + } + catch (SecurityTokenException ex) + { + // Assert - Verify the exception message contains the expected content + if (ex.Message.Contains("Actor chain exceeded maximum depth of 2")) + { + // Test passed - expected exception was thrown with the right message + } + else + { + context.Diffs.Add($"Exception message does not contain expected content. Message: {ex.Message}"); + } + } + catch (Exception ex) + { + // Unexpected exception type + context.Diffs.Add($"Unexpected exception type: {ex.GetType()}, Message: {ex.Message}"); + } + + TestUtilities.AssertFailIfErrors(context); + } + + [Fact] + public void CreateToken_WithClaimsActorChainExceedingMaxDepth_ThrowsException() + { + var context = new CompareContext($"{this}.CreateToken_WithClaimsActorChainExceedingMaxDepth_ThrowsException"); + + try + { + // Arrange + var handler = new JsonWebTokenHandler(); + JsonWebTokenHandler.MaxActorChainLength = 2; // Set max depth to 2 for the test + + // Create nested actor for level 3 (will exceed max depth) + var level3Actor = new CaseSensitiveClaimsIdentity("Level3Auth"); + level3Actor.AddClaim(new Claim("sub", "level3-actor")); + level3Actor.AddClaim(new Claim("name", "Level 3 Actor")); + + // Create nested actor for level 2 + var level2Actor = new CaseSensitiveClaimsIdentity("Level2Auth"); + level2Actor.AddClaim(new Claim("sub", "level2-actor")); + level2Actor.AddClaim(new Claim("name", "Level 2 Actor")); + level2Actor.Actor = level3Actor; // Will exceed max depth in Claims dictionary + + // Create actor for level 1 to be included in Claims dictionary + var level1Actor = new CaseSensitiveClaimsIdentity("Level1Auth"); + level1Actor.AddClaim(new Claim("sub", "level1-actor")); + level1Actor.AddClaim(new Claim("name", "Level 1 Actor")); + level1Actor.Actor = level2Actor; + + // Create the main identity + var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); + mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); + mainIdentity.AddClaim(new Claim("name", "Main User")); + + // Create token with actor chain in Claims dictionary + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = mainIdentity, + Issuer = "https://example.com", + Audience = "https://api.example.com", + SigningCredentials = Default.AsymmetricSigningCredentials, + Claims = new Dictionary + { + { JwtRegisteredClaimNames.Actort, level1Actor } + } + }; + + // Act - This should throw a SecurityTokenException + var token = handler.CreateToken(tokenDescriptor); + context.Diffs.Add("Expected exception was not thrown."); + } + catch (SecurityTokenException ex) + { + // Assert - Verify the exception message contains the expected content + if (ex.Message.Contains("Actor chain exceeded maximum depth of 2")) + { + // Test passed - expected exception was thrown with the right message + } + else + { + context.Diffs.Add($"Exception message does not contain expected content. Message: {ex.Message}"); + } + } + catch (Exception ex) + { + // Unexpected exception type + context.Diffs.Add($"Unexpected exception type: {ex.GetType()}, Message: {ex.Message}"); + } + TestUtilities.AssertFailIfErrors(context); } } } + From 7a657e95351ed62cd28bd6ee64b244e0b426f9e5 Mon Sep 17 00:00:00 2001 From: saurabhsathe-ms Date: Mon, 5 May 2025 22:47:46 -0700 Subject: [PATCH 10/52] Added new testcases, added comments to testcase and modified existing testcase ot now expect he new exception expected. --- .../ActorClaimsTests.cs | 274 +++--------------- 1 file changed, 44 insertions(+), 230 deletions(-) diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs index 56e63e1e53..62c4d6c97e 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs @@ -13,7 +13,7 @@ namespace Microsoft.IdentityModel.Tests { public class ActorClaimsTests { - [Fact] + [Fact] // This tests that the actor token is properly serialized when added to the claims dictionary without any nesting. public void ActorTokenInClaimsDictionaryShouldBeProperlySerialized() { var context = new CompareContext($"{this}.ActorTokenInClaimsDictionaryShouldBeProperlySerialized"); @@ -69,7 +69,7 @@ public void ActorTokenInClaimsDictionaryShouldBeProperlySerialized() TestUtilities.AssertFailIfErrors(context); } - [Fact] + [Fact]// This tests that the actor token is properly serialized when added as Subject without any nesting. public void ActorTokenAsSubjectShouldBeProperlySerialized() { var context = new CompareContext($"{this}.ActorTokenAsSubjectShouldBeProperlySerialized"); @@ -122,7 +122,7 @@ public void ActorTokenAsSubjectShouldBeProperlySerialized() TestUtilities.AssertFailIfErrors(context); } - [Fact] + [Fact]// This tests that the actor token from claims is preferred over that of subject when both are specified. public void ActorTokenInBothClaimsAndSubjectShouldPreferClaimsValue() { var context = new CompareContext($"{this}.ActorTokenInBothClaimsAndSubjectShouldPreferClaimsValue"); @@ -186,7 +186,7 @@ public void ActorTokenInBothClaimsAndSubjectShouldPreferClaimsValue() TestUtilities.AssertFailIfErrors(context); } - [Fact] + [Fact]// This tests that the actor token is properly serialized when added to the claims dictionary with nested actors. public void NestedActorTokenInClaimsDictionaryShouldBeProperlySerialized() { var context = new CompareContext($"{this}.NestedActorTokenInClaimsDictionaryShouldBeProperlySerialized"); @@ -259,7 +259,7 @@ public void NestedActorTokenInClaimsDictionaryShouldBeProperlySerialized() TestUtilities.AssertFailIfErrors(context); } - [Fact] + [Fact]//This tests that the actor token is properly serialized when added as Subject with nested actors. public void NestedActorTokenAsSubjectShouldBeProperlySerialized() { var context = new CompareContext($"{this}.NestedActorTokenAsSubjectShouldBeProperlySerialized"); @@ -328,22 +328,9 @@ public void NestedActorTokenAsSubjectShouldBeProperlySerialized() TestUtilities.AssertFailIfErrors(context); } - [Fact] - public void MaxActorChainLength_DefaultValue_Is5() - { - // Arrange - var handler = new JsonWebTokenHandler(); - - // Assert - Assert.Equal(5, JsonWebTokenHandler.MaxActorChainLength); - } - - [Fact] + [Fact]// This tests that the MaxActorChainLength can be changed. public void MaxActorChainLength_CanBeChanged() { - // Arrange - var handler = new JsonWebTokenHandler(); - // Act JsonWebTokenHandler.MaxActorChainLength = 10; @@ -351,71 +338,8 @@ public void MaxActorChainLength_CanBeChanged() Assert.Equal(10, JsonWebTokenHandler.MaxActorChainLength); } - [Fact] - public void NestedActorTokens_WithinMaxDepth_AreSerializedProperly() - { - // Arrange - var handler = new JsonWebTokenHandler(); - JsonWebTokenHandler.MaxActorChainLength = 3; // Allow 3 levels of nesting - - // Create nested actor identities (3 levels) - var level3Actor = new CaseSensitiveClaimsIdentity("Level3Auth"); - level3Actor.AddClaim(new Claim("sub", "level3-actor")); - level3Actor.AddClaim(new Claim("name", "Level 3 Actor")); - - var level2Actor = new CaseSensitiveClaimsIdentity("Level2Auth"); - level2Actor.AddClaim(new Claim("sub", "level2-actor")); - level2Actor.AddClaim(new Claim("name", "Level 2 Actor")); - level2Actor.Actor = level3Actor; - - var level1Actor = new CaseSensitiveClaimsIdentity("Level1Auth"); - level1Actor.AddClaim(new Claim("sub", "level1-actor")); - level1Actor.AddClaim(new Claim("name", "Level 1 Actor")); - level1Actor.Actor = level2Actor; - - // Create the main identity - var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); - mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); - mainIdentity.AddClaim(new Claim("name", "Main User")); - mainIdentity.Actor = level1Actor; - - // Create token descriptor - var tokenDescriptor = new SecurityTokenDescriptor - { - Subject = mainIdentity, - Issuer = "https://example.com", - Audience = "https://api.example.com", - SigningCredentials = Default.AsymmetricSigningCredentials - }; - - // Act - var token = handler.CreateToken(tokenDescriptor); - var jwtToken = handler.ReadJsonWebToken(token); - - // Assert - Assert.NotNull(jwtToken.Actor); - - // Check first level actor - var level1Token = handler.ReadJsonWebToken(jwtToken.Actor); - Assert.Equal("level1-actor", level1Token.Payload.GetValue("sub")); - Assert.Equal("Level 1 Actor", level1Token.Payload.GetValue("name")); - Assert.NotNull(level1Token.Actor); - - // Check second level actor - var level2Token = handler.ReadJsonWebToken(level1Token.Actor); - Assert.Equal("level2-actor", level2Token.Payload.GetValue("sub")); - Assert.Equal("Level 2 Actor", level2Token.Payload.GetValue("name")); - Assert.NotNull(level2Token.Actor); - - // Check third level actor - var level3Token = handler.ReadJsonWebToken(level2Token.Actor); - Assert.Equal("level3-actor", level3Token.Payload.GetValue("sub")); - Assert.Equal("Level 3 Actor", level3Token.Payload.GetValue("name")); - Assert.False(level3Token.Payload.HasClaim("actort"), "There should be no deeper actor at level 3"); - } - - [Fact] - public void NestedActorTokens_ExceedingMaxDepth_ThrowsException() + [Fact]// This tests that an exception is thrown when actor token at level 1 is provided as Subject and there are more nested actors than the MaxActorChainLength. + public void NestedSubjectActorTokens_ExceedingMaxDepth_ThrowsException() { var context = new CompareContext($"{this}.NestedActorTokens_ExceedingMaxDepth_ThrowsException"); @@ -482,40 +406,50 @@ public void NestedActorTokens_ExceedingMaxDepth_ThrowsException() - [Fact] - public void NestedActorTokens_WithZeroMaxDepth_ThrowsException() + [Fact]// This tests that an exception is thrown when MaxActorChainLength is set to 0 and actor specified as Subject. + public void NestedClaimsDictionaryActorTokens_ExceedingMaxDepth_ThrowsException() { - var context = new CompareContext($"{this}.NestedActorTokens_WithZeroMaxDepth_ThrowsException"); + var context = new CompareContext($"{this}.NestedClaimsDictionaryActorTokens_ExceedingMaxDepth_ThrowsException"); try { // Arrange var handler = new JsonWebTokenHandler(); - JsonWebTokenHandler.MaxActorChainLength = 0; // Setting to 0 should now cause an exception + JsonWebTokenHandler.MaxActorChainLength = 1; // Allow only 1 level of nesting - // Create an actor identity + // Create nested actor identities + var nestedActorIdentity = new CaseSensitiveClaimsIdentity("NestedActorAuth"); + nestedActorIdentity.AddClaim(new Claim("sub", "nested-actor-id")); + nestedActorIdentity.AddClaim(new Claim("name", "Nested Actor")); + + // Create actor identity with nested actor var actorIdentity = new CaseSensitiveClaimsIdentity("ActorAuth"); actorIdentity.AddClaim(new Claim("sub", "actor-subject-id")); actorIdentity.AddClaim(new Claim("name", "Actor Name")); + actorIdentity.Actor = nestedActorIdentity; // This should be ignored due to MaxActorChainLength - // Create the main identity with Actor set + // Create the main identity var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); mainIdentity.AddClaim(new Claim("name", "Main User")); - mainIdentity.Actor = actorIdentity; - // Create token descriptor + // Create token with actor in Claims dictionary var tokenDescriptor = new SecurityTokenDescriptor { Subject = mainIdentity, Issuer = "https://example.com", Audience = "https://api.example.com", - SigningCredentials = Default.AsymmetricSigningCredentials + SigningCredentials = Default.AsymmetricSigningCredentials, + Claims = new Dictionary + { + { JwtRegisteredClaimNames.Actort, actorIdentity } + } }; - // Act - This should throw a SecurityTokenException + // Act var token = handler.CreateToken(tokenDescriptor); context.Diffs.Add("Expected exception was not thrown."); + } catch (SecurityTokenException ex) { @@ -539,64 +473,12 @@ public void NestedActorTokens_WithZeroMaxDepth_ThrowsException() TestUtilities.AssertFailIfErrors(context); } - [Fact] - public void ActorTokenInClaimsDictionary_RespectsMaxActorChainLength() - { - // Arrange - var handler = new JsonWebTokenHandler(); - JsonWebTokenHandler.MaxActorChainLength = 1; // Allow only 1 level of nesting - - // Create nested actor identities - var nestedActorIdentity = new CaseSensitiveClaimsIdentity("NestedActorAuth"); - nestedActorIdentity.AddClaim(new Claim("sub", "nested-actor-id")); - nestedActorIdentity.AddClaim(new Claim("name", "Nested Actor")); - - // Create actor identity with nested actor - var actorIdentity = new CaseSensitiveClaimsIdentity("ActorAuth"); - actorIdentity.AddClaim(new Claim("sub", "actor-subject-id")); - actorIdentity.AddClaim(new Claim("name", "Actor Name")); - actorIdentity.Actor = nestedActorIdentity; // This should be ignored due to MaxActorChainLength - - // Create the main identity - var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); - mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); - mainIdentity.AddClaim(new Claim("name", "Main User")); - - // Create token with actor in Claims dictionary - var tokenDescriptor = new SecurityTokenDescriptor - { - Subject = mainIdentity, - Issuer = "https://example.com", - Audience = "https://api.example.com", - SigningCredentials = Default.AsymmetricSigningCredentials, - Claims = new Dictionary - { - { JwtRegisteredClaimNames.Actort, actorIdentity } - } - }; - - // Act - var token = handler.CreateToken(tokenDescriptor); - var jwtToken = handler.ReadJsonWebToken(token); - - // Assert - Assert.True(jwtToken.Payload.HasClaim("actort"), "JWT token should contain 'actort' claim"); - - // Read the actor token - var actorToken = handler.ReadJsonWebToken(jwtToken.Actor); - Assert.Equal("actor-subject-id", actorToken.Payload.GetValue("sub")); - Assert.Equal("Actor Name", actorToken.Payload.GetValue("name")); - - // The nested actor should not be included because MaxActorChainLength is 1 - Assert.False(actorToken.Payload.HasClaim("actort"), "There should be no nested actor claim due to MaxActorChainLength"); - } - - [Fact] + [Fact]// In this test, with 1 MaxActorChainLength, the subject actor has 2 levels of nesting while claims dictionary has 1 level. This verifies that the claim from claims dictionary is preferred and we dont get exception for the subject claim. public void ActorTokens_MixedSourceRespectMaxActorChainLength() { // Arrange var handler = new JsonWebTokenHandler(); - JsonWebTokenHandler.MaxActorChainLength = 2; // Allow 2 levels of nesting + JsonWebTokenHandler.MaxActorChainLength = 1; // Allow 1 levels of nesting // Create level 2 actor (will be in claims dictionary) var level2Actor = new CaseSensitiveClaimsIdentity("Level2Auth"); @@ -629,9 +511,9 @@ public void ActorTokens_MixedSourceRespectMaxActorChainLength() SigningCredentials = Default.AsymmetricSigningCredentials, // Add level 2 actor in claims dictionary to replace level 1's actor Claims = new Dictionary - { - { JwtRegisteredClaimNames.Actort, level2Actor } - } + { + { JwtRegisteredClaimNames.Actort, level2Actor } + } }; // Act @@ -650,18 +532,18 @@ public void ActorTokens_MixedSourceRespectMaxActorChainLength() Assert.False(actorToken.Payload.HasClaim("actort"), "There should be no nested actor claim due to MaxActorChainLength"); } - [Fact] - public void CreateToken_ExceedingMaxActorChainDepth_ThrowsException() + [Fact]// This tests that an exception is thrown when actor token at level 1 is provided in claims dictionary and there are more nested actors than the MaxActorChainLength. + public void NestedClaimTokens_ExceedingMaxDepth_ThrowsException() { - var context = new CompareContext($"{this}.CreateToken_ExceedingMaxActorChainDepth_ThrowsException"); + var context = new CompareContext($"{this}.NestedActorTokens_ExceedingMaxDepth_ThrowsException"); try { // Arrange var handler = new JsonWebTokenHandler(); - JsonWebTokenHandler.MaxActorChainLength = 2; // Set max depth to 2 for the test + JsonWebTokenHandler.MaxActorChainLength = 2; // Allow only 2 levels of nesting - // Create nested actor identities (3 levels, exceeding max depth) + // Create nested actor identities (3 levels, but we'll set MaxActorChainLength to 2) var level3Actor = new CaseSensitiveClaimsIdentity("Level3Auth"); level3Actor.AddClaim(new Claim("sub", "level3-actor")); level3Actor.AddClaim(new Claim("name", "Level 3 Actor")); @@ -676,80 +558,11 @@ public void CreateToken_ExceedingMaxActorChainDepth_ThrowsException() level1Actor.AddClaim(new Claim("name", "Level 1 Actor")); level1Actor.Actor = level2Actor; - // Create the main identity with Actor set - var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); - mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); - mainIdentity.AddClaim(new Claim("name", "Main User")); - mainIdentity.Actor = level1Actor; - - // Create token descriptor - var tokenDescriptor = new SecurityTokenDescriptor - { - Subject = mainIdentity, - Issuer = "https://example.com", - Audience = "https://api.example.com", - SigningCredentials = Default.AsymmetricSigningCredentials - }; - - // Act - This should throw a SecurityTokenException - var token = handler.CreateToken(tokenDescriptor); - context.Diffs.Add("Expected exception was not thrown."); - } - catch (SecurityTokenException ex) - { - // Assert - Verify the exception message contains the expected content - if (ex.Message.Contains("Actor chain exceeded maximum depth of 2")) - { - // Test passed - expected exception was thrown with the right message - } - else - { - context.Diffs.Add($"Exception message does not contain expected content. Message: {ex.Message}"); - } - } - catch (Exception ex) - { - // Unexpected exception type - context.Diffs.Add($"Unexpected exception type: {ex.GetType()}, Message: {ex.Message}"); - } - - TestUtilities.AssertFailIfErrors(context); - } - - [Fact] - public void CreateToken_WithClaimsActorChainExceedingMaxDepth_ThrowsException() - { - var context = new CompareContext($"{this}.CreateToken_WithClaimsActorChainExceedingMaxDepth_ThrowsException"); - - try - { - // Arrange - var handler = new JsonWebTokenHandler(); - JsonWebTokenHandler.MaxActorChainLength = 2; // Set max depth to 2 for the test - - // Create nested actor for level 3 (will exceed max depth) - var level3Actor = new CaseSensitiveClaimsIdentity("Level3Auth"); - level3Actor.AddClaim(new Claim("sub", "level3-actor")); - level3Actor.AddClaim(new Claim("name", "Level 3 Actor")); - - // Create nested actor for level 2 - var level2Actor = new CaseSensitiveClaimsIdentity("Level2Auth"); - level2Actor.AddClaim(new Claim("sub", "level2-actor")); - level2Actor.AddClaim(new Claim("name", "Level 2 Actor")); - level2Actor.Actor = level3Actor; // Will exceed max depth in Claims dictionary - - // Create actor for level 1 to be included in Claims dictionary - var level1Actor = new CaseSensitiveClaimsIdentity("Level1Auth"); - level1Actor.AddClaim(new Claim("sub", "level1-actor")); - level1Actor.AddClaim(new Claim("name", "Level 1 Actor")); - level1Actor.Actor = level2Actor; - // Create the main identity var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); mainIdentity.AddClaim(new Claim("name", "Main User")); - - // Create token with actor chain in Claims dictionary + // Create token descriptor var tokenDescriptor = new SecurityTokenDescriptor { Subject = mainIdentity, @@ -757,9 +570,9 @@ public void CreateToken_WithClaimsActorChainExceedingMaxDepth_ThrowsException() Audience = "https://api.example.com", SigningCredentials = Default.AsymmetricSigningCredentials, Claims = new Dictionary - { - { JwtRegisteredClaimNames.Actort, level1Actor } - } + { + { JwtRegisteredClaimNames.Actort, level1Actor } + } }; // Act - This should throw a SecurityTokenException @@ -769,7 +582,7 @@ public void CreateToken_WithClaimsActorChainExceedingMaxDepth_ThrowsException() catch (SecurityTokenException ex) { // Assert - Verify the exception message contains the expected content - if (ex.Message.Contains("Actor chain exceeded maximum depth of 2")) + if (ex.Message.Contains("IDX14313")) { // Test passed - expected exception was thrown with the right message } @@ -786,6 +599,7 @@ public void CreateToken_WithClaimsActorChainExceedingMaxDepth_ThrowsException() TestUtilities.AssertFailIfErrors(context); } + } } From 04257cde5938272456e1115ecf2c5f211d429e52 Mon Sep 17 00:00:00 2001 From: saurabhsathe-ms Date: Tue, 6 May 2025 09:37:55 -0700 Subject: [PATCH 11/52] NIT repairs --- .../JsonWebTokenHandler.CreateToken.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs index 1b7422717e..48b2024d2a 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs @@ -139,6 +139,7 @@ public virtual string CreateToken(SecurityTokenDescriptor tokenDescriptor) LogMessages.IDX14116, LogHelper.MarkAsNonPII(nameof(tokenDescriptor.AdditionalInnerHeaderClaims)), LogHelper.MarkAsNonPII(string.Join(", ", JwtTokenUtilities.DefaultHeaderParameters))))); + return CreateToken( tokenDescriptor, SetDefaultTimesOnTokenCreation, @@ -678,7 +679,7 @@ internal static void WriteJwsPayload( // Duplicates are resolved according to the following priority: // SecurityTokenDescriptor.{Audience/Audiences, Issuer, Expires, IssuedAt, NotBefore}, SecurityTokenDescriptor.Claims, SecurityTokenDescriptor.Subject.Claims // SecurityTokenDescriptor.Claims are KeyValuePairs, whereas SecurityTokenDescriptor.Subject.Claims are System.Security.Claims.Claim and are processed differently. - Console.WriteLine($"Actor token claim depth :{actorChainDepth}"); + if (tokenDescriptor.Claims != null && tokenDescriptor.Claims.Count > 0) { foreach (KeyValuePair kvp in tokenDescriptor.Claims) @@ -764,6 +765,7 @@ internal static void WriteJwsPayload( nbfSet = true; } + JsonPrimitives.WriteObject(ref writer, kvp.Key, kvp.Value); } if (tokenDescriptor.Claims.ContainsKey(JwtRegisteredClaimNames.Actort)) @@ -825,6 +827,7 @@ internal static void WriteJwsPayload( writer.WriteEndObject(); writer.Flush(); } + internal static void AddSubjectClaims( ref Utf8JsonWriter writer, SecurityTokenDescriptor tokenDescriptor, @@ -931,6 +934,7 @@ internal static void AddSubjectClaims( payload[claim.Type] = jsonClaimValue; } } + foreach (KeyValuePair kvp in payload) JsonPrimitives.WriteObject(ref writer, kvp.Key, kvp.Value); From d35420093558f4c76455cc0b8873fcaa8fe6afcf Mon Sep 17 00:00:00 2001 From: saurabhsathe-ms Date: Tue, 6 May 2025 11:27:44 -0700 Subject: [PATCH 12/52] Added one testcase to test MaxActorChainLength values. --- .../JsonWebTokenHandler.CreateToken.cs | 19 +++++++++- .../ActorClaimsTests.cs | 35 +++++++++++++++---- 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs index 48b2024d2a..8892bf2226 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs @@ -27,12 +27,29 @@ public partial class JsonWebTokenHandler : TokenHandler { private static readonly SecurityTokenDescriptor s_emptyTokenDescriptor = new(); + private static int _maxActorChainLength = 5; // Default value + /// /// Gets or sets the maximum depth allowed when processing nested actor tokens. /// This prevents excessive recursion when handling deeply nested actor tokens. + /// The value must be at least 0. Value 0 would mean that the actor token is not allowed to be nested. /// Default value is 5. /// - public static int MaxActorChainLength { get; set; } = 5; + /// Thrown if the value is less than 1. + public static int MaxActorChainLength + { + get => _maxActorChainLength; + set + { + if (value < 0) + throw LogHelper.LogExceptionMessage( + new ArgumentOutOfRangeException(nameof(value), + LogHelper.FormatInvariant("IDX14314: MaxActorChainLength must be non negative. Value provided: {0}", value))); + + _maxActorChainLength = value; + } + } + /// /// Creates an unsigned JSON Web Signature (JWS). /// diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs index 62c4d6c97e..575695766f 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs @@ -328,14 +328,37 @@ public void NestedActorTokenAsSubjectShouldBeProperlySerialized() TestUtilities.AssertFailIfErrors(context); } - [Fact]// This tests that the MaxActorChainLength can be changed. - public void MaxActorChainLength_CanBeChanged() + + [Fact] // This tests that the MaxActorChainLength rejects negative values but accepts all the permissible values + public void MaxActorChainLength_RejectsNegativeValues() { - // Act - JsonWebTokenHandler.MaxActorChainLength = 10; + // Arrange + int originalValue = JsonWebTokenHandler.MaxActorChainLength; - // Assert - Assert.Equal(10, JsonWebTokenHandler.MaxActorChainLength); + try + { + // Act & Assert - Valid value 0 should not throw + JsonWebTokenHandler.MaxActorChainLength = 0; + Assert.Equal(0, JsonWebTokenHandler.MaxActorChainLength); + + // Act & Assert - Negative value + var ex = Assert.Throws(() => + JsonWebTokenHandler.MaxActorChainLength = -5); + Assert.Contains("MaxActorChainLength must be non negative", ex.Message); + + // Act & Assert - Valid value 1 should not throw + JsonWebTokenHandler.MaxActorChainLength = 1; + Assert.Equal(1, JsonWebTokenHandler.MaxActorChainLength); + + // Act & Assert - Valid larger value + JsonWebTokenHandler.MaxActorChainLength = 10; + Assert.Equal(10, JsonWebTokenHandler.MaxActorChainLength); + } + finally + { + // Restore to original value + JsonWebTokenHandler.MaxActorChainLength = originalValue; + } } [Fact]// This tests that an exception is thrown when actor token at level 1 is provided as Subject and there are more nested actors than the MaxActorChainLength. From ff64dddbdaef3ad67360d2aa8727032937ba433b Mon Sep 17 00:00:00 2001 From: saurabhsathe-ms Date: Tue, 6 May 2025 13:11:49 -0700 Subject: [PATCH 13/52] Removed Console.Writeline debugging statements --- .../JsonWebTokenHandler.CreateToken.cs | 1 - .../ActorClaimsTests.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs index 8892bf2226..622088f1a9 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs @@ -803,7 +803,6 @@ internal static void WriteJwsPayload( } ClaimsIdentity actor = tokenDescriptor.Claims[JwtRegisteredClaimNames.Actort] as ClaimsIdentity; - Console.WriteLine($"Claims Identity identifier :{actor.AuthenticationType} and main token identifier {tokenDescriptor.ToString()}"); var actorTokenDescriptor = new SecurityTokenDescriptor { Subject = actor diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs index 575695766f..ab650bdf33 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs @@ -476,7 +476,6 @@ public void NestedClaimsDictionaryActorTokens_ExceedingMaxDepth_ThrowsException( } catch (SecurityTokenException ex) { - Console.WriteLine("Here is the exception message: " + ex.Message); // Assert - Verify the exception message contains the expected content if (ex.Message.Contains("IDX14313")) { From f66c4d06a5b1e9ecb07b20c9ec09f1ec2d95446b Mon Sep 17 00:00:00 2001 From: saurabhsathe-ms Date: Tue, 6 May 2025 22:22:52 -0700 Subject: [PATCH 14/52] Moved the Actor chain length parameter into a Configuration class that I will need for deserialization as well --- .../JsonWebTokenHandler.Configuration.cs | 34 +++++++++++++++++++ .../JsonWebTokenHandler.CreateToken.cs | 31 +++-------------- .../PublicAPI.Unshipped.txt | 2 -- .../ActorClaimsTests.cs | 26 +++++++------- 4 files changed, 51 insertions(+), 42 deletions(-) create mode 100644 src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.Configuration.cs diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.Configuration.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.Configuration.cs new file mode 100644 index 0000000000..5cbf2a61a8 --- /dev/null +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.Configuration.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using Microsoft.IdentityModel.Logging; + +namespace Microsoft.IdentityModel.JsonWebTokens +{ + internal class JsonWebTokenConfiguration + { + private static int s_maxActorChainLength = 5; // Default value + + /// + /// Gets or sets the maximum depth allowed when processing nested actor tokens. + /// This prevents excessive recursion when handling deeply nested actor tokens. + /// The value must be at least 0. Value 0 would mean that the actor token is not allowed to be nested. + /// Default value is 5. + /// + /// Thrown if the value is less than 1. + public static int MaxActorChainLength + { + get => s_maxActorChainLength; + set + { + if (value < 0) + throw LogHelper.LogExceptionMessage( + new ArgumentOutOfRangeException(nameof(value), + LogHelper.FormatInvariant("IDX14314: MaxActorChainLength must be non negative. Value provided: {0}", value))); + + s_maxActorChainLength = value; + } + } + } +} diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs index 622088f1a9..c595363ca8 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs @@ -27,29 +27,6 @@ public partial class JsonWebTokenHandler : TokenHandler { private static readonly SecurityTokenDescriptor s_emptyTokenDescriptor = new(); - private static int _maxActorChainLength = 5; // Default value - - /// - /// Gets or sets the maximum depth allowed when processing nested actor tokens. - /// This prevents excessive recursion when handling deeply nested actor tokens. - /// The value must be at least 0. Value 0 would mean that the actor token is not allowed to be nested. - /// Default value is 5. - /// - /// Thrown if the value is less than 1. - public static int MaxActorChainLength - { - get => _maxActorChainLength; - set - { - if (value < 0) - throw LogHelper.LogExceptionMessage( - new ArgumentOutOfRangeException(nameof(value), - LogHelper.FormatInvariant("IDX14314: MaxActorChainLength must be non negative. Value provided: {0}", value))); - - _maxActorChainLength = value; - } - } - /// /// Creates an unsigned JSON Web Signature (JWS). /// @@ -788,13 +765,13 @@ internal static void WriteJwsPayload( if (tokenDescriptor.Claims.ContainsKey(JwtRegisteredClaimNames.Actort)) { // Check for maximum actor chain depth - if (actorChainDepth >= MaxActorChainLength) + if (actorChainDepth >= JsonWebTokenConfiguration.MaxActorChainLength) { throw LogHelper.LogExceptionMessage( new SecurityTokenException( LogHelper.FormatInvariant( LogMessages.IDX14313, - LogHelper.MarkAsNonPII(MaxActorChainLength)))); + LogHelper.MarkAsNonPII(JsonWebTokenConfiguration.MaxActorChainLength)))); } if (isActorTokenSet) { @@ -868,13 +845,13 @@ internal static void AddSubjectClaims( if (!isActorTokenSet && tokenDescriptor.Subject.Actor != null) { - if (actorChainDepth >= MaxActorChainLength) + if (actorChainDepth >= JsonWebTokenConfiguration.MaxActorChainLength) { throw LogHelper.LogExceptionMessage( new SecurityTokenException( LogHelper.FormatInvariant( LogMessages.IDX14313, - LogHelper.MarkAsNonPII(MaxActorChainLength)))); + LogHelper.MarkAsNonPII(JsonWebTokenConfiguration.MaxActorChainLength)))); } var actorTokenDescriptor = new SecurityTokenDescriptor { diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.JsonWebTokens/PublicAPI.Unshipped.txt index 20ef11973e..e69de29bb2 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.JsonWebTokens/PublicAPI.Unshipped.txt @@ -1,2 +0,0 @@ -static Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.MaxActorChainLength.get -> int -static Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.MaxActorChainLength.set -> void \ No newline at end of file diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs index ab650bdf33..5130ac5c6b 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs @@ -333,31 +333,31 @@ public void NestedActorTokenAsSubjectShouldBeProperlySerialized() public void MaxActorChainLength_RejectsNegativeValues() { // Arrange - int originalValue = JsonWebTokenHandler.MaxActorChainLength; + int originalValue = JsonWebTokenConfiguration.MaxActorChainLength; try { // Act & Assert - Valid value 0 should not throw - JsonWebTokenHandler.MaxActorChainLength = 0; - Assert.Equal(0, JsonWebTokenHandler.MaxActorChainLength); + JsonWebTokenConfiguration.MaxActorChainLength = 0; + Assert.Equal(0, JsonWebTokenConfiguration.MaxActorChainLength); // Act & Assert - Negative value var ex = Assert.Throws(() => - JsonWebTokenHandler.MaxActorChainLength = -5); + JsonWebTokenConfiguration.MaxActorChainLength = -5); Assert.Contains("MaxActorChainLength must be non negative", ex.Message); // Act & Assert - Valid value 1 should not throw - JsonWebTokenHandler.MaxActorChainLength = 1; - Assert.Equal(1, JsonWebTokenHandler.MaxActorChainLength); + JsonWebTokenConfiguration.MaxActorChainLength = 1; + Assert.Equal(1, JsonWebTokenConfiguration.MaxActorChainLength); // Act & Assert - Valid larger value - JsonWebTokenHandler.MaxActorChainLength = 10; - Assert.Equal(10, JsonWebTokenHandler.MaxActorChainLength); + JsonWebTokenConfiguration.MaxActorChainLength = 10; + Assert.Equal(10, JsonWebTokenConfiguration.MaxActorChainLength); } finally { // Restore to original value - JsonWebTokenHandler.MaxActorChainLength = originalValue; + JsonWebTokenConfiguration.MaxActorChainLength = originalValue; } } @@ -370,7 +370,7 @@ public void NestedSubjectActorTokens_ExceedingMaxDepth_ThrowsException() { // Arrange var handler = new JsonWebTokenHandler(); - JsonWebTokenHandler.MaxActorChainLength = 2; // Allow only 2 levels of nesting + JsonWebTokenConfiguration.MaxActorChainLength = 2; // Allow only 2 levels of nesting // Create nested actor identities (3 levels, but we'll set MaxActorChainLength to 2) var level3Actor = new CaseSensitiveClaimsIdentity("Level3Auth"); @@ -438,7 +438,7 @@ public void NestedClaimsDictionaryActorTokens_ExceedingMaxDepth_ThrowsException( { // Arrange var handler = new JsonWebTokenHandler(); - JsonWebTokenHandler.MaxActorChainLength = 1; // Allow only 1 level of nesting + JsonWebTokenConfiguration.MaxActorChainLength = 1; // Allow only 1 level of nesting // Create nested actor identities var nestedActorIdentity = new CaseSensitiveClaimsIdentity("NestedActorAuth"); @@ -500,7 +500,7 @@ public void ActorTokens_MixedSourceRespectMaxActorChainLength() { // Arrange var handler = new JsonWebTokenHandler(); - JsonWebTokenHandler.MaxActorChainLength = 1; // Allow 1 levels of nesting + JsonWebTokenConfiguration.MaxActorChainLength = 1; // Allow 1 levels of nesting // Create level 2 actor (will be in claims dictionary) var level2Actor = new CaseSensitiveClaimsIdentity("Level2Auth"); @@ -563,7 +563,7 @@ public void NestedClaimTokens_ExceedingMaxDepth_ThrowsException() { // Arrange var handler = new JsonWebTokenHandler(); - JsonWebTokenHandler.MaxActorChainLength = 2; // Allow only 2 levels of nesting + JsonWebTokenConfiguration.MaxActorChainLength = 2; // Allow only 2 levels of nesting // Create nested actor identities (3 levels, but we'll set MaxActorChainLength to 2) var level3Actor = new CaseSensitiveClaimsIdentity("Level3Auth"); From 5dc4a49f63b60be5077a0ffeb640c75e89aa1c97 Mon Sep 17 00:00:00 2001 From: saurabhsathe-ms Date: Wed, 7 May 2025 10:00:22 -0700 Subject: [PATCH 15/52] Made the configuration class public and static --- .../JsonWebTokenHandler.Configuration.cs | 11 ++++++++++- .../PublicAPI.Unshipped.txt | 3 +++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.Configuration.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.Configuration.cs index 5cbf2a61a8..ddbd8fa4eb 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.Configuration.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.Configuration.cs @@ -6,7 +6,16 @@ namespace Microsoft.IdentityModel.JsonWebTokens { - internal class JsonWebTokenConfiguration + /// + /// Contains configuration settings for JSON Web Token processing. + /// This class provides centralized control over various aspects of JWT handling. + /// + /// + /// The JsonWebTokenConfiguration class allows applications to customize token processing + /// behavior application-wide, including settings like actor token chain depth limitations. + /// This helps prevent security issues like excessive recursion and denial of service attacks. + /// + public static class JsonWebTokenConfiguration { private static int s_maxActorChainLength = 5; // Default value diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.JsonWebTokens/PublicAPI.Unshipped.txt index e69de29bb2..a19eb99ab2 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.JsonWebTokens/PublicAPI.Unshipped.txt @@ -0,0 +1,3 @@ +Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenConfiguration +static Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenConfiguration.MaxActorChainLength.get -> int +static Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenConfiguration.MaxActorChainLength.set -> void From ccb068860ea61f1ff89282c4b85df9aaadf35db8 Mon Sep 17 00:00:00 2001 From: saurabhsathe-ms Date: Wed, 7 May 2025 15:28:30 -0700 Subject: [PATCH 16/52] Update src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.Configuration.cs Co-authored-by: Westin Musser <127992899+westin-m@users.noreply.github.com> --- .../JsonWebTokenHandler.Configuration.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.Configuration.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.Configuration.cs index ddbd8fa4eb..44d4e09697 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.Configuration.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.Configuration.cs @@ -17,7 +17,8 @@ namespace Microsoft.IdentityModel.JsonWebTokens /// public static class JsonWebTokenConfiguration { - private static int s_maxActorChainLength = 5; // Default value + // Default value + private static int s_maxActorChainLength = 5; /// /// Gets or sets the maximum depth allowed when processing nested actor tokens. From 92a6285e24c348f7600d9f2c9b8e98bfe88d4d2f Mon Sep 17 00:00:00 2001 From: saurabhsathe-ms Date: Wed, 7 May 2025 15:28:51 -0700 Subject: [PATCH 17/52] Update src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.Configuration.cs Co-authored-by: Westin Musser <127992899+westin-m@users.noreply.github.com> --- .../JsonWebTokenHandler.Configuration.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.Configuration.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.Configuration.cs index 44d4e09697..1c20a36c55 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.Configuration.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.Configuration.cs @@ -26,7 +26,7 @@ public static class JsonWebTokenConfiguration /// The value must be at least 0. Value 0 would mean that the actor token is not allowed to be nested. /// Default value is 5. /// - /// Thrown if the value is less than 1. + /// Thrown if the value is less than 0. public static int MaxActorChainLength { get => s_maxActorChainLength; From 99f7fd5bc1998452b02185b7c33364c42631e7b3 Mon Sep 17 00:00:00 2001 From: saurabhsathe-ms Date: Wed, 7 May 2025 16:30:53 -0700 Subject: [PATCH 18/52] Repaired testcases as per the suggestions on PR --- .../ActorClaimsTests.cs | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs index 5130ac5c6b..d7da2ce2d6 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs @@ -409,11 +409,7 @@ public void NestedSubjectActorTokens_ExceedingMaxDepth_ThrowsException() catch (SecurityTokenException ex) { // Assert - Verify the exception message contains the expected content - if (ex.Message.Contains("IDX14313")) - { - // Test passed - expected exception was thrown with the right message - } - else + if (!ex.Message.Contains("IDX14313")) { context.Diffs.Add($"Exception message does not contain expected content. Message: {ex.Message}"); } @@ -477,11 +473,7 @@ public void NestedClaimsDictionaryActorTokens_ExceedingMaxDepth_ThrowsException( catch (SecurityTokenException ex) { // Assert - Verify the exception message contains the expected content - if (ex.Message.Contains("IDX14313")) - { - // Test passed - expected exception was thrown with the right message - } - else + if (!ex.Message.Contains("IDX14313")) { context.Diffs.Add($"Exception message does not contain expected content. Message: {ex.Message}"); } @@ -604,11 +596,7 @@ public void NestedClaimTokens_ExceedingMaxDepth_ThrowsException() catch (SecurityTokenException ex) { // Assert - Verify the exception message contains the expected content - if (ex.Message.Contains("IDX14313")) - { - // Test passed - expected exception was thrown with the right message - } - else + if (!ex.Message.Contains("IDX14313")) { context.Diffs.Add($"Exception message does not contain expected content. Message: {ex.Message}"); } From 4185264f49e9dd699aa3f2b6576b10fa73f6f521 Mon Sep 17 00:00:00 2001 From: saurabhsathe-ms Date: Wed, 7 May 2025 16:39:30 -0700 Subject: [PATCH 19/52] Renamed JsonWebTokenConfiguration to JsonWebTokenHandlerConfiguration --- .../JsonWebTokenHandler.Configuration.cs | 4 +-- .../JsonWebTokenHandler.CreateToken.cs | 8 +++--- .../PublicAPI.Unshipped.txt | 6 ++--- .../ActorClaimsTests.cs | 26 +++++++++---------- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.Configuration.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.Configuration.cs index 1c20a36c55..3f9bb320cb 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.Configuration.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.Configuration.cs @@ -11,11 +11,11 @@ namespace Microsoft.IdentityModel.JsonWebTokens /// This class provides centralized control over various aspects of JWT handling. /// /// - /// The JsonWebTokenConfiguration class allows applications to customize token processing + /// The JsonWebTokenHandlerConfiguration class allows applications to customize token processing /// behavior application-wide, including settings like actor token chain depth limitations. /// This helps prevent security issues like excessive recursion and denial of service attacks. /// - public static class JsonWebTokenConfiguration + public static class JsonWebTokenHandlerConfiguration { // Default value private static int s_maxActorChainLength = 5; diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs index c595363ca8..775f4e71d8 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs @@ -765,13 +765,13 @@ internal static void WriteJwsPayload( if (tokenDescriptor.Claims.ContainsKey(JwtRegisteredClaimNames.Actort)) { // Check for maximum actor chain depth - if (actorChainDepth >= JsonWebTokenConfiguration.MaxActorChainLength) + if (actorChainDepth >= JsonWebTokenHandlerConfiguration.MaxActorChainLength) { throw LogHelper.LogExceptionMessage( new SecurityTokenException( LogHelper.FormatInvariant( LogMessages.IDX14313, - LogHelper.MarkAsNonPII(JsonWebTokenConfiguration.MaxActorChainLength)))); + LogHelper.MarkAsNonPII(JsonWebTokenHandlerConfiguration.MaxActorChainLength)))); } if (isActorTokenSet) { @@ -845,13 +845,13 @@ internal static void AddSubjectClaims( if (!isActorTokenSet && tokenDescriptor.Subject.Actor != null) { - if (actorChainDepth >= JsonWebTokenConfiguration.MaxActorChainLength) + if (actorChainDepth >= JsonWebTokenHandlerConfiguration.MaxActorChainLength) { throw LogHelper.LogExceptionMessage( new SecurityTokenException( LogHelper.FormatInvariant( LogMessages.IDX14313, - LogHelper.MarkAsNonPII(JsonWebTokenConfiguration.MaxActorChainLength)))); + LogHelper.MarkAsNonPII(JsonWebTokenHandlerConfiguration.MaxActorChainLength)))); } var actorTokenDescriptor = new SecurityTokenDescriptor { diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.JsonWebTokens/PublicAPI.Unshipped.txt index a19eb99ab2..9c8a5eb04a 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.JsonWebTokens/PublicAPI.Unshipped.txt @@ -1,3 +1,3 @@ -Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenConfiguration -static Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenConfiguration.MaxActorChainLength.get -> int -static Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenConfiguration.MaxActorChainLength.set -> void +Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandlerConfiguration +static Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandlerConfiguration.MaxActorChainLength.get -> int +static Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandlerConfiguration.MaxActorChainLength.set -> void diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs index d7da2ce2d6..777db3f24c 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs @@ -333,31 +333,31 @@ public void NestedActorTokenAsSubjectShouldBeProperlySerialized() public void MaxActorChainLength_RejectsNegativeValues() { // Arrange - int originalValue = JsonWebTokenConfiguration.MaxActorChainLength; + int originalValue = JsonWebTokenHandlerConfiguration.MaxActorChainLength; try { // Act & Assert - Valid value 0 should not throw - JsonWebTokenConfiguration.MaxActorChainLength = 0; - Assert.Equal(0, JsonWebTokenConfiguration.MaxActorChainLength); + JsonWebTokenHandlerConfiguration.MaxActorChainLength = 0; + Assert.Equal(0, JsonWebTokenHandlerConfiguration.MaxActorChainLength); // Act & Assert - Negative value var ex = Assert.Throws(() => - JsonWebTokenConfiguration.MaxActorChainLength = -5); + JsonWebTokenHandlerConfiguration.MaxActorChainLength = -5); Assert.Contains("MaxActorChainLength must be non negative", ex.Message); // Act & Assert - Valid value 1 should not throw - JsonWebTokenConfiguration.MaxActorChainLength = 1; - Assert.Equal(1, JsonWebTokenConfiguration.MaxActorChainLength); + JsonWebTokenHandlerConfiguration.MaxActorChainLength = 1; + Assert.Equal(1, JsonWebTokenHandlerConfiguration.MaxActorChainLength); // Act & Assert - Valid larger value - JsonWebTokenConfiguration.MaxActorChainLength = 10; - Assert.Equal(10, JsonWebTokenConfiguration.MaxActorChainLength); + JsonWebTokenHandlerConfiguration.MaxActorChainLength = 10; + Assert.Equal(10, JsonWebTokenHandlerConfiguration.MaxActorChainLength); } finally { // Restore to original value - JsonWebTokenConfiguration.MaxActorChainLength = originalValue; + JsonWebTokenHandlerConfiguration.MaxActorChainLength = originalValue; } } @@ -370,7 +370,7 @@ public void NestedSubjectActorTokens_ExceedingMaxDepth_ThrowsException() { // Arrange var handler = new JsonWebTokenHandler(); - JsonWebTokenConfiguration.MaxActorChainLength = 2; // Allow only 2 levels of nesting + JsonWebTokenHandlerConfiguration.MaxActorChainLength = 2; // Allow only 2 levels of nesting // Create nested actor identities (3 levels, but we'll set MaxActorChainLength to 2) var level3Actor = new CaseSensitiveClaimsIdentity("Level3Auth"); @@ -434,7 +434,7 @@ public void NestedClaimsDictionaryActorTokens_ExceedingMaxDepth_ThrowsException( { // Arrange var handler = new JsonWebTokenHandler(); - JsonWebTokenConfiguration.MaxActorChainLength = 1; // Allow only 1 level of nesting + JsonWebTokenHandlerConfiguration.MaxActorChainLength = 1; // Allow only 1 level of nesting // Create nested actor identities var nestedActorIdentity = new CaseSensitiveClaimsIdentity("NestedActorAuth"); @@ -492,7 +492,7 @@ public void ActorTokens_MixedSourceRespectMaxActorChainLength() { // Arrange var handler = new JsonWebTokenHandler(); - JsonWebTokenConfiguration.MaxActorChainLength = 1; // Allow 1 levels of nesting + JsonWebTokenHandlerConfiguration.MaxActorChainLength = 1; // Allow 1 levels of nesting // Create level 2 actor (will be in claims dictionary) var level2Actor = new CaseSensitiveClaimsIdentity("Level2Auth"); @@ -555,7 +555,7 @@ public void NestedClaimTokens_ExceedingMaxDepth_ThrowsException() { // Arrange var handler = new JsonWebTokenHandler(); - JsonWebTokenConfiguration.MaxActorChainLength = 2; // Allow only 2 levels of nesting + JsonWebTokenHandlerConfiguration.MaxActorChainLength = 2; // Allow only 2 levels of nesting // Create nested actor identities (3 levels, but we'll set MaxActorChainLength to 2) var level3Actor = new CaseSensitiveClaimsIdentity("Level3Auth"); From 8dcbdb3c53089cf1404f81e63b97b386e5f5d0c7 Mon Sep 17 00:00:00 2001 From: saurabhsathe-ms Date: Wed, 7 May 2025 16:54:30 -0700 Subject: [PATCH 20/52] Removed the comments on testcase --- .../ActorClaimsTests.cs | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs index 777db3f24c..3a4cb60c89 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs @@ -13,7 +13,7 @@ namespace Microsoft.IdentityModel.Tests { public class ActorClaimsTests { - [Fact] // This tests that the actor token is properly serialized when added to the claims dictionary without any nesting. + [Fact] public void ActorTokenInClaimsDictionaryShouldBeProperlySerialized() { var context = new CompareContext($"{this}.ActorTokenInClaimsDictionaryShouldBeProperlySerialized"); @@ -69,7 +69,7 @@ public void ActorTokenInClaimsDictionaryShouldBeProperlySerialized() TestUtilities.AssertFailIfErrors(context); } - [Fact]// This tests that the actor token is properly serialized when added as Subject without any nesting. + [Fact] public void ActorTokenAsSubjectShouldBeProperlySerialized() { var context = new CompareContext($"{this}.ActorTokenAsSubjectShouldBeProperlySerialized"); @@ -122,7 +122,7 @@ public void ActorTokenAsSubjectShouldBeProperlySerialized() TestUtilities.AssertFailIfErrors(context); } - [Fact]// This tests that the actor token from claims is preferred over that of subject when both are specified. + [Fact] public void ActorTokenInBothClaimsAndSubjectShouldPreferClaimsValue() { var context = new CompareContext($"{this}.ActorTokenInBothClaimsAndSubjectShouldPreferClaimsValue"); @@ -186,7 +186,7 @@ public void ActorTokenInBothClaimsAndSubjectShouldPreferClaimsValue() TestUtilities.AssertFailIfErrors(context); } - [Fact]// This tests that the actor token is properly serialized when added to the claims dictionary with nested actors. + [Fact] public void NestedActorTokenInClaimsDictionaryShouldBeProperlySerialized() { var context = new CompareContext($"{this}.NestedActorTokenInClaimsDictionaryShouldBeProperlySerialized"); @@ -259,7 +259,7 @@ public void NestedActorTokenInClaimsDictionaryShouldBeProperlySerialized() TestUtilities.AssertFailIfErrors(context); } - [Fact]//This tests that the actor token is properly serialized when added as Subject with nested actors. + [Fact] public void NestedActorTokenAsSubjectShouldBeProperlySerialized() { var context = new CompareContext($"{this}.NestedActorTokenAsSubjectShouldBeProperlySerialized"); @@ -329,7 +329,7 @@ public void NestedActorTokenAsSubjectShouldBeProperlySerialized() TestUtilities.AssertFailIfErrors(context); } - [Fact] // This tests that the MaxActorChainLength rejects negative values but accepts all the permissible values + [Fact] public void MaxActorChainLength_RejectsNegativeValues() { // Arrange @@ -361,7 +361,7 @@ public void MaxActorChainLength_RejectsNegativeValues() } } - [Fact]// This tests that an exception is thrown when actor token at level 1 is provided as Subject and there are more nested actors than the MaxActorChainLength. + [Fact] public void NestedSubjectActorTokens_ExceedingMaxDepth_ThrowsException() { var context = new CompareContext($"{this}.NestedActorTokens_ExceedingMaxDepth_ThrowsException"); @@ -423,9 +423,7 @@ public void NestedSubjectActorTokens_ExceedingMaxDepth_ThrowsException() TestUtilities.AssertFailIfErrors(context); } - - - [Fact]// This tests that an exception is thrown when MaxActorChainLength is set to 0 and actor specified as Subject. + [Fact] public void NestedClaimsDictionaryActorTokens_ExceedingMaxDepth_ThrowsException() { var context = new CompareContext($"{this}.NestedClaimsDictionaryActorTokens_ExceedingMaxDepth_ThrowsException"); @@ -487,7 +485,7 @@ public void NestedClaimsDictionaryActorTokens_ExceedingMaxDepth_ThrowsException( TestUtilities.AssertFailIfErrors(context); } - [Fact]// In this test, with 1 MaxActorChainLength, the subject actor has 2 levels of nesting while claims dictionary has 1 level. This verifies that the claim from claims dictionary is preferred and we dont get exception for the subject claim. + [Fact] public void ActorTokens_MixedSourceRespectMaxActorChainLength() { // Arrange @@ -546,7 +544,7 @@ public void ActorTokens_MixedSourceRespectMaxActorChainLength() Assert.False(actorToken.Payload.HasClaim("actort"), "There should be no nested actor claim due to MaxActorChainLength"); } - [Fact]// This tests that an exception is thrown when actor token at level 1 is provided in claims dictionary and there are more nested actors than the MaxActorChainLength. + [Fact] public void NestedClaimTokens_ExceedingMaxDepth_ThrowsException() { var context = new CompareContext($"{this}.NestedActorTokens_ExceedingMaxDepth_ThrowsException"); From 89cd0e470e6e1db91b2a366d01fa9ac9b9a1116f Mon Sep 17 00:00:00 2001 From: saurabhsathe-ms Date: Wed, 7 May 2025 18:00:30 -0700 Subject: [PATCH 21/52] Updated the configuration summary. --- .../JsonWebTokenHandler.Configuration.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.Configuration.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.Configuration.cs index 3f9bb320cb..9fa7fd451c 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.Configuration.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.Configuration.cs @@ -7,8 +7,7 @@ namespace Microsoft.IdentityModel.JsonWebTokens { /// - /// Contains configuration settings for JSON Web Token processing. - /// This class provides centralized control over various aspects of JWT handling. + /// Contains configuration settings for JWT handler. /// /// /// The JsonWebTokenHandlerConfiguration class allows applications to customize token processing From 3ad51eb7d79ca0752fedd7f063163c2915df2cf7 Mon Sep 17 00:00:00 2001 From: saurabhsathe-ms Date: Wed, 7 May 2025 23:22:44 -0700 Subject: [PATCH 22/52] Moved the MaxActorChainLength to SecurityTokenDescriptor --- .../JsonWebTokenHandler.Configuration.cs | 43 ------------------- .../JsonWebTokenHandler.CreateToken.cs | 8 ++-- .../PublicAPI.Unshipped.txt | 3 -- .../PublicAPI/net462/PublicAPI.Unshipped.txt | 2 + .../PublicAPI/net472/PublicAPI.Unshipped.txt | 2 + .../PublicAPI/net6.0/PublicAPI.Unshipped.txt | 2 + .../PublicAPI/net8.0/PublicAPI.Unshipped.txt | 2 + .../PublicAPI/net9.0/PublicAPI.Unshipped.txt | 2 + .../netstandard2.0/PublicAPI.Unshipped.txt | 2 + .../SecurityTokenDescriptor.cs | 23 ++++++++++ .../ActorClaimsTests.cs | 26 +++++------ 11 files changed, 52 insertions(+), 63 deletions(-) delete mode 100644 src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.Configuration.cs diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.Configuration.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.Configuration.cs deleted file mode 100644 index 9fa7fd451c..0000000000 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.Configuration.cs +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using Microsoft.IdentityModel.Logging; - -namespace Microsoft.IdentityModel.JsonWebTokens -{ - /// - /// Contains configuration settings for JWT handler. - /// - /// - /// The JsonWebTokenHandlerConfiguration class allows applications to customize token processing - /// behavior application-wide, including settings like actor token chain depth limitations. - /// This helps prevent security issues like excessive recursion and denial of service attacks. - /// - public static class JsonWebTokenHandlerConfiguration - { - // Default value - private static int s_maxActorChainLength = 5; - - /// - /// Gets or sets the maximum depth allowed when processing nested actor tokens. - /// This prevents excessive recursion when handling deeply nested actor tokens. - /// The value must be at least 0. Value 0 would mean that the actor token is not allowed to be nested. - /// Default value is 5. - /// - /// Thrown if the value is less than 0. - public static int MaxActorChainLength - { - get => s_maxActorChainLength; - set - { - if (value < 0) - throw LogHelper.LogExceptionMessage( - new ArgumentOutOfRangeException(nameof(value), - LogHelper.FormatInvariant("IDX14314: MaxActorChainLength must be non negative. Value provided: {0}", value))); - - s_maxActorChainLength = value; - } - } - } -} diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs index 775f4e71d8..c2352e5223 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs @@ -765,13 +765,13 @@ internal static void WriteJwsPayload( if (tokenDescriptor.Claims.ContainsKey(JwtRegisteredClaimNames.Actort)) { // Check for maximum actor chain depth - if (actorChainDepth >= JsonWebTokenHandlerConfiguration.MaxActorChainLength) + if (actorChainDepth >= SecurityTokenDescriptor.MaxActorChainLength) { throw LogHelper.LogExceptionMessage( new SecurityTokenException( LogHelper.FormatInvariant( LogMessages.IDX14313, - LogHelper.MarkAsNonPII(JsonWebTokenHandlerConfiguration.MaxActorChainLength)))); + LogHelper.MarkAsNonPII(SecurityTokenDescriptor.MaxActorChainLength)))); } if (isActorTokenSet) { @@ -845,13 +845,13 @@ internal static void AddSubjectClaims( if (!isActorTokenSet && tokenDescriptor.Subject.Actor != null) { - if (actorChainDepth >= JsonWebTokenHandlerConfiguration.MaxActorChainLength) + if (actorChainDepth >= SecurityTokenDescriptor.MaxActorChainLength) { throw LogHelper.LogExceptionMessage( new SecurityTokenException( LogHelper.FormatInvariant( LogMessages.IDX14313, - LogHelper.MarkAsNonPII(JsonWebTokenHandlerConfiguration.MaxActorChainLength)))); + LogHelper.MarkAsNonPII(SecurityTokenDescriptor.MaxActorChainLength)))); } var actorTokenDescriptor = new SecurityTokenDescriptor { diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.JsonWebTokens/PublicAPI.Unshipped.txt index 9c8a5eb04a..e69de29bb2 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.JsonWebTokens/PublicAPI.Unshipped.txt @@ -1,3 +0,0 @@ -Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandlerConfiguration -static Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandlerConfiguration.MaxActorChainLength.get -> int -static Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandlerConfiguration.MaxActorChainLength.set -> void diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net462/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net462/PublicAPI.Unshipped.txt index e69de29bb2..a5c8cf3311 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net462/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net462/PublicAPI.Unshipped.txt @@ -0,0 +1,2 @@ +static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.get -> int +static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.set -> void \ No newline at end of file diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net472/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net472/PublicAPI.Unshipped.txt index e69de29bb2..096dbb88b6 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net472/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net472/PublicAPI.Unshipped.txt @@ -0,0 +1,2 @@ +static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.get -> int +static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.set -> void diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net6.0/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net6.0/PublicAPI.Unshipped.txt index e69de29bb2..096dbb88b6 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net6.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net6.0/PublicAPI.Unshipped.txt @@ -0,0 +1,2 @@ +static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.get -> int +static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.set -> void diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net8.0/PublicAPI.Unshipped.txt index e69de29bb2..096dbb88b6 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net8.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net8.0/PublicAPI.Unshipped.txt @@ -0,0 +1,2 @@ +static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.get -> int +static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.set -> void diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net9.0/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net9.0/PublicAPI.Unshipped.txt index e69de29bb2..096dbb88b6 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net9.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net9.0/PublicAPI.Unshipped.txt @@ -0,0 +1,2 @@ +static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.get -> int +static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.set -> void diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt index e69de29bb2..096dbb88b6 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt @@ -0,0 +1,2 @@ +static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.get -> int +static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.set -> void diff --git a/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs b/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs index 3b327a48b4..86cb12f606 100644 --- a/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs +++ b/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs @@ -6,6 +6,7 @@ using System.ComponentModel; using System.Security.Claims; using System.Threading; +using Microsoft.IdentityModel.Logging; namespace Microsoft.IdentityModel.Tokens { @@ -116,5 +117,27 @@ public class SecurityTokenDescriptor /// [DefaultValue(true)] public bool IncludeKeyIdInHeader { get; set; } = true; + + private static int s_maxActorChainLength = 5; + /// + /// Gets or sets the maximum depth allowed when processing nested actor tokens. + /// This prevents excessive recursion when handling deeply nested actor tokens. + /// The value must be at least 0. Value 0 would mean that the actor token is not allowed to be nested. + /// Default value is 5. Max value is also 5 + /// + /// Thrown if the value is less than 0. + public static int MaxActorChainLength + { + get => s_maxActorChainLength; + set + { + if (value < 0) + throw LogHelper.LogExceptionMessage( + new ArgumentOutOfRangeException(nameof(value), + LogHelper.FormatInvariant("MaxActorChainLength must be non negative and less than or equal to 5. Value provided: {0}", value))); + + s_maxActorChainLength = value; + } + } } } diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs index 3a4cb60c89..0929ddc58c 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs @@ -333,31 +333,31 @@ public void NestedActorTokenAsSubjectShouldBeProperlySerialized() public void MaxActorChainLength_RejectsNegativeValues() { // Arrange - int originalValue = JsonWebTokenHandlerConfiguration.MaxActorChainLength; + int originalValue = SecurityTokenDescriptor.MaxActorChainLength; try { // Act & Assert - Valid value 0 should not throw - JsonWebTokenHandlerConfiguration.MaxActorChainLength = 0; - Assert.Equal(0, JsonWebTokenHandlerConfiguration.MaxActorChainLength); + SecurityTokenDescriptor.MaxActorChainLength = 0; + Assert.Equal(0, SecurityTokenDescriptor.MaxActorChainLength); // Act & Assert - Negative value var ex = Assert.Throws(() => - JsonWebTokenHandlerConfiguration.MaxActorChainLength = -5); + SecurityTokenDescriptor.MaxActorChainLength = -5); Assert.Contains("MaxActorChainLength must be non negative", ex.Message); // Act & Assert - Valid value 1 should not throw - JsonWebTokenHandlerConfiguration.MaxActorChainLength = 1; - Assert.Equal(1, JsonWebTokenHandlerConfiguration.MaxActorChainLength); + SecurityTokenDescriptor.MaxActorChainLength = 1; + Assert.Equal(1, SecurityTokenDescriptor.MaxActorChainLength); // Act & Assert - Valid larger value - JsonWebTokenHandlerConfiguration.MaxActorChainLength = 10; - Assert.Equal(10, JsonWebTokenHandlerConfiguration.MaxActorChainLength); + SecurityTokenDescriptor.MaxActorChainLength = 10; + Assert.Equal(10, SecurityTokenDescriptor.MaxActorChainLength); } finally { // Restore to original value - JsonWebTokenHandlerConfiguration.MaxActorChainLength = originalValue; + SecurityTokenDescriptor.MaxActorChainLength = originalValue; } } @@ -370,7 +370,7 @@ public void NestedSubjectActorTokens_ExceedingMaxDepth_ThrowsException() { // Arrange var handler = new JsonWebTokenHandler(); - JsonWebTokenHandlerConfiguration.MaxActorChainLength = 2; // Allow only 2 levels of nesting + SecurityTokenDescriptor.MaxActorChainLength = 2; // Allow only 2 levels of nesting // Create nested actor identities (3 levels, but we'll set MaxActorChainLength to 2) var level3Actor = new CaseSensitiveClaimsIdentity("Level3Auth"); @@ -432,7 +432,7 @@ public void NestedClaimsDictionaryActorTokens_ExceedingMaxDepth_ThrowsException( { // Arrange var handler = new JsonWebTokenHandler(); - JsonWebTokenHandlerConfiguration.MaxActorChainLength = 1; // Allow only 1 level of nesting + SecurityTokenDescriptor.MaxActorChainLength = 1; // Allow only 1 level of nesting // Create nested actor identities var nestedActorIdentity = new CaseSensitiveClaimsIdentity("NestedActorAuth"); @@ -490,7 +490,7 @@ public void ActorTokens_MixedSourceRespectMaxActorChainLength() { // Arrange var handler = new JsonWebTokenHandler(); - JsonWebTokenHandlerConfiguration.MaxActorChainLength = 1; // Allow 1 levels of nesting + SecurityTokenDescriptor.MaxActorChainLength = 1; // Allow 1 levels of nesting // Create level 2 actor (will be in claims dictionary) var level2Actor = new CaseSensitiveClaimsIdentity("Level2Auth"); @@ -553,7 +553,7 @@ public void NestedClaimTokens_ExceedingMaxDepth_ThrowsException() { // Arrange var handler = new JsonWebTokenHandler(); - JsonWebTokenHandlerConfiguration.MaxActorChainLength = 2; // Allow only 2 levels of nesting + SecurityTokenDescriptor.MaxActorChainLength = 2; // Allow only 2 levels of nesting // Create nested actor identities (3 levels, but we'll set MaxActorChainLength to 2) var level3Actor = new CaseSensitiveClaimsIdentity("Level3Auth"); From 3f348bc0f2677d1319b46854b6ab63db00e30eaf Mon Sep 17 00:00:00 2001 From: saurabhsathe-ms Date: Thu, 8 May 2025 00:08:01 -0700 Subject: [PATCH 23/52] All test cases passed and introduced JWTclaimTypeName --- .../JsonWebTokenHandler.CreateToken.cs | 10 +++--- .../InternalAPI.Unshipped.txt | 1 + .../LogMessages.cs | 1 + .../PublicAPI/net462/PublicAPI.Unshipped.txt | 2 ++ .../PublicAPI/net472/PublicAPI.Unshipped.txt | 2 ++ .../PublicAPI/net6.0/PublicAPI.Unshipped.txt | 2 ++ .../PublicAPI/net8.0/PublicAPI.Unshipped.txt | 2 ++ .../PublicAPI/net9.0/PublicAPI.Unshipped.txt | 2 ++ .../netstandard2.0/PublicAPI.Unshipped.txt | 2 ++ .../SecurityTokenDescriptor.cs | 35 +++++++++++++++++-- .../ActorClaimsTests.cs | 14 ++++++-- 11 files changed, 64 insertions(+), 9 deletions(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs index c2352e5223..bed4c0d5a9 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs @@ -678,7 +678,7 @@ internal static void WriteJwsPayload( { foreach (KeyValuePair kvp in tokenDescriptor.Claims) { - if (kvp.Key.Equals(JwtRegisteredClaimNames.Actort, StringComparison.Ordinal)) + if (kvp.Key.Equals(SecurityTokenDescriptor.ActorClaimTypeName, StringComparison.Ordinal)) { continue; } @@ -762,7 +762,7 @@ internal static void WriteJwsPayload( JsonPrimitives.WriteObject(ref writer, kvp.Key, kvp.Value); } - if (tokenDescriptor.Claims.ContainsKey(JwtRegisteredClaimNames.Actort)) + if (tokenDescriptor.Claims.ContainsKey(SecurityTokenDescriptor.ActorClaimTypeName)) { // Check for maximum actor chain depth if (actorChainDepth >= SecurityTokenDescriptor.MaxActorChainLength) @@ -779,14 +779,14 @@ internal static void WriteJwsPayload( LogHelper.LogInformation(LogHelper.FormatInvariant(LogMessages.IDX14113, LogHelper.MarkAsNonPII(nameof(tokenDescriptor.Expires)))); } - ClaimsIdentity actor = tokenDescriptor.Claims[JwtRegisteredClaimNames.Actort] as ClaimsIdentity; + ClaimsIdentity actor = tokenDescriptor.Claims[SecurityTokenDescriptor.ActorClaimTypeName] as ClaimsIdentity; var actorTokenDescriptor = new SecurityTokenDescriptor { Subject = actor }; actorChainDepth = actorChainDepth + 1; string actorToken = CreateToken(actorTokenDescriptor, false, 0, actorChainDepth); - JsonPrimitives.WriteObject(ref writer, JwtRegisteredClaimNames.Actort, actorToken); + JsonPrimitives.WriteObject(ref writer, SecurityTokenDescriptor.ActorClaimTypeName, actorToken); isActorTokenSet = true; } } @@ -859,7 +859,7 @@ internal static void AddSubjectClaims( }; string actorToken = CreateToken(actorTokenDescriptor, false, 0, actorChainDepth + 1); - writer.WritePropertyName(JwtRegisteredClaimNames.Actort); + writer.WritePropertyName(SecurityTokenDescriptor.ActorClaimTypeName); writer.WriteStringValue(actorToken); isActorTokenSet = true; } diff --git a/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt index 666d342acc..cda77fce85 100644 --- a/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt @@ -1,2 +1,3 @@ const Microsoft.IdentityModel.Tokens.AppContextSwitches.UseCapitalizedXMLTypeAttrSwitch = "Switch.Microsoft.IdentityModel.UseCapitalizedXMLTypeAttr" -> string +const Microsoft.IdentityModel.Tokens.LogMessages.IDX11027 = "IDX11027: Invalid JsonWebToken handler configuration parameter value provided for {0}" -> string static Microsoft.IdentityModel.Tokens.AppContextSwitches.UseCapitalizedXMLTypeAttr.get -> bool \ No newline at end of file diff --git a/src/Microsoft.IdentityModel.Tokens/LogMessages.cs b/src/Microsoft.IdentityModel.Tokens/LogMessages.cs index 163d8e8d73..aa14d63ff2 100644 --- a/src/Microsoft.IdentityModel.Tokens/LogMessages.cs +++ b/src/Microsoft.IdentityModel.Tokens/LogMessages.cs @@ -285,6 +285,7 @@ internal static class LogMessages public const string IDX11023 = "IDX11023: Expecting json reader to be positioned on '{0}', reader was positioned at: '{1}', Reading: '{2}', Position: '{3}', CurrentDepth: '{4}', BytesConsumed: '{5}'."; public const string IDX11025 = "IDX11025: Cannot serialize object of type: '{0}' into property: '{1}'."; public const string IDX11026 = "IDX11026: Unable to get claim value as a string from claim type:'{0}', value type was:'{1}'. Acceptable types are String, IList, and System.Text.Json.JsonElement."; + public const string IDX11027 = "IDX11027: Invalid JsonWebToken handler configuration parameter value provided for {0}"; #pragma warning restore 1591 } diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net462/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net462/PublicAPI.Unshipped.txt index a5c8cf3311..90f0a238ef 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net462/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net462/PublicAPI.Unshipped.txt @@ -1,2 +1,4 @@ +static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimTypeName.get -> string +static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimTypeName.set -> void static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.get -> int static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.set -> void \ No newline at end of file diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net472/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net472/PublicAPI.Unshipped.txt index 096dbb88b6..2f44a2bbe9 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net472/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net472/PublicAPI.Unshipped.txt @@ -1,2 +1,4 @@ +static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimTypeName.get -> string +static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimTypeName.set -> void static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.get -> int static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.set -> void diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net6.0/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net6.0/PublicAPI.Unshipped.txt index 096dbb88b6..2f44a2bbe9 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net6.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net6.0/PublicAPI.Unshipped.txt @@ -1,2 +1,4 @@ +static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimTypeName.get -> string +static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimTypeName.set -> void static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.get -> int static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.set -> void diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net8.0/PublicAPI.Unshipped.txt index 096dbb88b6..2f44a2bbe9 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net8.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net8.0/PublicAPI.Unshipped.txt @@ -1,2 +1,4 @@ +static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimTypeName.get -> string +static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimTypeName.set -> void static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.get -> int static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.set -> void diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net9.0/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net9.0/PublicAPI.Unshipped.txt index 096dbb88b6..2f44a2bbe9 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net9.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net9.0/PublicAPI.Unshipped.txt @@ -1,2 +1,4 @@ +static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimTypeName.get -> string +static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimTypeName.set -> void static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.get -> int static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.set -> void diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt index 096dbb88b6..2f44a2bbe9 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt @@ -1,2 +1,4 @@ +static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimTypeName.get -> string +static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimTypeName.set -> void static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.get -> int static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.set -> void diff --git a/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs b/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs index 86cb12f606..971872f088 100644 --- a/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs +++ b/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs @@ -133,11 +133,42 @@ public static int MaxActorChainLength { if (value < 0) throw LogHelper.LogExceptionMessage( - new ArgumentOutOfRangeException(nameof(value), - LogHelper.FormatInvariant("MaxActorChainLength must be non negative and less than or equal to 5. Value provided: {0}", value))); + new ArgumentOutOfRangeException( + LogHelper.FormatInvariant( + LogMessages.IDX11027, + LogHelper.MarkAsNonPII("MaxActorChainLength")) + + ". Permissible values are integers in range 0 to 5")); s_maxActorChainLength = value; } } + + private static string s_actoryClaimTypeName = "act"; + /// + /// Gets or sets the claim type name for the actor claim. + /// Permissible values are 'act' or 'actort'. + /// + /// + /// Thrown if the value is null. + /// + /// + /// Thrown if the value is not 'act' or 'actort'. + /// + public static string ActorClaimTypeName + { + get => s_actoryClaimTypeName; + set + { + if (string.IsNullOrEmpty(value) || (!value.Equals("act") && !value.Equals("actort"))) + throw LogHelper.LogExceptionMessage( + new ArgumentOutOfRangeException( + LogHelper.FormatInvariant( + LogMessages.IDX11027, + LogHelper.MarkAsNonPII("ActorClaimTypeName")) + + ". Permissible values are 'act' or 'actort'.")); + + s_actoryClaimTypeName = value; + } + } } } diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs index 0929ddc58c..dc1ced9d8d 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs @@ -17,10 +17,10 @@ public class ActorClaimsTests public void ActorTokenInClaimsDictionaryShouldBeProperlySerialized() { var context = new CompareContext($"{this}.ActorTokenInClaimsDictionaryShouldBeProperlySerialized"); + SecurityTokenDescriptor.ActorClaimTypeName = "actort"; try { - AppContext.SetSwitch(AppContextSwitches.UseClaimsIdentityTypeSwitch, true); // Create a ClaimsIdentity for the actor var actorIdentity = new CaseSensitiveClaimsIdentity("ActorAuth"); actorIdentity.AddClaim(new Claim("sub", "actor-subject-id")); @@ -73,6 +73,7 @@ public void ActorTokenInClaimsDictionaryShouldBeProperlySerialized() public void ActorTokenAsSubjectShouldBeProperlySerialized() { var context = new CompareContext($"{this}.ActorTokenAsSubjectShouldBeProperlySerialized"); + SecurityTokenDescriptor.ActorClaimTypeName = "actort"; try { @@ -126,6 +127,7 @@ public void ActorTokenAsSubjectShouldBeProperlySerialized() public void ActorTokenInBothClaimsAndSubjectShouldPreferClaimsValue() { var context = new CompareContext($"{this}.ActorTokenInBothClaimsAndSubjectShouldPreferClaimsValue"); + SecurityTokenDescriptor.ActorClaimTypeName = "actort"; try { @@ -190,6 +192,7 @@ public void ActorTokenInBothClaimsAndSubjectShouldPreferClaimsValue() public void NestedActorTokenInClaimsDictionaryShouldBeProperlySerialized() { var context = new CompareContext($"{this}.NestedActorTokenInClaimsDictionaryShouldBeProperlySerialized"); + SecurityTokenDescriptor.ActorClaimTypeName = "actort"; try { @@ -263,6 +266,7 @@ public void NestedActorTokenInClaimsDictionaryShouldBeProperlySerialized() public void NestedActorTokenAsSubjectShouldBeProperlySerialized() { var context = new CompareContext($"{this}.NestedActorTokenAsSubjectShouldBeProperlySerialized"); + SecurityTokenDescriptor.ActorClaimTypeName = "actort"; try { @@ -334,6 +338,7 @@ public void MaxActorChainLength_RejectsNegativeValues() { // Arrange int originalValue = SecurityTokenDescriptor.MaxActorChainLength; + SecurityTokenDescriptor.ActorClaimTypeName = "actort"; try { @@ -344,7 +349,7 @@ public void MaxActorChainLength_RejectsNegativeValues() // Act & Assert - Negative value var ex = Assert.Throws(() => SecurityTokenDescriptor.MaxActorChainLength = -5); - Assert.Contains("MaxActorChainLength must be non negative", ex.Message); + Assert.Contains("IDX11027", ex.Message); // Act & Assert - Valid value 1 should not throw SecurityTokenDescriptor.MaxActorChainLength = 1; @@ -365,6 +370,7 @@ public void MaxActorChainLength_RejectsNegativeValues() public void NestedSubjectActorTokens_ExceedingMaxDepth_ThrowsException() { var context = new CompareContext($"{this}.NestedActorTokens_ExceedingMaxDepth_ThrowsException"); + SecurityTokenDescriptor.ActorClaimTypeName = "actort"; try { @@ -427,6 +433,7 @@ public void NestedSubjectActorTokens_ExceedingMaxDepth_ThrowsException() public void NestedClaimsDictionaryActorTokens_ExceedingMaxDepth_ThrowsException() { var context = new CompareContext($"{this}.NestedClaimsDictionaryActorTokens_ExceedingMaxDepth_ThrowsException"); + SecurityTokenDescriptor.ActorClaimTypeName = "actort"; try { @@ -488,6 +495,8 @@ public void NestedClaimsDictionaryActorTokens_ExceedingMaxDepth_ThrowsException( [Fact] public void ActorTokens_MixedSourceRespectMaxActorChainLength() { + SecurityTokenDescriptor.ActorClaimTypeName = "actort"; + // Arrange var handler = new JsonWebTokenHandler(); SecurityTokenDescriptor.MaxActorChainLength = 1; // Allow 1 levels of nesting @@ -548,6 +557,7 @@ public void ActorTokens_MixedSourceRespectMaxActorChainLength() public void NestedClaimTokens_ExceedingMaxDepth_ThrowsException() { var context = new CompareContext($"{this}.NestedActorTokens_ExceedingMaxDepth_ThrowsException"); + SecurityTokenDescriptor.ActorClaimTypeName = "actort"; try { From 787099b71643d8767dfb9b7c4b0f1f4e33c56765 Mon Sep 17 00:00:00 2001 From: saurabhsathe-ms Date: Thu, 8 May 2025 00:16:27 -0700 Subject: [PATCH 24/52] Updated JsonWebToken.cs to now use SecurityTokenDescriptor.ActorClaimNameType instead of JWTRegisteredClaimName --- src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs | 2 +- .../ActorClaimsTests.cs | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs index 887e20b55b..6b2ab557f6 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs @@ -1020,7 +1020,7 @@ public string Actor { get { - _act ??= Payload.GetStringValue(JwtRegisteredClaimNames.Actort); + _act ??= Payload.GetStringValue(SecurityTokenDescriptor.ActorClaimTypeName); return _act; } } diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs index dc1ced9d8d..648dab5dcd 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs @@ -17,7 +17,6 @@ public class ActorClaimsTests public void ActorTokenInClaimsDictionaryShouldBeProperlySerialized() { var context = new CompareContext($"{this}.ActorTokenInClaimsDictionaryShouldBeProperlySerialized"); - SecurityTokenDescriptor.ActorClaimTypeName = "actort"; try { @@ -43,7 +42,7 @@ public void ActorTokenInClaimsDictionaryShouldBeProperlySerialized() SigningCredentials = Default.AsymmetricSigningCredentials, Claims = new Dictionary { - { JwtRegisteredClaimNames.Actort, actorIdentity } + { SecurityTokenDescriptor.ActorClaimTypeName, actorIdentity } } }; @@ -51,7 +50,7 @@ public void ActorTokenInClaimsDictionaryShouldBeProperlySerialized() JsonWebToken decodedToken = tokenHandler.ReadJsonWebToken(token); // Verify actor claim exists in the token - Assert.True(decodedToken.Payload.HasClaim("actort"), "JWT token should contain 'actort' claim"); + Assert.True(decodedToken.Payload.HasClaim(SecurityTokenDescriptor.ActorClaimTypeName), "JWT token should contain 'actort' claim"); // Get the actor token and verify it contains the expected claims var actorTokenString = decodedToken.Actor; Assert.NotNull(actorTokenString); From a92b6d62bc5d4fd7264805f05929ad26c2a9da6d Mon Sep 17 00:00:00 2001 From: saurabhsathe-ms Date: Thu, 8 May 2025 19:49:24 -0700 Subject: [PATCH 25/52] Introduced a flag that we will be using to turn the feature on or off --- src/Microsoft.IdentityModel.Tokens/AppContextSwitches.cs | 9 +++++++++ .../InternalAPI.Unshipped.txt | 4 +++- .../SecurityTokenDescriptor.cs | 4 ++-- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.IdentityModel.Tokens/AppContextSwitches.cs b/src/Microsoft.IdentityModel.Tokens/AppContextSwitches.cs index 7f32a2fb85..9b506c5117 100644 --- a/src/Microsoft.IdentityModel.Tokens/AppContextSwitches.cs +++ b/src/Microsoft.IdentityModel.Tokens/AppContextSwitches.cs @@ -98,6 +98,12 @@ internal static class AppContextSwitches private static bool? _useCapitalizedXMLTypeAttr; internal static bool UseCapitalizedXMLTypeAttr => _useCapitalizedXMLTypeAttr ??= (AppContext.TryGetSwitch(UseCapitalizedXMLTypeAttrSwitch, out bool useCapitalizedXMLTypeAttr) && useCapitalizedXMLTypeAttr); + /// + /// Enable the legacy behavior of actor claims. The legacy behavior is to use "actort" as claim name and also not serialize actor claim. + /// + internal const string UseLegacyActorClaimBehaviorSwitch = "Switch.Microsoft.IdentityModel.UseLegacyActorClaimBehavior"; + private static bool? _useLegacyActorClaimBehavior; + internal static bool UseLegacyActorClaimBehavior => _useLegacyActorClaimBehavior ??= (AppContext.TryGetSwitch(UseLegacyActorClaimBehaviorSwitch, out bool UseLegacyActorClaimBehavior) && UseLegacyActorClaimBehavior); /// /// Used for testing to reset all switches to its default value. /// @@ -123,6 +129,9 @@ internal static void ResetAllSwitches() _useCapitalizedXMLTypeAttr = null; AppContext.SetSwitch(UseCapitalizedXMLTypeAttrSwitch, false); + + _useLegacyActorClaimBehavior = null; + AppContext.SetSwitch(UseLegacyActorClaimBehaviorSwitch, false); } } } diff --git a/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt index cda77fce85..404bf3e0cc 100644 --- a/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt @@ -1,3 +1,5 @@ const Microsoft.IdentityModel.Tokens.AppContextSwitches.UseCapitalizedXMLTypeAttrSwitch = "Switch.Microsoft.IdentityModel.UseCapitalizedXMLTypeAttr" -> string +const Microsoft.IdentityModel.Tokens.AppContextSwitches.UseLegacyActorClaimBehaviorSwitch = "Switch.Microsoft.IdentityModel.UseLegacyActorClaimBehavior" -> string const Microsoft.IdentityModel.Tokens.LogMessages.IDX11027 = "IDX11027: Invalid JsonWebToken handler configuration parameter value provided for {0}" -> string -static Microsoft.IdentityModel.Tokens.AppContextSwitches.UseCapitalizedXMLTypeAttr.get -> bool \ No newline at end of file +static Microsoft.IdentityModel.Tokens.AppContextSwitches.UseCapitalizedXMLTypeAttr.get -> bool +static Microsoft.IdentityModel.Tokens.AppContextSwitches.UseLegacyActorClaimBehavior.get -> bool \ No newline at end of file diff --git a/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs b/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs index 971872f088..ed9b5aec1f 100644 --- a/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs +++ b/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs @@ -159,13 +159,13 @@ public static string ActorClaimTypeName get => s_actoryClaimTypeName; set { - if (string.IsNullOrEmpty(value) || (!value.Equals("act") && !value.Equals("actort"))) + if (string.IsNullOrEmpty(value)) throw LogHelper.LogExceptionMessage( new ArgumentOutOfRangeException( LogHelper.FormatInvariant( LogMessages.IDX11027, LogHelper.MarkAsNonPII("ActorClaimTypeName")) - + ". Permissible values are 'act' or 'actort'.")); + + ". ActorClaimTypeName cannot be empty.")); s_actoryClaimTypeName = value; } From 36623eac3f37b9e3bea69fe9c0847578b36a6ca7 Mon Sep 17 00:00:00 2001 From: saurabhsathe-ms Date: Mon, 12 May 2025 10:29:57 -0700 Subject: [PATCH 26/52] Cleaned the code and brought it all under one function --- .../JsonWebTokenHandler.CreateToken.cs | 109 +++++++++--------- .../SecurityTokenDescriptor.cs | 2 +- 2 files changed, 56 insertions(+), 55 deletions(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs index bed4c0d5a9..59b6dc94bd 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs @@ -622,7 +622,6 @@ internal static void WriteJwsPayload( bool iatSet = false; bool descriptorClaimsNbfChecked = false; bool nbfSet = false; - bool isActorTokenSet = false; writer.WriteStartObject(); @@ -762,36 +761,9 @@ internal static void WriteJwsPayload( JsonPrimitives.WriteObject(ref writer, kvp.Key, kvp.Value); } - if (tokenDescriptor.Claims.ContainsKey(SecurityTokenDescriptor.ActorClaimTypeName)) - { - // Check for maximum actor chain depth - if (actorChainDepth >= SecurityTokenDescriptor.MaxActorChainLength) - { - throw LogHelper.LogExceptionMessage( - new SecurityTokenException( - LogHelper.FormatInvariant( - LogMessages.IDX14313, - LogHelper.MarkAsNonPII(SecurityTokenDescriptor.MaxActorChainLength)))); - } - if (isActorTokenSet) - { - if (LogHelper.IsEnabled(EventLogLevel.Informational)) - LogHelper.LogInformation(LogHelper.FormatInvariant(LogMessages.IDX14113, LogHelper.MarkAsNonPII(nameof(tokenDescriptor.Expires)))); - - } - ClaimsIdentity actor = tokenDescriptor.Claims[SecurityTokenDescriptor.ActorClaimTypeName] as ClaimsIdentity; - var actorTokenDescriptor = new SecurityTokenDescriptor - { - Subject = actor - }; - actorChainDepth = actorChainDepth + 1; - string actorToken = CreateToken(actorTokenDescriptor, false, 0, actorChainDepth); - JsonPrimitives.WriteObject(ref writer, SecurityTokenDescriptor.ActorClaimTypeName, actorToken); - isActorTokenSet = true; - } } - - AddSubjectClaims(ref writer, tokenDescriptor, audienceSet, issuerSet, ref expSet, ref iatSet, ref nbfSet, ref isActorTokenSet, actorChainDepth); + ProcessActorToken(writer, tokenDescriptor, actorChainDepth); + AddSubjectClaims(ref writer, tokenDescriptor, audienceSet, issuerSet, ref expSet, ref iatSet, ref nbfSet); // By default we set these three properties only if they haven't been detected before. if (setDefaultTimesOnTokenCreation && !(expSet && iatSet && nbfSet)) @@ -828,9 +800,7 @@ internal static void AddSubjectClaims( bool issuerSet, ref bool expSet, ref bool iatSet, - ref bool nbfSet, - ref bool isActorTokenSet, - int actorChainDepth = 0) + ref bool nbfSet) { if (tokenDescriptor.Subject == null) return; @@ -843,26 +813,6 @@ internal static void AddSubjectClaims( bool checkClaims = tokenDescriptor.Claims != null && tokenDescriptor.Claims.Count > 0; - if (!isActorTokenSet && tokenDescriptor.Subject.Actor != null) - { - if (actorChainDepth >= SecurityTokenDescriptor.MaxActorChainLength) - { - throw LogHelper.LogExceptionMessage( - new SecurityTokenException( - LogHelper.FormatInvariant( - LogMessages.IDX14313, - LogHelper.MarkAsNonPII(SecurityTokenDescriptor.MaxActorChainLength)))); - } - var actorTokenDescriptor = new SecurityTokenDescriptor - { - Subject = tokenDescriptor.Subject.Actor, - }; - - string actorToken = CreateToken(actorTokenDescriptor, false, 0, actorChainDepth + 1); - writer.WritePropertyName(SecurityTokenDescriptor.ActorClaimTypeName); - writer.WriteStringValue(actorToken); - isActorTokenSet = true; - } foreach (Claim claim in tokenDescriptor.Subject.Claims) { if (claim == null) @@ -1129,7 +1079,58 @@ internal static byte[] WriteJweHeader(SecurityTokenDescriptor tokenDescriptor) } } } - + private static void ProcessActorToken( + Utf8JsonWriter writer, + SecurityTokenDescriptor tokenDescriptor, + int actorChainDepth = 0) + { + if (tokenDescriptor == null) + { + throw new ArgumentNullException(nameof(tokenDescriptor)); + } + SecurityTokenDescriptor actorTokenDescriptor = null; + if (tokenDescriptor.Claims != null && tokenDescriptor.Claims.ContainsKey(SecurityTokenDescriptor.ActorClaimTypeName)) + { + // Check for maximum actor chain depth + if (actorChainDepth >= SecurityTokenDescriptor.MaxActorChainLength) + { + throw LogHelper.LogExceptionMessage( + new SecurityTokenException( + LogHelper.FormatInvariant( + LogMessages.IDX14313, + LogHelper.MarkAsNonPII(SecurityTokenDescriptor.MaxActorChainLength)))); + } + ClaimsIdentity actor = tokenDescriptor.Claims[SecurityTokenDescriptor.ActorClaimTypeName] as ClaimsIdentity; + actorTokenDescriptor = new SecurityTokenDescriptor + { + Subject = actor + }; + actorChainDepth = actorChainDepth + 1; + string actorToken = CreateToken(actorTokenDescriptor, false, 0, actorChainDepth); + JsonPrimitives.WriteObject(ref writer, SecurityTokenDescriptor.ActorClaimTypeName, actorToken); + } + else + { + if (tokenDescriptor.Subject != null && tokenDescriptor.Subject.Actor != null) + { + if (actorChainDepth >= SecurityTokenDescriptor.MaxActorChainLength) + { + throw LogHelper.LogExceptionMessage( + new SecurityTokenException( + LogHelper.FormatInvariant( + LogMessages.IDX14313, + LogHelper.MarkAsNonPII(SecurityTokenDescriptor.MaxActorChainLength)))); + } + actorTokenDescriptor = new SecurityTokenDescriptor + { + Subject = tokenDescriptor.Subject.Actor, + }; + string actorToken = CreateToken(actorTokenDescriptor, false, 0, actorChainDepth + 1); + writer.WritePropertyName(SecurityTokenDescriptor.ActorClaimTypeName); + writer.WriteStringValue(actorToken); + } + } + } internal static byte[] CompressToken(byte[] utf8Bytes, string compressionAlgorithm) { if (string.IsNullOrEmpty(compressionAlgorithm)) diff --git a/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs b/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs index ed9b5aec1f..fd1734fe17 100644 --- a/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs +++ b/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs @@ -143,7 +143,7 @@ public static int MaxActorChainLength } } - private static string s_actoryClaimTypeName = "act"; + private static string s_actoryClaimTypeName = "actort"; /// /// Gets or sets the claim type name for the actor claim. /// Permissible values are 'act' or 'actort'. From e3c25f90bc078ed1ba1eaa1e27f261a5745d3530 Mon Sep 17 00:00:00 2001 From: saurabhsathe-ms Date: Mon, 12 May 2025 20:02:23 -0700 Subject: [PATCH 27/52] Implemented non static version of ActorClaimName --- .../JsonWebToken.cs | 30 +- .../JsonWebTokenHandler.CreateToken.cs | 42 +-- .../LogMessages.cs | 2 +- .../AppContextSwitches.cs | 10 +- .../InternalAPI.Unshipped.txt | 6 +- .../PublicAPI/net462/PublicAPI.Unshipped.txt | 8 +- .../PublicAPI/net472/PublicAPI.Unshipped.txt | 8 +- .../PublicAPI/net6.0/PublicAPI.Unshipped.txt | 9 +- .../PublicAPI/net8.0/PublicAPI.Unshipped.txt | 8 +- .../PublicAPI/net9.0/PublicAPI.Unshipped.txt | 8 +- .../netstandard2.0/PublicAPI.Unshipped.txt | 8 +- .../SecurityTokenDescriptor.cs | 17 +- .../ActorClaimsTests.cs | 258 +++++++++++------- 13 files changed, 254 insertions(+), 160 deletions(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs index 6b2ab557f6..5fb6e83752 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs @@ -57,6 +57,34 @@ public partial class JsonWebToken : SecurityToken internal DateTime? _nbfDateTime; internal DateTime? _validFrom; internal DateTime? _validTo; + + private static string s_actorClaimName = "act"; + /// + /// Gets or sets the claim type name for the actor claim. + /// Permissible values are 'act' or 'actort'. + /// + /// + /// Thrown if the value is null. + /// + /// + /// Thrown if the value is not 'act' or 'actort'. + /// + internal static string ActorClaimName + { + get => AppContextSwitches.SerializeDeserializeActorClaim ? s_actorClaimName : "actort"; + set + { + if (string.IsNullOrEmpty(value)) + throw LogHelper.LogExceptionMessage( + new ArgumentNullException( + LogHelper.FormatInvariant( + LogMessages.IDX14313, + LogHelper.MarkAsNonPII("ActorClaimName")) + + ". ActorClaimName cannot be empty.")); + + s_actorClaimName = value; + } + } #endregion /// @@ -1020,7 +1048,7 @@ public string Actor { get { - _act ??= Payload.GetStringValue(SecurityTokenDescriptor.ActorClaimTypeName); + _act ??= Payload.GetStringValue(ActorClaimName); return _act; } } diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs index 59b6dc94bd..f0ec1c9f94 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs @@ -677,8 +677,12 @@ internal static void WriteJwsPayload( { foreach (KeyValuePair kvp in tokenDescriptor.Claims) { - if (kvp.Key.Equals(SecurityTokenDescriptor.ActorClaimTypeName, StringComparison.Ordinal)) + Console.WriteLine(kvp.Key); + AppContext.TryGetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, out bool isnebaled); + Console.WriteLine($"Equal?{tokenDescriptor.ActorClaimName}, {isnebaled}"); + if (kvp.Key.Equals(tokenDescriptor.ActorClaimName, StringComparison.Ordinal)) { + Console.WriteLine(kvp.Value); continue; } if (!descriptorClaimsAudienceChecked && kvp.Key.Equals(JwtRegisteredClaimNames.Aud, StringComparison.Ordinal)) @@ -758,11 +762,12 @@ internal static void WriteJwsPayload( nbfSet = true; } - JsonPrimitives.WriteObject(ref writer, kvp.Key, kvp.Value); } } - ProcessActorToken(writer, tokenDescriptor, actorChainDepth); + if (AppContextSwitches.SerializeDeserializeActorClaim) + WriteActorToken(writer, tokenDescriptor, actorChainDepth); + AddSubjectClaims(ref writer, tokenDescriptor, audienceSet, issuerSet, ref expSet, ref iatSet, ref nbfSet); // By default we set these three properties only if they haven't been detected before. @@ -1079,7 +1084,7 @@ internal static byte[] WriteJweHeader(SecurityTokenDescriptor tokenDescriptor) } } } - private static void ProcessActorToken( + private static void WriteActorToken( Utf8JsonWriter writer, SecurityTokenDescriptor tokenDescriptor, int actorChainDepth = 0) @@ -1089,47 +1094,50 @@ private static void ProcessActorToken( throw new ArgumentNullException(nameof(tokenDescriptor)); } SecurityTokenDescriptor actorTokenDescriptor = null; - if (tokenDescriptor.Claims != null && tokenDescriptor.Claims.ContainsKey(SecurityTokenDescriptor.ActorClaimTypeName)) + if (tokenDescriptor.Claims != null && tokenDescriptor.Claims.ContainsKey(tokenDescriptor.ActorClaimName)) { // Check for maximum actor chain depth - if (actorChainDepth >= SecurityTokenDescriptor.MaxActorChainLength) + if (actorChainDepth >= tokenDescriptor.MaxActorChainLength) { throw LogHelper.LogExceptionMessage( new SecurityTokenException( LogHelper.FormatInvariant( LogMessages.IDX14313, - LogHelper.MarkAsNonPII(SecurityTokenDescriptor.MaxActorChainLength)))); + LogHelper.MarkAsNonPII(tokenDescriptor.MaxActorChainLength)))); } - ClaimsIdentity actor = tokenDescriptor.Claims[SecurityTokenDescriptor.ActorClaimTypeName] as ClaimsIdentity; + ClaimsIdentity actor = tokenDescriptor.Claims[tokenDescriptor.ActorClaimName] as ClaimsIdentity; actorTokenDescriptor = new SecurityTokenDescriptor { - Subject = actor + Subject = actor, }; - actorChainDepth = actorChainDepth + 1; - string actorToken = CreateToken(actorTokenDescriptor, false, 0, actorChainDepth); - JsonPrimitives.WriteObject(ref writer, SecurityTokenDescriptor.ActorClaimTypeName, actorToken); } else { if (tokenDescriptor.Subject != null && tokenDescriptor.Subject.Actor != null) { - if (actorChainDepth >= SecurityTokenDescriptor.MaxActorChainLength) + if (actorChainDepth >= tokenDescriptor.MaxActorChainLength) { throw LogHelper.LogExceptionMessage( new SecurityTokenException( LogHelper.FormatInvariant( LogMessages.IDX14313, - LogHelper.MarkAsNonPII(SecurityTokenDescriptor.MaxActorChainLength)))); + LogHelper.MarkAsNonPII(tokenDescriptor.MaxActorChainLength)))); } actorTokenDescriptor = new SecurityTokenDescriptor { Subject = tokenDescriptor.Subject.Actor, }; - string actorToken = CreateToken(actorTokenDescriptor, false, 0, actorChainDepth + 1); - writer.WritePropertyName(SecurityTokenDescriptor.ActorClaimTypeName); - writer.WriteStringValue(actorToken); } } + + if (actorTokenDescriptor != null) + { + actorTokenDescriptor.MaxActorChainLength = tokenDescriptor.MaxActorChainLength; + actorTokenDescriptor.ActorClaimName = tokenDescriptor.ActorClaimName; + string actorToken = CreateToken(actorTokenDescriptor, false, 0, actorChainDepth + 1); + writer.WritePropertyName(tokenDescriptor.ActorClaimName); + writer.WriteStringValue(actorToken); + } } internal static byte[] CompressToken(byte[] utf8Bytes, string compressionAlgorithm) { diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/LogMessages.cs b/src/Microsoft.IdentityModel.JsonWebTokens/LogMessages.cs index b3d4fd6683..3db53dc759 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/LogMessages.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/LogMessages.cs @@ -51,6 +51,6 @@ internal static class LogMessages internal const string IDX14310 = "IDX14310: JWE authentication tag is missing."; internal const string IDX14311 = "IDX14311: Unable to decode the authentication tag as a Base64Url encoded string."; internal const string IDX14312 = "IDX14312: Unable to decode the cipher text as a Base64Url encoded string."; - internal const string IDX14313 = "IDX14313: Unable to serialize actor token. Actor token chain exceeded maximum depth of {0}"; + internal const string IDX14313 = "IDX14313: Unable to set actor token claim name. Actor token claim name cannot be null or empty"; } } diff --git a/src/Microsoft.IdentityModel.Tokens/AppContextSwitches.cs b/src/Microsoft.IdentityModel.Tokens/AppContextSwitches.cs index 9b506c5117..a6634d5af3 100644 --- a/src/Microsoft.IdentityModel.Tokens/AppContextSwitches.cs +++ b/src/Microsoft.IdentityModel.Tokens/AppContextSwitches.cs @@ -101,9 +101,9 @@ internal static class AppContextSwitches /// /// Enable the legacy behavior of actor claims. The legacy behavior is to use "actort" as claim name and also not serialize actor claim. /// - internal const string UseLegacyActorClaimBehaviorSwitch = "Switch.Microsoft.IdentityModel.UseLegacyActorClaimBehavior"; - private static bool? _useLegacyActorClaimBehavior; - internal static bool UseLegacyActorClaimBehavior => _useLegacyActorClaimBehavior ??= (AppContext.TryGetSwitch(UseLegacyActorClaimBehaviorSwitch, out bool UseLegacyActorClaimBehavior) && UseLegacyActorClaimBehavior); + internal const string SerializeDeserializeActorClaimSwitch = "Switch.Microsoft.IdentityModel.SerializeDeserializeActorClaim"; + private static bool? _serializeDeserializeActorClaim; + internal static bool SerializeDeserializeActorClaim => _serializeDeserializeActorClaim ??= (AppContext.TryGetSwitch(SerializeDeserializeActorClaimSwitch, out bool SerializeDeserializeActorClaim) && SerializeDeserializeActorClaim); /// /// Used for testing to reset all switches to its default value. /// @@ -130,8 +130,8 @@ internal static void ResetAllSwitches() _useCapitalizedXMLTypeAttr = null; AppContext.SetSwitch(UseCapitalizedXMLTypeAttrSwitch, false); - _useLegacyActorClaimBehavior = null; - AppContext.SetSwitch(UseLegacyActorClaimBehaviorSwitch, false); + _serializeDeserializeActorClaim = null; + AppContext.SetSwitch(SerializeDeserializeActorClaimSwitch, false); } } } diff --git a/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt index 404bf3e0cc..0ab5d08a66 100644 --- a/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt @@ -1,5 +1,5 @@ +const Microsoft.IdentityModel.Tokens.AppContextSwitches.SerializeDeserializeActorClaimSwitch = "Switch.Microsoft.IdentityModel.SerializeDeserializeActorClaim" -> string const Microsoft.IdentityModel.Tokens.AppContextSwitches.UseCapitalizedXMLTypeAttrSwitch = "Switch.Microsoft.IdentityModel.UseCapitalizedXMLTypeAttr" -> string -const Microsoft.IdentityModel.Tokens.AppContextSwitches.UseLegacyActorClaimBehaviorSwitch = "Switch.Microsoft.IdentityModel.UseLegacyActorClaimBehavior" -> string const Microsoft.IdentityModel.Tokens.LogMessages.IDX11027 = "IDX11027: Invalid JsonWebToken handler configuration parameter value provided for {0}" -> string -static Microsoft.IdentityModel.Tokens.AppContextSwitches.UseCapitalizedXMLTypeAttr.get -> bool -static Microsoft.IdentityModel.Tokens.AppContextSwitches.UseLegacyActorClaimBehavior.get -> bool \ No newline at end of file +static Microsoft.IdentityModel.Tokens.AppContextSwitches.SerializeDeserializeActorClaim.get -> bool +static Microsoft.IdentityModel.Tokens.AppContextSwitches.UseCapitalizedXMLTypeAttr.get -> bool \ No newline at end of file diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net462/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net462/PublicAPI.Unshipped.txt index 90f0a238ef..0eaa8f883e 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net462/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net462/PublicAPI.Unshipped.txt @@ -1,4 +1,4 @@ -static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimTypeName.get -> string -static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimTypeName.set -> void -static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.get -> int -static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.set -> void \ No newline at end of file +Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimName.get -> string +Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimName.set -> void +Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.get -> int +Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.set -> void diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net472/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net472/PublicAPI.Unshipped.txt index 2f44a2bbe9..0eaa8f883e 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net472/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net472/PublicAPI.Unshipped.txt @@ -1,4 +1,4 @@ -static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimTypeName.get -> string -static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimTypeName.set -> void -static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.get -> int -static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.set -> void +Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimName.get -> string +Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimName.set -> void +Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.get -> int +Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.set -> void diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net6.0/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net6.0/PublicAPI.Unshipped.txt index 2f44a2bbe9..30d724e156 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net6.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net6.0/PublicAPI.Unshipped.txt @@ -1,4 +1,5 @@ -static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimTypeName.get -> string -static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimTypeName.set -> void -static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.get -> int -static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.set -> void +Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimName.get -> string +Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimName.set -> void +Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.get -> int +Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.set -> void + diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net8.0/PublicAPI.Unshipped.txt index 2f44a2bbe9..0eaa8f883e 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net8.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net8.0/PublicAPI.Unshipped.txt @@ -1,4 +1,4 @@ -static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimTypeName.get -> string -static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimTypeName.set -> void -static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.get -> int -static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.set -> void +Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimName.get -> string +Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimName.set -> void +Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.get -> int +Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.set -> void diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net9.0/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net9.0/PublicAPI.Unshipped.txt index 2f44a2bbe9..0eaa8f883e 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net9.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net9.0/PublicAPI.Unshipped.txt @@ -1,4 +1,4 @@ -static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimTypeName.get -> string -static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimTypeName.set -> void -static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.get -> int -static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.set -> void +Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimName.get -> string +Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimName.set -> void +Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.get -> int +Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.set -> void diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt index 2f44a2bbe9..0eaa8f883e 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt @@ -1,4 +1,4 @@ -static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimTypeName.get -> string -static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimTypeName.set -> void -static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.get -> int -static Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.set -> void +Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimName.get -> string +Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimName.set -> void +Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.get -> int +Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.set -> void diff --git a/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs b/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs index fd1734fe17..74f7b20f24 100644 --- a/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs +++ b/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs @@ -118,7 +118,7 @@ public class SecurityTokenDescriptor [DefaultValue(true)] public bool IncludeKeyIdInHeader { get; set; } = true; - private static int s_maxActorChainLength = 5; + private int s_maxActorChainLength = 5; /// /// Gets or sets the maximum depth allowed when processing nested actor tokens. /// This prevents excessive recursion when handling deeply nested actor tokens. @@ -126,7 +126,7 @@ public class SecurityTokenDescriptor /// Default value is 5. Max value is also 5 /// /// Thrown if the value is less than 0. - public static int MaxActorChainLength + public int MaxActorChainLength { get => s_maxActorChainLength; set @@ -143,7 +143,7 @@ public static int MaxActorChainLength } } - private static string s_actoryClaimTypeName = "actort"; + private string s_actorClaimName = "act"; /// /// Gets or sets the claim type name for the actor claim. /// Permissible values are 'act' or 'actort'. @@ -154,9 +154,9 @@ public static int MaxActorChainLength /// /// Thrown if the value is not 'act' or 'actort'. /// - public static string ActorClaimTypeName + public string ActorClaimName { - get => s_actoryClaimTypeName; + get => AppContextSwitches.SerializeDeserializeActorClaim ? s_actorClaimName : "actort"; set { if (string.IsNullOrEmpty(value)) @@ -164,10 +164,9 @@ public static string ActorClaimTypeName new ArgumentOutOfRangeException( LogHelper.FormatInvariant( LogMessages.IDX11027, - LogHelper.MarkAsNonPII("ActorClaimTypeName")) - + ". ActorClaimTypeName cannot be empty.")); - - s_actoryClaimTypeName = value; + LogHelper.MarkAsNonPII("ActorClaimName")) + + ". ActorClaimName cannot be empty.")); + s_actorClaimName = value; } } } diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs index 648dab5dcd..f0329f2e15 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs @@ -17,7 +17,9 @@ public class ActorClaimsTests public void ActorTokenInClaimsDictionaryShouldBeProperlySerialized() { var context = new CompareContext($"{this}.ActorTokenInClaimsDictionaryShouldBeProperlySerialized"); - + bool switchValue = false; + AppContext.TryGetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, out switchValue); + AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, true); try { // Create a ClaimsIdentity for the actor @@ -42,15 +44,14 @@ public void ActorTokenInClaimsDictionaryShouldBeProperlySerialized() SigningCredentials = Default.AsymmetricSigningCredentials, Claims = new Dictionary { - { SecurityTokenDescriptor.ActorClaimTypeName, actorIdentity } + { "act", actorIdentity } } }; - var token = tokenHandler.CreateToken(tokenDescriptor); JsonWebToken decodedToken = tokenHandler.ReadJsonWebToken(token); // Verify actor claim exists in the token - Assert.True(decodedToken.Payload.HasClaim(SecurityTokenDescriptor.ActorClaimTypeName), "JWT token should contain 'actort' claim"); + Assert.True(decodedToken.Payload.HasClaim(tokenDescriptor.ActorClaimName), "JWT token should contain 'actort' claim"); // Get the actor token and verify it contains the expected claims var actorTokenString = decodedToken.Actor; Assert.NotNull(actorTokenString); @@ -64,6 +65,10 @@ public void ActorTokenInClaimsDictionaryShouldBeProperlySerialized() { context.Diffs.Add($"Exception: {ex}"); } + finally + { + AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, switchValue); + } TestUtilities.AssertFailIfErrors(context); } @@ -72,8 +77,9 @@ public void ActorTokenInClaimsDictionaryShouldBeProperlySerialized() public void ActorTokenAsSubjectShouldBeProperlySerialized() { var context = new CompareContext($"{this}.ActorTokenAsSubjectShouldBeProperlySerialized"); - SecurityTokenDescriptor.ActorClaimTypeName = "actort"; - + bool switchValue = false; + AppContext.TryGetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, out switchValue); + AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, true); try { // Create actor identity @@ -98,12 +104,11 @@ public void ActorTokenAsSubjectShouldBeProperlySerialized() Expires = DateTime.UtcNow.AddHours(1), SigningCredentials = Default.AsymmetricSigningCredentials }; - var token = tokenHandler.CreateToken(tokenDescriptor); JsonWebToken decodedToken = tokenHandler.ReadJsonWebToken(token); // Verify actor claim exists in the token - Assert.True(decodedToken.Payload.HasClaim("actort"), "JWT token should contain 'actort' claim"); + Assert.True(decodedToken.Payload.HasClaim("act"), "JWT token should contain 'act' claim"); // Get the actor token and verify it contains the expected claims var actorTokenString = decodedToken.Actor; @@ -118,6 +123,10 @@ public void ActorTokenAsSubjectShouldBeProperlySerialized() { context.Diffs.Add($"Exception: {ex}"); } + finally + { + AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, switchValue); + } TestUtilities.AssertFailIfErrors(context); } @@ -126,8 +135,9 @@ public void ActorTokenAsSubjectShouldBeProperlySerialized() public void ActorTokenInBothClaimsAndSubjectShouldPreferClaimsValue() { var context = new CompareContext($"{this}.ActorTokenInBothClaimsAndSubjectShouldPreferClaimsValue"); - SecurityTokenDescriptor.ActorClaimTypeName = "actort"; - + bool switchValue = false; + AppContext.TryGetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, out switchValue); + AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, true); try { // Create actor identity for Subject.Actor (should be ignored) @@ -158,15 +168,16 @@ public void ActorTokenInBothClaimsAndSubjectShouldPreferClaimsValue() // Add Claims actor that should take precedence Claims = new Dictionary { - { JwtRegisteredClaimNames.Actort, claimsActorIdentity } + { "act", claimsActorIdentity } } }; + tokenDescriptor.ActorClaimName = "act"; // Set the actor claim name to "act" for testing var token = tokenHandler.CreateToken(tokenDescriptor); JsonWebToken decodedToken = tokenHandler.ReadJsonWebToken(token); // Verify actor claim exists - Assert.True(decodedToken.Payload.HasClaim("actort"), "JWT token should contain 'actort' claim"); + Assert.True(decodedToken.Payload.HasClaim("act"), "JWT token should contain 'actort' claim"); // Get the actor token and verify it contains the expected claims var actorTokenString = decodedToken.Actor; @@ -183,6 +194,10 @@ public void ActorTokenInBothClaimsAndSubjectShouldPreferClaimsValue() { context.Diffs.Add($"Exception: {ex}"); } + finally + { + AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, switchValue); + } TestUtilities.AssertFailIfErrors(context); } @@ -191,8 +206,9 @@ public void ActorTokenInBothClaimsAndSubjectShouldPreferClaimsValue() public void NestedActorTokenInClaimsDictionaryShouldBeProperlySerialized() { var context = new CompareContext($"{this}.NestedActorTokenInClaimsDictionaryShouldBeProperlySerialized"); - SecurityTokenDescriptor.ActorClaimTypeName = "actort"; - + bool switchValue = false; + AppContext.TryGetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, out switchValue); + AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, true); try { // Create nested actor identity @@ -222,15 +238,14 @@ public void NestedActorTokenInClaimsDictionaryShouldBeProperlySerialized() SigningCredentials = Default.AsymmetricSigningCredentials, Claims = new Dictionary { - { JwtRegisteredClaimNames.Actort, actorIdentity } + { "act", actorIdentity } } }; - var token = tokenHandler.CreateToken(tokenDescriptor); JsonWebToken decodedToken = tokenHandler.ReadJsonWebToken(token); // Verify actor claim exists - Assert.True(decodedToken.Payload.HasClaim("actort"), "JWT token should contain 'actort' claim"); + Assert.True(decodedToken.Payload.HasClaim("act"), "JWT token should contain 'actort' claim"); // Read the main actor token var actorTokenString = decodedToken.Actor; @@ -242,7 +257,7 @@ public void NestedActorTokenInClaimsDictionaryShouldBeProperlySerialized() Assert.Equal("Actor Name", actorJwt.Payload.GetValue("name")); // Verify nested actor exists - Assert.True(actorJwt.Payload.HasClaim("actort"), "Actor token should contain nested 'actort' claim"); + Assert.True(actorJwt.Payload.HasClaim("act"), "Actor token should contain nested 'actort' claim"); // Read the nested actor token var nestedActorTokenString = actorJwt.Actor; @@ -257,6 +272,10 @@ public void NestedActorTokenInClaimsDictionaryShouldBeProperlySerialized() { context.Diffs.Add($"Exception: {ex}"); } + finally + { + AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, switchValue); + } TestUtilities.AssertFailIfErrors(context); } @@ -265,8 +284,8 @@ public void NestedActorTokenInClaimsDictionaryShouldBeProperlySerialized() public void NestedActorTokenAsSubjectShouldBeProperlySerialized() { var context = new CompareContext($"{this}.NestedActorTokenAsSubjectShouldBeProperlySerialized"); - SecurityTokenDescriptor.ActorClaimTypeName = "actort"; - + bool switchValue = false; + AppContext.TryGetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, out switchValue); try { // Create nested actor @@ -296,12 +315,13 @@ public void NestedActorTokenAsSubjectShouldBeProperlySerialized() Expires = DateTime.UtcNow.AddHours(1), SigningCredentials = Default.AsymmetricSigningCredentials }; - + AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, true); + tokenDescriptor.ActorClaimName = "act"; // Set the actor claim name to "act" for testing var token = tokenHandler.CreateToken(tokenDescriptor); JsonWebToken decodedToken = tokenHandler.ReadJsonWebToken(token); // Verify actor claim exists - Assert.True(decodedToken.Payload.HasClaim("actort"), "JWT token should contain 'actort' claim"); + Assert.True(decodedToken.Payload.HasClaim("act"), "JWT token should contain 'actort' claim"); // Read the main actor token var actorTokenString = decodedToken.Actor; @@ -313,7 +333,7 @@ public void NestedActorTokenAsSubjectShouldBeProperlySerialized() Assert.Equal("Actor Name", actorJwt.Payload.GetValue("name")); // Verify nested actor exists - Assert.True(actorJwt.Payload.HasClaim("actort"), "Actor token should contain nested 'actort' claim"); + Assert.True(actorJwt.Payload.HasClaim("act"), "Actor token should contain nested 'actort' claim"); // Read the nested actor token var nestedActorTokenString = actorJwt.Actor; @@ -328,40 +348,55 @@ public void NestedActorTokenAsSubjectShouldBeProperlySerialized() { context.Diffs.Add($"Exception: {ex}"); } - + finally + { + AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, switchValue); + } TestUtilities.AssertFailIfErrors(context); } [Fact] public void MaxActorChainLength_RejectsNegativeValues() { - // Arrange - int originalValue = SecurityTokenDescriptor.MaxActorChainLength; - SecurityTokenDescriptor.ActorClaimTypeName = "actort"; + bool switchValue = false; + AppContext.TryGetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, out switchValue); + // Arrange + SecurityTokenDescriptor tokenDescriptor = new SecurityTokenDescriptor + { + Subject = null, + Issuer = "https://example.com", + Audience = "https://api.example.com", + SigningCredentials = Default.AsymmetricSigningCredentials + }; + AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, true); + tokenDescriptor.ActorClaimName = "act"; // Set the actor claim name to "act" for testing + int originalValue = tokenDescriptor.MaxActorChainLength; try { + tokenDescriptor.ActorClaimName = "act"; // Set the actor claim name to "act" for testing // Act & Assert - Valid value 0 should not throw - SecurityTokenDescriptor.MaxActorChainLength = 0; - Assert.Equal(0, SecurityTokenDescriptor.MaxActorChainLength); + tokenDescriptor.MaxActorChainLength = 0; + Assert.Equal(0, tokenDescriptor.MaxActorChainLength); // Act & Assert - Negative value var ex = Assert.Throws(() => - SecurityTokenDescriptor.MaxActorChainLength = -5); + tokenDescriptor.MaxActorChainLength = -5); Assert.Contains("IDX11027", ex.Message); // Act & Assert - Valid value 1 should not throw - SecurityTokenDescriptor.MaxActorChainLength = 1; - Assert.Equal(1, SecurityTokenDescriptor.MaxActorChainLength); + tokenDescriptor.MaxActorChainLength = 1; + Assert.Equal(1, tokenDescriptor.MaxActorChainLength); // Act & Assert - Valid larger value - SecurityTokenDescriptor.MaxActorChainLength = 10; - Assert.Equal(10, SecurityTokenDescriptor.MaxActorChainLength); + tokenDescriptor.MaxActorChainLength = 10; + Assert.Equal(10, tokenDescriptor.MaxActorChainLength); } finally { // Restore to original value - SecurityTokenDescriptor.MaxActorChainLength = originalValue; + tokenDescriptor.MaxActorChainLength = originalValue; + AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, switchValue); } } @@ -369,13 +404,13 @@ public void MaxActorChainLength_RejectsNegativeValues() public void NestedSubjectActorTokens_ExceedingMaxDepth_ThrowsException() { var context = new CompareContext($"{this}.NestedActorTokens_ExceedingMaxDepth_ThrowsException"); - SecurityTokenDescriptor.ActorClaimTypeName = "actort"; + bool switchValue = false; + AppContext.TryGetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, out switchValue); try { // Arrange var handler = new JsonWebTokenHandler(); - SecurityTokenDescriptor.MaxActorChainLength = 2; // Allow only 2 levels of nesting // Create nested actor identities (3 levels, but we'll set MaxActorChainLength to 2) var level3Actor = new CaseSensitiveClaimsIdentity("Level3Auth"); @@ -406,6 +441,9 @@ public void NestedSubjectActorTokens_ExceedingMaxDepth_ThrowsException() Audience = "https://api.example.com", SigningCredentials = Default.AsymmetricSigningCredentials }; + AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, true); + tokenDescriptor.ActorClaimName = "act"; // Set the actor claim name to "act" for testing + tokenDescriptor.MaxActorChainLength = 2; // Allow only 2 levels of nesting // Act - This should throw a SecurityTokenException var token = handler.CreateToken(tokenDescriptor); @@ -424,6 +462,10 @@ public void NestedSubjectActorTokens_ExceedingMaxDepth_ThrowsException() // Unexpected exception type context.Diffs.Add($"Unexpected exception type: {ex.GetType()}, Message: {ex.Message}"); } + finally + { + AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, switchValue); + } TestUtilities.AssertFailIfErrors(context); } @@ -432,14 +474,14 @@ public void NestedSubjectActorTokens_ExceedingMaxDepth_ThrowsException() public void NestedClaimsDictionaryActorTokens_ExceedingMaxDepth_ThrowsException() { var context = new CompareContext($"{this}.NestedClaimsDictionaryActorTokens_ExceedingMaxDepth_ThrowsException"); - SecurityTokenDescriptor.ActorClaimTypeName = "actort"; + bool switchValue = false; + AppContext.TryGetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, out switchValue); try { // Arrange var handler = new JsonWebTokenHandler(); - SecurityTokenDescriptor.MaxActorChainLength = 1; // Allow only 1 level of nesting - + string actorname = "act"; // Create nested actor identities var nestedActorIdentity = new CaseSensitiveClaimsIdentity("NestedActorAuth"); nestedActorIdentity.AddClaim(new Claim("sub", "nested-actor-id")); @@ -465,14 +507,15 @@ public void NestedClaimsDictionaryActorTokens_ExceedingMaxDepth_ThrowsException( SigningCredentials = Default.AsymmetricSigningCredentials, Claims = new Dictionary { - { JwtRegisteredClaimNames.Actort, actorIdentity } + { actorname, actorIdentity } } }; - + AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, true); + tokenDescriptor.ActorClaimName = actorname; // Set the actor claim name to "act" for testing + tokenDescriptor.MaxActorChainLength = 1; // Allow only 1 level of nesting // Act var token = handler.CreateToken(tokenDescriptor); context.Diffs.Add("Expected exception was not thrown."); - } catch (SecurityTokenException ex) { @@ -487,82 +530,91 @@ public void NestedClaimsDictionaryActorTokens_ExceedingMaxDepth_ThrowsException( // Unexpected exception type context.Diffs.Add($"Unexpected exception type: {ex.GetType()}, Message: {ex.Message}"); } - + finally + { + AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, switchValue); + } TestUtilities.AssertFailIfErrors(context); } [Fact] public void ActorTokens_MixedSourceRespectMaxActorChainLength() { - SecurityTokenDescriptor.ActorClaimTypeName = "actort"; + bool switchValue = false; + AppContext.TryGetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, out switchValue); + AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, true); + try + { + // Arrange + var handler = new JsonWebTokenHandler(); + string actorname = "act"; + // Create level 2 actor (will be in claims dictionary) + var level2Actor = new CaseSensitiveClaimsIdentity("Level2Auth"); + level2Actor.AddClaim(new Claim("sub", "level2-actor")); + level2Actor.AddClaim(new Claim("name", "Level 2 Actor")); - // Arrange - var handler = new JsonWebTokenHandler(); - SecurityTokenDescriptor.MaxActorChainLength = 1; // Allow 1 levels of nesting - - // Create level 2 actor (will be in claims dictionary) - var level2Actor = new CaseSensitiveClaimsIdentity("Level2Auth"); - level2Actor.AddClaim(new Claim("sub", "level2-actor")); - level2Actor.AddClaim(new Claim("name", "Level 2 Actor")); - - // Create nested actors that should be truncated - var level3Actor = new CaseSensitiveClaimsIdentity("Level3Auth"); - level3Actor.AddClaim(new Claim("sub", "level3-actor")); - level3Actor.AddClaim(new Claim("name", "Level 3 Actor")); - - // Create level 1 actor with nested actor - var level1Actor = new CaseSensitiveClaimsIdentity("Level1Auth"); - level1Actor.AddClaim(new Claim("sub", "level1-actor")); - level1Actor.AddClaim(new Claim("name", "Level 1 Actor")); - level1Actor.Actor = level3Actor; // This should be ignored due to MaxActorChainLength - - // Create the main identity - var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); - mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); - mainIdentity.AddClaim(new Claim("name", "Main User")); - mainIdentity.Actor = level1Actor; - - // Create a token with additional actor in Claims dictionary - var tokenDescriptor = new SecurityTokenDescriptor - { - Subject = mainIdentity, - Issuer = "https://example.com", - Audience = "https://api.example.com", - SigningCredentials = Default.AsymmetricSigningCredentials, - // Add level 2 actor in claims dictionary to replace level 1's actor - Claims = new Dictionary - { - { JwtRegisteredClaimNames.Actort, level2Actor } - } - }; + // Create nested actors that should be truncated + var level3Actor = new CaseSensitiveClaimsIdentity("Level3Auth"); + level3Actor.AddClaim(new Claim("sub", "level3-actor")); + level3Actor.AddClaim(new Claim("name", "Level 3 Actor")); - // Act - var token = handler.CreateToken(tokenDescriptor); - var jwtToken = handler.ReadJsonWebToken(token); + // Create level 1 actor with nested actor + var level1Actor = new CaseSensitiveClaimsIdentity("Level1Auth"); + level1Actor.AddClaim(new Claim("sub", "level1-actor")); + level1Actor.AddClaim(new Claim("name", "Level 1 Actor")); + level1Actor.Actor = level3Actor; // This should be ignored due to MaxActorChainLength - // Assert - Assert.True(jwtToken.Payload.HasClaim("actort"), "JWT token should contain 'actort' claim"); + // Create the main identity + var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); + mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); + mainIdentity.AddClaim(new Claim("name", "Main User")); + mainIdentity.Actor = level1Actor; - // Verify we get the actor from Claims dictionary (should be level2Actor) - var actorToken = handler.ReadJsonWebToken(jwtToken.Actor); - Assert.Equal("level2-actor", actorToken.Payload.GetValue("sub")); - Assert.Equal("Level 2 Actor", actorToken.Payload.GetValue("name")); + // Create a token with additional actor in Claims dictionary + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = mainIdentity, + Issuer = "https://example.com", + Audience = "https://api.example.com", + SigningCredentials = Default.AsymmetricSigningCredentials, + // Add level 2 actor in claims dictionary to replace level 1's actor + Claims = new Dictionary + { + { actorname, level2Actor } + } + }; + tokenDescriptor.ActorClaimName = actorname; + tokenDescriptor.MaxActorChainLength = 1; + var token = handler.CreateToken(tokenDescriptor); + var jwtToken = handler.ReadJsonWebToken(token); - // There should be no nested actor because we're already at max depth - Assert.False(actorToken.Payload.HasClaim("actort"), "There should be no nested actor claim due to MaxActorChainLength"); - } + // Assert + Assert.True(jwtToken.Payload.HasClaim(actorname), "JWT token should contain 'actort' claim"); + // Verify we get the actor from Claims dictionary (should be level2Actor) + var actorToken = handler.ReadJsonWebToken(jwtToken.Actor); + Assert.Equal("level2-actor", actorToken.Payload.GetValue("sub")); + Assert.Equal("Level 2 Actor", actorToken.Payload.GetValue("name")); + + // There should be no nested actor because we're already at max depth + Assert.False(actorToken.Payload.HasClaim("actort"), "There should be no nested actor claim due to MaxActorChainLength"); + } + finally + { + AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, switchValue); + } + } [Fact] public void NestedClaimTokens_ExceedingMaxDepth_ThrowsException() { var context = new CompareContext($"{this}.NestedActorTokens_ExceedingMaxDepth_ThrowsException"); - SecurityTokenDescriptor.ActorClaimTypeName = "actort"; + bool switchValue = false; + AppContext.TryGetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, out switchValue); try { // Arrange var handler = new JsonWebTokenHandler(); - SecurityTokenDescriptor.MaxActorChainLength = 2; // Allow only 2 levels of nesting // Create nested actor identities (3 levels, but we'll set MaxActorChainLength to 2) var level3Actor = new CaseSensitiveClaimsIdentity("Level3Auth"); @@ -592,9 +644,12 @@ public void NestedClaimTokens_ExceedingMaxDepth_ThrowsException() SigningCredentials = Default.AsymmetricSigningCredentials, Claims = new Dictionary { - { JwtRegisteredClaimNames.Actort, level1Actor } + { "act", level1Actor } } }; + AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, true); + tokenDescriptor.ActorClaimName = "act"; // Set the actor claim name to "act" for testing + tokenDescriptor.MaxActorChainLength = 2; // Allow only 2 levels of nesting // Act - This should throw a SecurityTokenException var token = handler.CreateToken(tokenDescriptor); @@ -613,7 +668,10 @@ public void NestedClaimTokens_ExceedingMaxDepth_ThrowsException() // Unexpected exception type context.Diffs.Add($"Unexpected exception type: {ex.GetType()}, Message: {ex.Message}"); } - + finally + { + AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, switchValue); + } TestUtilities.AssertFailIfErrors(context); } From e20d0671f74c859d8aa20c81abdedb02fec3f01b Mon Sep 17 00:00:00 2001 From: saurabhsathe-ms Date: Mon, 12 May 2025 23:29:51 -0700 Subject: [PATCH 28/52] Removed console lines, Added a condition to check if the max actor chain length exceeds 5. --- .../JsonWebTokenHandler.CreateToken.cs | 3 - .../SecurityTokenDescriptor.cs | 2 +- .../ActorClaimsTests.cs | 94 +++++++++---------- 3 files changed, 44 insertions(+), 55 deletions(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs index f0ec1c9f94..7760c03abd 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs @@ -677,12 +677,9 @@ internal static void WriteJwsPayload( { foreach (KeyValuePair kvp in tokenDescriptor.Claims) { - Console.WriteLine(kvp.Key); AppContext.TryGetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, out bool isnebaled); - Console.WriteLine($"Equal?{tokenDescriptor.ActorClaimName}, {isnebaled}"); if (kvp.Key.Equals(tokenDescriptor.ActorClaimName, StringComparison.Ordinal)) { - Console.WriteLine(kvp.Value); continue; } if (!descriptorClaimsAudienceChecked && kvp.Key.Equals(JwtRegisteredClaimNames.Aud, StringComparison.Ordinal)) diff --git a/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs b/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs index 74f7b20f24..161fe878bb 100644 --- a/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs +++ b/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs @@ -131,7 +131,7 @@ public int MaxActorChainLength get => s_maxActorChainLength; set { - if (value < 0) + if (value < 0 || value > 5) throw LogHelper.LogExceptionMessage( new ArgumentOutOfRangeException( LogHelper.FormatInvariant( diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs index f0329f2e15..2ad5ba5696 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs @@ -60,6 +60,7 @@ public void ActorTokenInClaimsDictionaryShouldBeProperlySerialized() Assert.Equal("actor-subject-id", actorJwt.Payload.GetValue("sub")); Assert.Equal("Actor Name", actorJwt.Payload.GetValue("name")); Assert.Equal("admin", actorJwt.Payload.GetValue("role")); + TestUtilities.AssertFailIfErrors(context); } catch (Exception ex) { @@ -67,10 +68,8 @@ public void ActorTokenInClaimsDictionaryShouldBeProperlySerialized() } finally { - AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, switchValue); + AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, false); } - - TestUtilities.AssertFailIfErrors(context); } [Fact] @@ -118,6 +117,7 @@ public void ActorTokenAsSubjectShouldBeProperlySerialized() Assert.Equal("actor-subject-id", actorJwt.Payload.GetValue("sub")); Assert.Equal("Actor Name", actorJwt.Payload.GetValue("name")); Assert.Equal("admin", actorJwt.Payload.GetValue("role")); + TestUtilities.AssertFailIfErrors(context); } catch (Exception ex) { @@ -125,10 +125,8 @@ public void ActorTokenAsSubjectShouldBeProperlySerialized() } finally { - AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, switchValue); + AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, false); } - - TestUtilities.AssertFailIfErrors(context); } [Fact] @@ -171,8 +169,6 @@ public void ActorTokenInBothClaimsAndSubjectShouldPreferClaimsValue() { "act", claimsActorIdentity } } }; - tokenDescriptor.ActorClaimName = "act"; // Set the actor claim name to "act" for testing - var token = tokenHandler.CreateToken(tokenDescriptor); JsonWebToken decodedToken = tokenHandler.ReadJsonWebToken(token); @@ -189,6 +185,7 @@ public void ActorTokenInBothClaimsAndSubjectShouldPreferClaimsValue() Assert.Equal("claims-actor-id", actorJwt.Payload.GetValue("sub")); Assert.Equal("Claims Actor", actorJwt.Payload.GetValue("name")); Assert.NotEqual("subject-actor-id", actorJwt.Payload.GetValue("sub")); + TestUtilities.AssertFailIfErrors(context); } catch (Exception ex) { @@ -196,10 +193,8 @@ public void ActorTokenInBothClaimsAndSubjectShouldPreferClaimsValue() } finally { - AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, switchValue); + AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, false); } - - TestUtilities.AssertFailIfErrors(context); } [Fact] @@ -267,6 +262,7 @@ public void NestedActorTokenInClaimsDictionaryShouldBeProperlySerialized() // Verify nested actor claims Assert.Equal("nested-actor-id", nestedActorJwt.Payload.GetValue("sub")); Assert.Equal("Nested Actor", nestedActorJwt.Payload.GetValue("name")); + TestUtilities.AssertFailIfErrors(context); } catch (Exception ex) { @@ -274,10 +270,8 @@ public void NestedActorTokenInClaimsDictionaryShouldBeProperlySerialized() } finally { - AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, switchValue); + AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, false); } - - TestUtilities.AssertFailIfErrors(context); } [Fact] @@ -313,10 +307,9 @@ public void NestedActorTokenAsSubjectShouldBeProperlySerialized() Issuer = "https://example.com", Audience = "https://api.example.com", Expires = DateTime.UtcNow.AddHours(1), - SigningCredentials = Default.AsymmetricSigningCredentials + SigningCredentials = Default.AsymmetricSigningCredentials, }; AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, true); - tokenDescriptor.ActorClaimName = "act"; // Set the actor claim name to "act" for testing var token = tokenHandler.CreateToken(tokenDescriptor); JsonWebToken decodedToken = tokenHandler.ReadJsonWebToken(token); @@ -343,6 +336,7 @@ public void NestedActorTokenAsSubjectShouldBeProperlySerialized() // Verify nested actor claims Assert.Equal("nested-actor-id", nestedActorJwt.Payload.GetValue("sub")); Assert.Equal("Nested Actor", nestedActorJwt.Payload.GetValue("name")); + TestUtilities.AssertFailIfErrors(context); } catch (Exception ex) { @@ -350,11 +344,10 @@ public void NestedActorTokenAsSubjectShouldBeProperlySerialized() } finally { - AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, switchValue); + AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, false); } - TestUtilities.AssertFailIfErrors(context); } - + [ResetAppContextSwitches] [Fact] public void MaxActorChainLength_RejectsNegativeValues() { @@ -388,15 +381,15 @@ public void MaxActorChainLength_RejectsNegativeValues() tokenDescriptor.MaxActorChainLength = 1; Assert.Equal(1, tokenDescriptor.MaxActorChainLength); - // Act & Assert - Valid larger value - tokenDescriptor.MaxActorChainLength = 10; - Assert.Equal(10, tokenDescriptor.MaxActorChainLength); + ex = Assert.Throws(() => + tokenDescriptor.MaxActorChainLength = 10); + Assert.Contains("IDX11027", ex.Message); } finally { // Restore to original value tokenDescriptor.MaxActorChainLength = originalValue; - AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, switchValue); + AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, false); } } @@ -406,7 +399,7 @@ public void NestedSubjectActorTokens_ExceedingMaxDepth_ThrowsException() var context = new CompareContext($"{this}.NestedActorTokens_ExceedingMaxDepth_ThrowsException"); bool switchValue = false; AppContext.TryGetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, out switchValue); - + AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, true); try { // Arrange @@ -439,15 +432,16 @@ public void NestedSubjectActorTokens_ExceedingMaxDepth_ThrowsException() Subject = mainIdentity, Issuer = "https://example.com", Audience = "https://api.example.com", - SigningCredentials = Default.AsymmetricSigningCredentials + SigningCredentials = Default.AsymmetricSigningCredentials, + ActorClaimName = "act", + MaxActorChainLength = 2 }; - AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, true); - tokenDescriptor.ActorClaimName = "act"; // Set the actor claim name to "act" for testing - tokenDescriptor.MaxActorChainLength = 2; // Allow only 2 levels of nesting // Act - This should throw a SecurityTokenException var token = handler.CreateToken(tokenDescriptor); context.Diffs.Add("Expected exception was not thrown."); + + TestUtilities.AssertFailIfErrors(context); } catch (SecurityTokenException ex) { @@ -464,10 +458,9 @@ public void NestedSubjectActorTokens_ExceedingMaxDepth_ThrowsException() } finally { - AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, switchValue); + AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, false); } - TestUtilities.AssertFailIfErrors(context); } [Fact] @@ -476,7 +469,7 @@ public void NestedClaimsDictionaryActorTokens_ExceedingMaxDepth_ThrowsException( var context = new CompareContext($"{this}.NestedClaimsDictionaryActorTokens_ExceedingMaxDepth_ThrowsException"); bool switchValue = false; AppContext.TryGetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, out switchValue); - + AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, true); try { // Arrange @@ -508,14 +501,15 @@ public void NestedClaimsDictionaryActorTokens_ExceedingMaxDepth_ThrowsException( Claims = new Dictionary { { actorname, actorIdentity } - } + }, + ActorClaimName = actorname, + MaxActorChainLength = 1 }; - AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, true); - tokenDescriptor.ActorClaimName = actorname; // Set the actor claim name to "act" for testing - tokenDescriptor.MaxActorChainLength = 1; // Allow only 1 level of nesting + // Act var token = handler.CreateToken(tokenDescriptor); context.Diffs.Add("Expected exception was not thrown."); + TestUtilities.AssertFailIfErrors(context); } catch (SecurityTokenException ex) { @@ -532,16 +526,13 @@ public void NestedClaimsDictionaryActorTokens_ExceedingMaxDepth_ThrowsException( } finally { - AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, switchValue); + AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, false); } - TestUtilities.AssertFailIfErrors(context); } - + [ResetAppContextSwitches] [Fact] public void ActorTokens_MixedSourceRespectMaxActorChainLength() { - bool switchValue = false; - AppContext.TryGetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, out switchValue); AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, true); try { @@ -581,15 +572,16 @@ public void ActorTokens_MixedSourceRespectMaxActorChainLength() Claims = new Dictionary { { actorname, level2Actor } - } + }, + ActorClaimName = actorname, + MaxActorChainLength = 1 }; - tokenDescriptor.ActorClaimName = actorname; - tokenDescriptor.MaxActorChainLength = 1; + var token = handler.CreateToken(tokenDescriptor); var jwtToken = handler.ReadJsonWebToken(token); // Assert - Assert.True(jwtToken.Payload.HasClaim(actorname), "JWT token should contain 'actort' claim"); + Assert.True(jwtToken.Payload.HasClaim(actorname), "JWT token should contain 'act' claim"); // Verify we get the actor from Claims dictionary (should be level2Actor) var actorToken = handler.ReadJsonWebToken(jwtToken.Actor); @@ -601,7 +593,7 @@ public void ActorTokens_MixedSourceRespectMaxActorChainLength() } finally { - AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, switchValue); + AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, false); } } [Fact] @@ -610,7 +602,7 @@ public void NestedClaimTokens_ExceedingMaxDepth_ThrowsException() var context = new CompareContext($"{this}.NestedActorTokens_ExceedingMaxDepth_ThrowsException"); bool switchValue = false; AppContext.TryGetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, out switchValue); - + var actorname = "act"; try { // Arrange @@ -645,15 +637,16 @@ public void NestedClaimTokens_ExceedingMaxDepth_ThrowsException() Claims = new Dictionary { { "act", level1Actor } - } + }, + ActorClaimName = actorname, + MaxActorChainLength = 1 }; AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, true); - tokenDescriptor.ActorClaimName = "act"; // Set the actor claim name to "act" for testing - tokenDescriptor.MaxActorChainLength = 2; // Allow only 2 levels of nesting // Act - This should throw a SecurityTokenException var token = handler.CreateToken(tokenDescriptor); context.Diffs.Add("Expected exception was not thrown."); + TestUtilities.AssertFailIfErrors(context); } catch (SecurityTokenException ex) { @@ -670,9 +663,8 @@ public void NestedClaimTokens_ExceedingMaxDepth_ThrowsException() } finally { - AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, switchValue); + AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, false); } - TestUtilities.AssertFailIfErrors(context); } } From d531454e55de0dd79e5935035e524087ba80cc35 Mon Sep 17 00:00:00 2001 From: saurabhsathe-ms Date: Tue, 13 May 2025 10:52:26 -0700 Subject: [PATCH 29/52] moved actor chain depth to Security Token Descriptor --- .../JsonWebToken.cs | 8 +++--- .../JsonWebTokenHandler.CreateToken.cs | 22 +++++++--------- .../PublicAPI/net462/PublicAPI.Unshipped.txt | 2 ++ .../PublicAPI/net472/PublicAPI.Unshipped.txt | 2 ++ .../PublicAPI/net6.0/PublicAPI.Unshipped.txt | 3 ++- .../PublicAPI/net8.0/PublicAPI.Unshipped.txt | 2 ++ .../PublicAPI/net9.0/PublicAPI.Unshipped.txt | 2 ++ .../netstandard2.0/PublicAPI.Unshipped.txt | 2 ++ .../SecurityTokenDescriptor.cs | 25 ++++++++++++++----- 9 files changed, 44 insertions(+), 24 deletions(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs index 5fb6e83752..9a293bc1d0 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs @@ -58,7 +58,7 @@ public partial class JsonWebToken : SecurityToken internal DateTime? _validFrom; internal DateTime? _validTo; - private static string s_actorClaimName = "act"; + private string actorClaimName = "act"; /// /// Gets or sets the claim type name for the actor claim. /// Permissible values are 'act' or 'actort'. @@ -69,9 +69,9 @@ public partial class JsonWebToken : SecurityToken /// /// Thrown if the value is not 'act' or 'actort'. /// - internal static string ActorClaimName + internal string ActorClaimName { - get => AppContextSwitches.SerializeDeserializeActorClaim ? s_actorClaimName : "actort"; + get => AppContextSwitches.SerializeDeserializeActorClaim ? actorClaimName : "actort"; set { if (string.IsNullOrEmpty(value)) @@ -82,7 +82,7 @@ internal static string ActorClaimName LogHelper.MarkAsNonPII("ActorClaimName")) + ". ActorClaimName cannot be empty.")); - s_actorClaimName = value; + actorClaimName = value; } } #endregion diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs index 7760c03abd..6a3b149878 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs @@ -143,8 +143,7 @@ public virtual string CreateToken(SecurityTokenDescriptor tokenDescriptor) internal static string CreateToken( SecurityTokenDescriptor tokenDescriptor, bool setdefaultTimesOnTokenCreation, - int tokenLifetimeInMinutes, - int actorChainDepth = 0) + int tokenLifetimeInMinutes) { // The form of a JWS is: Base64UrlEncoding(UTF8(Header)) | . | Base64UrlEncoding(Payload) | . | Base64UrlEncoding(Signature) // Where the Header is specifically the UTF8 bytes of the JSON, whereas the Payload encoding is not specified, but UTF8 is used by everyone. @@ -176,8 +175,7 @@ internal static string CreateToken( ref writer, tokenDescriptor, setdefaultTimesOnTokenCreation, - tokenLifetimeInMinutes, - actorChainDepth); + tokenLifetimeInMinutes); // mark end of payload int payloadEnd = (int)utf8ByteMemoryStream.Length; @@ -603,14 +601,12 @@ int sizeOfEncodedHeaderAndPayloadAsciiBytes /// The used to create the token. /// A boolean that controls if expiration, notbefore, issuedat should be added if missing. /// The default value for the token lifetime in minutes. - /// Controls the recursion length while parsing nested actor tokens /// A dictionary of claims. internal static void WriteJwsPayload( ref Utf8JsonWriter writer, SecurityTokenDescriptor tokenDescriptor, bool setDefaultTimesOnTokenCreation, - int tokenLifetimeInMinutes, - int actorChainDepth) + int tokenLifetimeInMinutes) { bool descriptorClaimsAudienceChecked = false; bool audienceSet = false; @@ -763,7 +759,7 @@ internal static void WriteJwsPayload( } } if (AppContextSwitches.SerializeDeserializeActorClaim) - WriteActorToken(writer, tokenDescriptor, actorChainDepth); + WriteActorToken(writer, tokenDescriptor); AddSubjectClaims(ref writer, tokenDescriptor, audienceSet, issuerSet, ref expSet, ref iatSet, ref nbfSet); @@ -1083,8 +1079,7 @@ internal static byte[] WriteJweHeader(SecurityTokenDescriptor tokenDescriptor) } private static void WriteActorToken( Utf8JsonWriter writer, - SecurityTokenDescriptor tokenDescriptor, - int actorChainDepth = 0) + SecurityTokenDescriptor tokenDescriptor) { if (tokenDescriptor == null) { @@ -1094,7 +1089,7 @@ private static void WriteActorToken( if (tokenDescriptor.Claims != null && tokenDescriptor.Claims.ContainsKey(tokenDescriptor.ActorClaimName)) { // Check for maximum actor chain depth - if (actorChainDepth >= tokenDescriptor.MaxActorChainLength) + if (tokenDescriptor.ActorChainDepth >= tokenDescriptor.MaxActorChainLength) { throw LogHelper.LogExceptionMessage( new SecurityTokenException( @@ -1112,7 +1107,7 @@ private static void WriteActorToken( { if (tokenDescriptor.Subject != null && tokenDescriptor.Subject.Actor != null) { - if (actorChainDepth >= tokenDescriptor.MaxActorChainLength) + if (tokenDescriptor.ActorChainDepth >= tokenDescriptor.MaxActorChainLength) { throw LogHelper.LogExceptionMessage( new SecurityTokenException( @@ -1131,7 +1126,8 @@ private static void WriteActorToken( { actorTokenDescriptor.MaxActorChainLength = tokenDescriptor.MaxActorChainLength; actorTokenDescriptor.ActorClaimName = tokenDescriptor.ActorClaimName; - string actorToken = CreateToken(actorTokenDescriptor, false, 0, actorChainDepth + 1); + actorTokenDescriptor.ActorChainDepth = actorTokenDescriptor.ActorChainDepth + 1; + string actorToken = CreateToken(actorTokenDescriptor, false, 0); writer.WritePropertyName(tokenDescriptor.ActorClaimName); writer.WriteStringValue(actorToken); } diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net462/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net462/PublicAPI.Unshipped.txt index 0eaa8f883e..0f71b7fc56 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net462/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net462/PublicAPI.Unshipped.txt @@ -1,3 +1,5 @@ +Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorChainDepth.get -> int +Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorChainDepth.set -> void Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimName.get -> string Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimName.set -> void Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.get -> int diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net472/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net472/PublicAPI.Unshipped.txt index 0eaa8f883e..0f71b7fc56 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net472/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net472/PublicAPI.Unshipped.txt @@ -1,3 +1,5 @@ +Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorChainDepth.get -> int +Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorChainDepth.set -> void Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimName.get -> string Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimName.set -> void Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.get -> int diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net6.0/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net6.0/PublicAPI.Unshipped.txt index 30d724e156..0f71b7fc56 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net6.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net6.0/PublicAPI.Unshipped.txt @@ -1,5 +1,6 @@ +Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorChainDepth.get -> int +Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorChainDepth.set -> void Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimName.get -> string Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimName.set -> void Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.get -> int Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.set -> void - diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net8.0/PublicAPI.Unshipped.txt index 0eaa8f883e..0f71b7fc56 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net8.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net8.0/PublicAPI.Unshipped.txt @@ -1,3 +1,5 @@ +Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorChainDepth.get -> int +Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorChainDepth.set -> void Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimName.get -> string Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimName.set -> void Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.get -> int diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net9.0/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net9.0/PublicAPI.Unshipped.txt index 0eaa8f883e..0f71b7fc56 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net9.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net9.0/PublicAPI.Unshipped.txt @@ -1,3 +1,5 @@ +Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorChainDepth.get -> int +Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorChainDepth.set -> void Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimName.get -> string Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimName.set -> void Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.get -> int diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt index 0eaa8f883e..0f71b7fc56 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt @@ -1,3 +1,5 @@ +Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorChainDepth.get -> int +Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorChainDepth.set -> void Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimName.get -> string Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimName.set -> void Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.get -> int diff --git a/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs b/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs index 161fe878bb..68a078f035 100644 --- a/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs +++ b/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs @@ -118,7 +118,7 @@ public class SecurityTokenDescriptor [DefaultValue(true)] public bool IncludeKeyIdInHeader { get; set; } = true; - private int s_maxActorChainLength = 5; + private int maxActorChainLength = 5; /// /// Gets or sets the maximum depth allowed when processing nested actor tokens. /// This prevents excessive recursion when handling deeply nested actor tokens. @@ -128,7 +128,7 @@ public class SecurityTokenDescriptor /// Thrown if the value is less than 0. public int MaxActorChainLength { - get => s_maxActorChainLength; + get => maxActorChainLength; set { if (value < 0 || value > 5) @@ -139,11 +139,11 @@ public int MaxActorChainLength LogHelper.MarkAsNonPII("MaxActorChainLength")) + ". Permissible values are integers in range 0 to 5")); - s_maxActorChainLength = value; + maxActorChainLength = value; } } - private string s_actorClaimName = "act"; + private string actorClaimName = "act"; /// /// Gets or sets the claim type name for the actor claim. /// Permissible values are 'act' or 'actort'. @@ -156,7 +156,7 @@ public int MaxActorChainLength /// public string ActorClaimName { - get => AppContextSwitches.SerializeDeserializeActorClaim ? s_actorClaimName : "actort"; + get => AppContextSwitches.SerializeDeserializeActorClaim ? actorClaimName : "actort"; set { if (string.IsNullOrEmpty(value)) @@ -166,7 +166,20 @@ public string ActorClaimName LogMessages.IDX11027, LogHelper.MarkAsNonPII("ActorClaimName")) + ". ActorClaimName cannot be empty.")); - s_actorClaimName = value; + actorClaimName = value; + } + } + private int _actorClainDepth; + /// + /// Gets or sets the depth of the actor chain. + /// This value determines the maximum depth of nested actor tokens that can be processed. + /// + public int ActorChainDepth + { + get => _actorClainDepth; + set + { + _actorClainDepth = value; } } } From d79c61bcaf14ce56c0e038785beb7cc591b766a8 Mon Sep 17 00:00:00 2001 From: saurabhsathe-ms Date: Thu, 15 May 2025 11:30:53 -0700 Subject: [PATCH 30/52] Updated validation parameters to validate JWT token upto a certain limit --- .../JsonWebTokenHandler.ValidateToken.cs | 17 ++- .../JsonWebTokenHandler.cs | 1 + .../PublicAPI.Unshipped.txt | 6 + .../TokenValidationParameters.cs | 66 ++++++++++ .../ActorClaimsTests.cs | 119 +++++++++++++++++- .../TokenValidationParametersTests.cs | 5 +- 6 files changed, 210 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs index 115e597b87..535b502967 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs @@ -452,7 +452,6 @@ public override async Task ValidateTokenAsync(SecurityTok var jwt = token as JsonWebToken; if (jwt == null) return new TokenValidationResult { Exception = LogHelper.LogArgumentException(nameof(token), $"{nameof(token)} must be a {nameof(JsonWebToken)}."), IsValid = false }; - try { return await ValidateTokenAsync(jwt, validationParameters).ConfigureAwait(false); @@ -496,7 +495,6 @@ internal async ValueTask ValidateTokenAsync( LogHelper.LogWarning(LogHelper.FormatInvariant(TokenLogMessages.IDX10261, validationParameters.ConfigurationManager.MetadataAddress, ex.ToString())); } } - TokenValidationResult tokenValidationResult = jsonWebToken.IsEncrypted ? await ValidateJWEAsync(jsonWebToken, validationParameters, currentConfiguration).ConfigureAwait(false) : await ValidateJWSAsync(jsonWebToken, validationParameters, currentConfiguration).ConfigureAwait(false); @@ -583,6 +581,21 @@ internal async ValueTask ValidateTokenPayloadAsync( Validators.ValidateTokenReplay(expires, jsonWebToken.EncodedToken, validationParameters); if (validationParameters.ValidateActor && !string.IsNullOrWhiteSpace(jsonWebToken.Actor)) { + if (AppContextSwitches.SerializeDeserializeActorClaim) + { + if (validationParameters.ActorChainDepth >= validationParameters.MaxActorChainLength) + { + throw LogHelper.LogExceptionMessage( + new SecurityTokenException( + LogHelper.FormatInvariant( + LogMessages.IDX14313, + LogHelper.MarkAsNonPII(validationParameters.MaxActorChainLength)))); + } + else + { + validationParameters.ActorChainDepth++; + } + } // Infinite recursion should not occur here, as the JsonWebToken passed into this method is (1) constructed from a string // AND (2) the signature is successfully validated on it. (1) implies that even if there are nested actor tokens, // they must end at some point since they cannot reference one another. (2) means that the token has a valid signature diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs index 173c06fe80..af3c86d2a6 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs @@ -553,6 +553,7 @@ private static TokenValidationResult ReadToken(string token, TokenValidationPara try { jsonWebToken = new JsonWebToken(token, validationParameters.TryReadJwtClaim); + jsonWebToken.ActorClaimName = validationParameters.ActorClaimName; } catch (Exception ex) { diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI.Unshipped.txt index e69de29bb2..66583ccb96 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI.Unshipped.txt @@ -0,0 +1,6 @@ +Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActorChainDepth.get -> int +Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActorChainDepth.set -> void +Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActorClaimName.get -> string +Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActorClaimName.set -> void +Microsoft.IdentityModel.Tokens.TokenValidationParameters.MaxActorChainLength.get -> int +Microsoft.IdentityModel.Tokens.TokenValidationParameters.MaxActorChainLength.set -> void \ No newline at end of file diff --git a/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs b/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs index fa6b589762..38ab2f8e4b 100644 --- a/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs +++ b/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs @@ -762,5 +762,71 @@ public string RoleClaimType /// The default is null. /// public IEnumerable ValidTypes { get; set; } + + + private int maxActorChainLength = 5; + /// + /// Gets or sets the maximum depth allowed when processing nested actor tokens. + /// This prevents excessive recursion when handling deeply nested actor tokens. + /// The value must be at least 0. Value 0 would mean that the actor token is not allowed to be nested. + /// Default value is 5. Max value is also 5 + /// + /// Thrown if the value is less than 0. + public int MaxActorChainLength + { + get => maxActorChainLength; + set + { + if (value < 0 || value > 5) + throw LogHelper.LogExceptionMessage( + new ArgumentOutOfRangeException( + LogHelper.FormatInvariant( + LogMessages.IDX11027, + LogHelper.MarkAsNonPII("MaxActorChainLength")) + + ". Permissible values are integers in range 0 to 5")); + + maxActorChainLength = value; + } + } + + private string actorClaimName = "act"; + /// + /// Gets or sets the claim type name for the actor claim. + /// Permissible values are 'act' or 'actort'. + /// + /// + /// Thrown if the value is null. + /// + /// + /// Thrown if the value is not 'act' or 'actort'. + /// + public string ActorClaimName + { + get => AppContextSwitches.SerializeDeserializeActorClaim ? actorClaimName : "actort"; + set + { + if (string.IsNullOrEmpty(value)) + throw LogHelper.LogExceptionMessage( + new ArgumentOutOfRangeException( + LogHelper.FormatInvariant( + LogMessages.IDX11027, + LogHelper.MarkAsNonPII("ActorClaimName")) + + ". ValidationParameters.ActorClaimName cannot be set to empty.")); + actorClaimName = value; + } + } + private int _actorClainDepth; + /// + /// Gets or sets the depth of the actor chain. + /// This value determines the maximum depth of nested actor tokens that can be processed. + /// + public int ActorChainDepth + { + get => _actorClainDepth; + set + { + _actorClainDepth = value; + } + } } } diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs index 2ad5ba5696..618dd29c10 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs @@ -8,6 +8,7 @@ using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.JsonWebTokens; using Xunit; +using System.Threading.Tasks; namespace Microsoft.IdentityModel.Tests { @@ -667,6 +668,122 @@ public void NestedClaimTokens_ExceedingMaxDepth_ThrowsException() } } + [Fact] + public async Task ValidateActorToken_WithMaxChainLength_ValidatesSuccessfully() + { + var context = new CompareContext($"{this}.ValidateActorToken_WithMaxChainLength_ValidatesSuccessfully"); + bool switchValue = false; + AppContext.TryGetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, out switchValue); + AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, true); + try + { + // Create a token with nested actors + var handler = new JsonWebTokenHandler(); + string actorname = "actortoken"; + // Create level 3 actor (innermost) + var level3Actor = new CaseSensitiveClaimsIdentity("Level3Auth"); + level3Actor.AddClaim(new Claim("sub", "level3-actor")); + level3Actor.AddClaim(new Claim("name", "Level 3 Actor")); + level3Actor.AddClaim(new Claim("exp", EpochTime.GetIntDate(DateTime.UtcNow.AddHours(1)).ToString())); + + + // Create level 2 actor with level 3 as its actor + var level2Actor = new CaseSensitiveClaimsIdentity("Level2Auth"); + level2Actor.AddClaim(new Claim("sub", "level2-actor")); + level2Actor.AddClaim(new Claim("name", "Level 2 Actor")); + level2Actor.Actor = level3Actor; + level2Actor.AddClaim(new Claim("exp", EpochTime.GetIntDate(DateTime.UtcNow.AddHours(1)).ToString())); + + // Create level 1 actor with level 2 as its actor + var level1Actor = new CaseSensitiveClaimsIdentity("Level1Auth"); + level1Actor.AddClaim(new Claim("sub", "level1-actor")); + level1Actor.AddClaim(new Claim("name", "Level 1 Actor")); + level1Actor.Actor = level2Actor; + level1Actor.AddClaim(new Claim("exp", EpochTime.GetIntDate(DateTime.UtcNow.AddHours(1)).ToString())); + + // Create main identity with level 1 as its actor + var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); + mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); + mainIdentity.AddClaim(new Claim("name", "Main User")); + mainIdentity.Actor = level1Actor; + + // Define audience + string audience = "https://api.example.com"; + string issuer = "https://example.com"; + + // Create token with actor chain + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = mainIdentity, + Issuer = issuer, + Audience = audience, + SigningCredentials = Default.AsymmetricSigningCredentials, + ActorClaimName = actorname, + MaxActorChainLength = 3 + }; + var token = handler.CreateToken(tokenDescriptor); + + // Configure validation parameters + var validationParameters = Default.AsymmetricSignTokenValidationParameters; + validationParameters.ValidIssuer = issuer; + validationParameters.ValidAudience = audience; + validationParameters.ValidateActor = true; + validationParameters.MaxActorChainLength = 3; + validationParameters.ActorClaimName = actorname; + validationParameters.ActorValidationParameters = validationParameters.Clone(); + + // Create actor validation parameters + var actorValidationParameters = validationParameters.Clone(); + actorValidationParameters.RequireSignedTokens = false; + actorValidationParameters.ValidateLifetime = false; + actorValidationParameters.ValidateAudience = false; + actorValidationParameters.ValidateIssuer = false; + + validationParameters.ActorValidationParameters = actorValidationParameters; + // Validate token + var result = await handler.ValidateTokenAsync(token, validationParameters); + if (!result.IsValid) + { + Console.WriteLine($"Validation failed: {result.Exception?.Message}"); + } + Assert.True(result.IsValid, "Token should be valid"); + + // Get the main JsonWebToken from the result + var mainToken = result.SecurityToken as JsonWebToken; + Assert.NotNull(mainToken); + Assert.Equal("main-subject-id", mainToken.Subject); + Console.WriteLine($"Main User Subject: {mainToken.Subject}"); + + // Follow and verify actor chain using JsonWebToken.Actor and ReadJsonWebToken + var currentToken = mainToken; + var actorLevels = new[] { "level1-actor", "level2-actor", "level3-actor" }; + + for (int i = 0; i < actorLevels.Length; i++) + { + // Get actor JWT string and convert it to JsonWebToken + var actorTokenString = currentToken.Actor; + Assert.False(string.IsNullOrEmpty(actorTokenString), $"Actor token at level {i} should not be null or empty"); + Console.WriteLine($"Here is the token for {i} iteration : {actorTokenString}"); + // Parse the actor token string into a JsonWebToken + var actorToken = handler.ReadJsonWebToken(actorTokenString); + Assert.NotNull(actorToken); + actorToken.ActorClaimName = actorname; + // Verify actor token claims + Assert.Equal(actorLevels[i], actorToken.Subject); + Assert.NotNull(actorToken.GetPayloadValue("name")); + Console.WriteLine($"Actor {i + 1}: Subject={actorToken.Subject}, Name={actorToken.GetPayloadValue("name")}"); + + // Move to next actor in the chain + currentToken = actorToken; + } + // Verify no more actors beyond max depth + Assert.True(string.IsNullOrEmpty(currentToken.Actor), "There should be no more actors beyond the specified depth"); + TestUtilities.AssertFailIfErrors(context); + } + finally + { + AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, false); + } + } } } - diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/TokenValidationParametersTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/TokenValidationParametersTests.cs index c728e0afa4..436eef9f5f 100644 --- a/test/Microsoft.IdentityModel.Tokens.Tests/TokenValidationParametersTests.cs +++ b/test/Microsoft.IdentityModel.Tokens.Tests/TokenValidationParametersTests.cs @@ -15,7 +15,7 @@ namespace Microsoft.IdentityModel.Tokens.Tests { public class TokenValidationParametersTests { - int ExpectedPropertyCount = 62; + int ExpectedPropertyCount = 65; // GetSets() compares the total property count which includes internal properties, against a list of public properties, minus delegates. // This allows us to keep track of any properties we are including in the total that are not public nor delegates. @@ -199,6 +199,8 @@ public void GetSets() PropertyNamesAndSetGetValue = new List>> { new KeyValuePair>("ActorValidationParameters", new List{(TokenValidationParameters)null, new TokenValidationParameters(), new TokenValidationParameters()}), + new KeyValuePair>("ActorClaimName", new List{"actort"}), + new KeyValuePair>("ActorChainDepth", new List{0,1}), new KeyValuePair>("AuthenticationType", new List{(string)null, Guid.NewGuid().ToString(), Guid.NewGuid().ToString()}), new KeyValuePair>("ClockSkew", new List{TokenValidationParameters.DefaultClockSkew, TimeSpan.FromHours(2), TimeSpan.FromMinutes(1)}), new KeyValuePair>("ConfigurationManager", new List{(BaseConfigurationManager)null, new ConfigurationManager("http://127.0.0.1", new OpenIdConnectConfigurationRetriever()), new ConfigurationManager("http://127.0.0.1", new WsFederationConfigurationRetriever()) }), @@ -212,6 +214,7 @@ public void GetSets() new KeyValuePair>("IssuerSigningKeys", new List{(IEnumerable)null, new List{KeyingMaterial.DefaultX509Key_2048, KeyingMaterial.RsaSecurityKey_1024}, new List()}), new KeyValuePair>("LogTokenId", new List{true, false, true}), new KeyValuePair>("LogValidationExceptions", new List{true, false, true}), + new KeyValuePair>("MaxActorChainLength", new List{5,2}), new KeyValuePair>("NameClaimType", new List{ClaimsIdentity.DefaultNameClaimType, Guid.NewGuid().ToString(), Guid.NewGuid().ToString()}), new KeyValuePair>("PropertyBag", new List{(IDictionary)null, new Dictionary {{"CustomKey", "CustomValue"}}, new Dictionary()}), new KeyValuePair>("RefreshBeforeValidation", new List{false, true, false}), From f7de606f1f41841a4ffd9d349a4d24272ded0df0 Mon Sep 17 00:00:00 2001 From: saurabhsathe-ms Date: Fri, 16 May 2025 17:28:06 -0700 Subject: [PATCH 31/52] Removed old serialization. Added json object serialization and updated the testcases --- .../JsonWebToken.cs | 30 +- .../JsonWebTokenHandler.CreateToken.cs | 74 ++--- .../JsonWebTokenHandler.ValidateToken.cs | 15 - .../JsonWebTokenHandler.cs | 1 - .../LogMessages.cs | 2 +- .../SecurityTokenDescriptor.cs | 12 +- .../ActorClaimsTests.cs | 266 +++++------------- 7 files changed, 122 insertions(+), 278 deletions(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs index 9a293bc1d0..887e20b55b 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs @@ -57,34 +57,6 @@ public partial class JsonWebToken : SecurityToken internal DateTime? _nbfDateTime; internal DateTime? _validFrom; internal DateTime? _validTo; - - private string actorClaimName = "act"; - /// - /// Gets or sets the claim type name for the actor claim. - /// Permissible values are 'act' or 'actort'. - /// - /// - /// Thrown if the value is null. - /// - /// - /// Thrown if the value is not 'act' or 'actort'. - /// - internal string ActorClaimName - { - get => AppContextSwitches.SerializeDeserializeActorClaim ? actorClaimName : "actort"; - set - { - if (string.IsNullOrEmpty(value)) - throw LogHelper.LogExceptionMessage( - new ArgumentNullException( - LogHelper.FormatInvariant( - LogMessages.IDX14313, - LogHelper.MarkAsNonPII("ActorClaimName")) - + ". ActorClaimName cannot be empty.")); - - actorClaimName = value; - } - } #endregion /// @@ -1048,7 +1020,7 @@ public string Actor { get { - _act ??= Payload.GetStringValue(ActorClaimName); + _act ??= Payload.GetStringValue(JwtRegisteredClaimNames.Actort); return _act; } } diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs index 6a3b149878..c41afc8bc9 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs @@ -759,7 +759,7 @@ internal static void WriteJwsPayload( } } if (AppContextSwitches.SerializeDeserializeActorClaim) - WriteActorToken(writer, tokenDescriptor); + WriteActorToken(writer, tokenDescriptor, setDefaultTimesOnTokenCreation, tokenLifetimeInMinutes); AddSubjectClaims(ref writer, tokenDescriptor, audienceSet, issuerSet, ref expSet, ref iatSet, ref nbfSet); @@ -1077,61 +1077,67 @@ internal static byte[] WriteJweHeader(SecurityTokenDescriptor tokenDescriptor) } } } - private static void WriteActorToken( + internal static void WriteActorToken( Utf8JsonWriter writer, - SecurityTokenDescriptor tokenDescriptor) + SecurityTokenDescriptor tokenDescriptor, + bool setDefaultTimesOnTokenCreation, + int tokenLifetimeInMinutes) { if (tokenDescriptor == null) - { throw new ArgumentNullException(nameof(tokenDescriptor)); + + var actorTokenDescriptor = CreateActorTokenDescriptor(tokenDescriptor); + if (actorTokenDescriptor == null || actorTokenDescriptor.Subject == null) + return; + writer.WritePropertyName(tokenDescriptor.ActorClaimName); + WriteJwsPayload(ref writer, actorTokenDescriptor, setDefaultTimesOnTokenCreation, tokenLifetimeInMinutes); + } + + private static void ValidateActorChainDepth(SecurityTokenDescriptor tokenDescriptor) + { + if (tokenDescriptor.ActorChainDepth >= tokenDescriptor.MaxActorChainLength) + { + throw LogHelper.LogExceptionMessage( + new SecurityTokenException(LogHelper.FormatInvariant( + LogMessages.IDX14313, + LogHelper.MarkAsNonPII(tokenDescriptor.ActorChainDepth), + LogHelper.MarkAsNonPII(tokenDescriptor.MaxActorChainLength)))); } + } + + private static SecurityTokenDescriptor CreateActorTokenDescriptor(SecurityTokenDescriptor tokenDescriptor) + { SecurityTokenDescriptor actorTokenDescriptor = null; - if (tokenDescriptor.Claims != null && tokenDescriptor.Claims.ContainsKey(tokenDescriptor.ActorClaimName)) + + // Check for actor in claims first + if (tokenDescriptor.Claims?.ContainsKey(tokenDescriptor.ActorClaimName) == true) { - // Check for maximum actor chain depth - if (tokenDescriptor.ActorChainDepth >= tokenDescriptor.MaxActorChainLength) - { - throw LogHelper.LogExceptionMessage( - new SecurityTokenException( - LogHelper.FormatInvariant( - LogMessages.IDX14313, - LogHelper.MarkAsNonPII(tokenDescriptor.MaxActorChainLength)))); - } ClaimsIdentity actor = tokenDescriptor.Claims[tokenDescriptor.ActorClaimName] as ClaimsIdentity; actorTokenDescriptor = new SecurityTokenDescriptor { Subject = actor, }; } - else + // Then check for actor in subject + else if (tokenDescriptor.Subject?.Actor != null) { - if (tokenDescriptor.Subject != null && tokenDescriptor.Subject.Actor != null) + actorTokenDescriptor = new SecurityTokenDescriptor { - if (tokenDescriptor.ActorChainDepth >= tokenDescriptor.MaxActorChainLength) - { - throw LogHelper.LogExceptionMessage( - new SecurityTokenException( - LogHelper.FormatInvariant( - LogMessages.IDX14313, - LogHelper.MarkAsNonPII(tokenDescriptor.MaxActorChainLength)))); - } - actorTokenDescriptor = new SecurityTokenDescriptor - { - Subject = tokenDescriptor.Subject.Actor, - }; - } + Subject = tokenDescriptor.Subject.Actor, + }; } - if (actorTokenDescriptor != null) { + ValidateActorChainDepth(tokenDescriptor); + actorTokenDescriptor.MaxActorChainLength = tokenDescriptor.MaxActorChainLength; actorTokenDescriptor.ActorClaimName = tokenDescriptor.ActorClaimName; - actorTokenDescriptor.ActorChainDepth = actorTokenDescriptor.ActorChainDepth + 1; - string actorToken = CreateToken(actorTokenDescriptor, false, 0); - writer.WritePropertyName(tokenDescriptor.ActorClaimName); - writer.WriteStringValue(actorToken); + actorTokenDescriptor.ActorChainDepth = tokenDescriptor.ActorChainDepth + 1; } + + return actorTokenDescriptor; } + internal static byte[] CompressToken(byte[] utf8Bytes, string compressionAlgorithm) { if (string.IsNullOrEmpty(compressionAlgorithm)) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs index 535b502967..8e3110db4c 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs @@ -581,21 +581,6 @@ internal async ValueTask ValidateTokenPayloadAsync( Validators.ValidateTokenReplay(expires, jsonWebToken.EncodedToken, validationParameters); if (validationParameters.ValidateActor && !string.IsNullOrWhiteSpace(jsonWebToken.Actor)) { - if (AppContextSwitches.SerializeDeserializeActorClaim) - { - if (validationParameters.ActorChainDepth >= validationParameters.MaxActorChainLength) - { - throw LogHelper.LogExceptionMessage( - new SecurityTokenException( - LogHelper.FormatInvariant( - LogMessages.IDX14313, - LogHelper.MarkAsNonPII(validationParameters.MaxActorChainLength)))); - } - else - { - validationParameters.ActorChainDepth++; - } - } // Infinite recursion should not occur here, as the JsonWebToken passed into this method is (1) constructed from a string // AND (2) the signature is successfully validated on it. (1) implies that even if there are nested actor tokens, // they must end at some point since they cannot reference one another. (2) means that the token has a valid signature diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs index af3c86d2a6..173c06fe80 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs @@ -553,7 +553,6 @@ private static TokenValidationResult ReadToken(string token, TokenValidationPara try { jsonWebToken = new JsonWebToken(token, validationParameters.TryReadJwtClaim); - jsonWebToken.ActorClaimName = validationParameters.ActorClaimName; } catch (Exception ex) { diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/LogMessages.cs b/src/Microsoft.IdentityModel.JsonWebTokens/LogMessages.cs index 3db53dc759..79333eaa37 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/LogMessages.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/LogMessages.cs @@ -51,6 +51,6 @@ internal static class LogMessages internal const string IDX14310 = "IDX14310: JWE authentication tag is missing."; internal const string IDX14311 = "IDX14311: Unable to decode the authentication tag as a Base64Url encoded string."; internal const string IDX14312 = "IDX14312: Unable to decode the cipher text as a Base64Url encoded string."; - internal const string IDX14313 = "IDX14313: Unable to set actor token claim name. Actor token claim name cannot be null or empty"; + internal const string IDX14313 = "IDX14313: Unable to serialize actor token. Maximum actor token depth reached. Current nesting depth is {0} while max depth set is {1}"; } } diff --git a/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs b/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs index 68a078f035..352dc288dc 100644 --- a/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs +++ b/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs @@ -118,7 +118,7 @@ public class SecurityTokenDescriptor [DefaultValue(true)] public bool IncludeKeyIdInHeader { get; set; } = true; - private int maxActorChainLength = 5; + private int _maxActorChainLength = 5; /// /// Gets or sets the maximum depth allowed when processing nested actor tokens. /// This prevents excessive recursion when handling deeply nested actor tokens. @@ -128,7 +128,7 @@ public class SecurityTokenDescriptor /// Thrown if the value is less than 0. public int MaxActorChainLength { - get => maxActorChainLength; + get => _maxActorChainLength; set { if (value < 0 || value > 5) @@ -139,11 +139,11 @@ public int MaxActorChainLength LogHelper.MarkAsNonPII("MaxActorChainLength")) + ". Permissible values are integers in range 0 to 5")); - maxActorChainLength = value; + _maxActorChainLength = value; } } - private string actorClaimName = "act"; + private string _actorClaimName = "act"; /// /// Gets or sets the claim type name for the actor claim. /// Permissible values are 'act' or 'actort'. @@ -156,7 +156,7 @@ public int MaxActorChainLength /// public string ActorClaimName { - get => AppContextSwitches.SerializeDeserializeActorClaim ? actorClaimName : "actort"; + get => _actorClaimName; set { if (string.IsNullOrEmpty(value)) @@ -166,7 +166,7 @@ public string ActorClaimName LogMessages.IDX11027, LogHelper.MarkAsNonPII("ActorClaimName")) + ". ActorClaimName cannot be empty.")); - actorClaimName = value; + _actorClaimName = value; } } private int _actorClainDepth; diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs index 618dd29c10..4ad40a6531 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs @@ -4,16 +4,16 @@ using System; using System.Collections.Generic; using System.Security.Claims; +using System.Text.Json; using Microsoft.IdentityModel.TestUtils; using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.JsonWebTokens; using Xunit; -using System.Threading.Tasks; - namespace Microsoft.IdentityModel.Tests { public class ActorClaimsTests { + [ResetAppContextSwitches] [Fact] public void ActorTokenInClaimsDictionaryShouldBeProperlySerialized() { @@ -21,6 +21,7 @@ public void ActorTokenInClaimsDictionaryShouldBeProperlySerialized() bool switchValue = false; AppContext.TryGetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, out switchValue); AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, true); + string actorname = "act"; try { // Create a ClaimsIdentity for the actor @@ -45,7 +46,7 @@ public void ActorTokenInClaimsDictionaryShouldBeProperlySerialized() SigningCredentials = Default.AsymmetricSigningCredentials, Claims = new Dictionary { - { "act", actorIdentity } + { actorname, actorIdentity } } }; var token = tokenHandler.CreateToken(tokenDescriptor); @@ -53,14 +54,14 @@ public void ActorTokenInClaimsDictionaryShouldBeProperlySerialized() // Verify actor claim exists in the token Assert.True(decodedToken.Payload.HasClaim(tokenDescriptor.ActorClaimName), "JWT token should contain 'actort' claim"); - // Get the actor token and verify it contains the expected claims - var actorTokenString = decodedToken.Actor; - Assert.NotNull(actorTokenString); - - JsonWebToken actorJwt = tokenHandler.ReadJsonWebToken(actorTokenString); - Assert.Equal("actor-subject-id", actorJwt.Payload.GetValue("sub")); - Assert.Equal("Actor Name", actorJwt.Payload.GetValue("name")); - Assert.Equal("admin", actorJwt.Payload.GetValue("role")); + // Verify the actor object directly + var actorObject = decodedToken.Payload.GetValue(tokenDescriptor.ActorClaimName); + Assert.Equal(JsonValueKind.Object, actorObject.ValueKind); + + // Verify actor claims directly from the JSON object + Assert.Equal("actor-subject-id", actorObject.GetProperty("sub").GetString()); + Assert.Equal("Actor Name", actorObject.GetProperty("name").GetString()); + Assert.Equal("admin", actorObject.GetProperty("role").GetString()); TestUtilities.AssertFailIfErrors(context); } catch (Exception ex) @@ -73,6 +74,7 @@ public void ActorTokenInClaimsDictionaryShouldBeProperlySerialized() } } + [ResetAppContextSwitches] [Fact] public void ActorTokenAsSubjectShouldBeProperlySerialized() { @@ -108,16 +110,19 @@ public void ActorTokenAsSubjectShouldBeProperlySerialized() JsonWebToken decodedToken = tokenHandler.ReadJsonWebToken(token); // Verify actor claim exists in the token - Assert.True(decodedToken.Payload.HasClaim("act"), "JWT token should contain 'act' claim"); + Assert.True(decodedToken.Payload.HasClaim(tokenDescriptor.ActorClaimName), "JWT token should contain 'act' claim"); + + // Verify actor claim exists in the token + Assert.True(decodedToken.Payload.HasClaim(tokenDescriptor.ActorClaimName), "JWT token should contain actor claim"); - // Get the actor token and verify it contains the expected claims - var actorTokenString = decodedToken.Actor; - Assert.NotNull(actorTokenString); + // Verify the actor object directly + var actorObject = decodedToken.Payload.GetValue(tokenDescriptor.ActorClaimName); + Assert.Equal(JsonValueKind.Object, actorObject.ValueKind); - JsonWebToken actorJwt = tokenHandler.ReadJsonWebToken(actorTokenString); - Assert.Equal("actor-subject-id", actorJwt.Payload.GetValue("sub")); - Assert.Equal("Actor Name", actorJwt.Payload.GetValue("name")); - Assert.Equal("admin", actorJwt.Payload.GetValue("role")); + // Verify actor claims directly from the JSON object + Assert.Equal("actor-subject-id", actorObject.GetProperty("sub").GetString()); + Assert.Equal("Actor Name", actorObject.GetProperty("name").GetString()); + Assert.Equal("admin", actorObject.GetProperty("role").GetString()); TestUtilities.AssertFailIfErrors(context); } catch (Exception ex) @@ -130,11 +135,13 @@ public void ActorTokenAsSubjectShouldBeProperlySerialized() } } + [ResetAppContextSwitches] [Fact] public void ActorTokenInBothClaimsAndSubjectShouldPreferClaimsValue() { var context = new CompareContext($"{this}.ActorTokenInBothClaimsAndSubjectShouldPreferClaimsValue"); bool switchValue = false; + string actorname = "act"; AppContext.TryGetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, out switchValue); AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, true); try @@ -167,25 +174,23 @@ public void ActorTokenInBothClaimsAndSubjectShouldPreferClaimsValue() // Add Claims actor that should take precedence Claims = new Dictionary { - { "act", claimsActorIdentity } + { actorname, claimsActorIdentity } } }; var token = tokenHandler.CreateToken(tokenDescriptor); JsonWebToken decodedToken = tokenHandler.ReadJsonWebToken(token); // Verify actor claim exists - Assert.True(decodedToken.Payload.HasClaim("act"), "JWT token should contain 'actort' claim"); + Assert.True(decodedToken.Payload.HasClaim(actorname), "JWT token should contain actor claim"); - // Get the actor token and verify it contains the expected claims - var actorTokenString = decodedToken.Actor; - Assert.NotNull(actorTokenString); + // Verify actor claim exists and is a JSON object + var actorObject = decodedToken.Payload.GetValue("act"); + Assert.Equal(JsonValueKind.Object, actorObject.ValueKind); - JsonWebToken actorJwt = tokenHandler.ReadJsonWebToken(actorTokenString); - - // Verify the Claims dictionary actor was used, not the Subject.Actor - Assert.Equal("claims-actor-id", actorJwt.Payload.GetValue("sub")); - Assert.Equal("Claims Actor", actorJwt.Payload.GetValue("name")); - Assert.NotEqual("subject-actor-id", actorJwt.Payload.GetValue("sub")); + // Verify Claims dictionary actor was used, not Subject.Actor + Assert.Equal("claims-actor-id", actorObject.GetProperty("sub").GetString()); + Assert.Equal("Claims Actor", actorObject.GetProperty("name").GetString()); + Assert.NotEqual("subject-actor-id", actorObject.GetProperty("sub").GetString()); TestUtilities.AssertFailIfErrors(context); } catch (Exception ex) @@ -198,6 +203,7 @@ public void ActorTokenInBothClaimsAndSubjectShouldPreferClaimsValue() } } + [ResetAppContextSwitches] [Fact] public void NestedActorTokenInClaimsDictionaryShouldBeProperlySerialized() { @@ -243,26 +249,21 @@ public void NestedActorTokenInClaimsDictionaryShouldBeProperlySerialized() // Verify actor claim exists Assert.True(decodedToken.Payload.HasClaim("act"), "JWT token should contain 'actort' claim"); - // Read the main actor token - var actorTokenString = decodedToken.Actor; - Assert.NotNull(actorTokenString); - JsonWebToken actorJwt = tokenHandler.ReadJsonWebToken(actorTokenString); - - // Verify main actor claims - Assert.Equal("actor-subject-id", actorJwt.Payload.GetValue("sub")); - Assert.Equal("Actor Name", actorJwt.Payload.GetValue("name")); + // Verify the actor object + var actorObject = decodedToken.Payload.GetValue("act"); + Assert.Equal(JsonValueKind.Object, actorObject.ValueKind); - // Verify nested actor exists - Assert.True(actorJwt.Payload.HasClaim("act"), "Actor token should contain nested 'actort' claim"); + // Verify main actor claims directly from JSON object + Assert.Equal("actor-subject-id", actorObject.GetProperty("sub").GetString()); + Assert.Equal("Actor Name", actorObject.GetProperty("name").GetString()); - // Read the nested actor token - var nestedActorTokenString = actorJwt.Actor; - Assert.NotNull(nestedActorTokenString); - JsonWebToken nestedActorJwt = tokenHandler.ReadJsonWebToken(nestedActorTokenString); + // Verify nested actor exists and is a JSON object + Assert.True(actorObject.TryGetProperty(tokenDescriptor.ActorClaimName, out var nestedActorElement)); + Assert.Equal(JsonValueKind.Object, nestedActorElement.ValueKind); - // Verify nested actor claims - Assert.Equal("nested-actor-id", nestedActorJwt.Payload.GetValue("sub")); - Assert.Equal("Nested Actor", nestedActorJwt.Payload.GetValue("name")); + // Verify nested actor claims directly from JSON object + Assert.Equal("nested-actor-id", nestedActorElement.GetProperty("sub").GetString()); + Assert.Equal("Nested Actor", nestedActorElement.GetProperty("name").GetString()); TestUtilities.AssertFailIfErrors(context); } catch (Exception ex) @@ -274,7 +275,7 @@ public void NestedActorTokenInClaimsDictionaryShouldBeProperlySerialized() AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, false); } } - + [ResetAppContextSwitches] [Fact] public void NestedActorTokenAsSubjectShouldBeProperlySerialized() { @@ -309,40 +310,35 @@ public void NestedActorTokenAsSubjectShouldBeProperlySerialized() Audience = "https://api.example.com", Expires = DateTime.UtcNow.AddHours(1), SigningCredentials = Default.AsymmetricSigningCredentials, + ActorClaimName = "act", }; AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, true); var token = tokenHandler.CreateToken(tokenDescriptor); JsonWebToken decodedToken = tokenHandler.ReadJsonWebToken(token); // Verify actor claim exists - Assert.True(decodedToken.Payload.HasClaim("act"), "JWT token should contain 'actort' claim"); + Assert.True(decodedToken.Payload.HasClaim("act"), "JWT token should contain 'act' claim"); - // Read the main actor token - var actorTokenString = decodedToken.Actor; - Assert.NotNull(actorTokenString); - JsonWebToken actorJwt = tokenHandler.ReadJsonWebToken(actorTokenString); + // Verify the actor object structure + var actorObject = decodedToken.Payload.GetValue("act"); + Console.WriteLine("actor token created: " + actorObject.ToString()); - // Verify main actor claims - Assert.Equal("actor-subject-id", actorJwt.Payload.GetValue("sub")); - Assert.Equal("Actor Name", actorJwt.Payload.GetValue("name")); + Assert.Equal(JsonValueKind.Object, actorObject.ValueKind); - // Verify nested actor exists - Assert.True(actorJwt.Payload.HasClaim("act"), "Actor token should contain nested 'actort' claim"); + // Verify main actor claims directly from JSON object + Assert.Equal("actor-subject-id", actorObject.GetProperty("sub").GetString()); + Assert.Equal("Actor Name", actorObject.GetProperty("name").GetString()); - // Read the nested actor token - var nestedActorTokenString = actorJwt.Actor; - Assert.NotNull(nestedActorTokenString); - JsonWebToken nestedActorJwt = tokenHandler.ReadJsonWebToken(nestedActorTokenString); + // Verify nested actor exists and is a JSON object + Assert.True(actorObject.TryGetProperty(tokenDescriptor.ActorClaimName, out var nestedActorElement)); + Assert.Equal(JsonValueKind.Object, nestedActorElement.ValueKind); + Console.WriteLine("nested token created: " + nestedActorElement.ToString()); - // Verify nested actor claims - Assert.Equal("nested-actor-id", nestedActorJwt.Payload.GetValue("sub")); - Assert.Equal("Nested Actor", nestedActorJwt.Payload.GetValue("name")); + // Verify nested actor claims directly from JSON object + Assert.Equal("nested-actor-id", nestedActorElement.GetProperty("sub").GetString()); + Assert.Equal("Nested Actor", nestedActorElement.GetProperty("name").GetString()); TestUtilities.AssertFailIfErrors(context); } - catch (Exception ex) - { - context.Diffs.Add($"Exception: {ex}"); - } finally { AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, false); @@ -394,6 +390,7 @@ public void MaxActorChainLength_RejectsNegativeValues() } } + [ResetAppContextSwitches] [Fact] public void NestedSubjectActorTokens_ExceedingMaxDepth_ThrowsException() { @@ -581,22 +578,25 @@ public void ActorTokens_MixedSourceRespectMaxActorChainLength() var token = handler.CreateToken(tokenDescriptor); var jwtToken = handler.ReadJsonWebToken(token); - // Assert + // Assert - Check actor object structure Assert.True(jwtToken.Payload.HasClaim(actorname), "JWT token should contain 'act' claim"); + var actorObject = jwtToken.Payload.GetValue(tokenDescriptor.ActorClaimName); + + Assert.Equal(JsonValueKind.Object, actorObject.ValueKind); // Verify we get the actor from Claims dictionary (should be level2Actor) - var actorToken = handler.ReadJsonWebToken(jwtToken.Actor); - Assert.Equal("level2-actor", actorToken.Payload.GetValue("sub")); - Assert.Equal("Level 2 Actor", actorToken.Payload.GetValue("name")); + Assert.Equal("level2-actor", actorObject.GetProperty("sub").GetString()); + Assert.Equal("Level 2 Actor", actorObject.GetProperty("name").GetString()); // There should be no nested actor because we're already at max depth - Assert.False(actorToken.Payload.HasClaim("actort"), "There should be no nested actor claim due to MaxActorChainLength"); + Assert.False(actorObject.TryGetProperty("act", out _), "There should be no nested actor claim due to MaxActorChainLength"); } finally { AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, false); } } + [ResetAppContextSwitches] [Fact] public void NestedClaimTokens_ExceedingMaxDepth_ThrowsException() { @@ -667,123 +667,5 @@ public void NestedClaimTokens_ExceedingMaxDepth_ThrowsException() AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, false); } } - - [Fact] - public async Task ValidateActorToken_WithMaxChainLength_ValidatesSuccessfully() - { - var context = new CompareContext($"{this}.ValidateActorToken_WithMaxChainLength_ValidatesSuccessfully"); - bool switchValue = false; - AppContext.TryGetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, out switchValue); - AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, true); - try - { - // Create a token with nested actors - var handler = new JsonWebTokenHandler(); - string actorname = "actortoken"; - // Create level 3 actor (innermost) - var level3Actor = new CaseSensitiveClaimsIdentity("Level3Auth"); - level3Actor.AddClaim(new Claim("sub", "level3-actor")); - level3Actor.AddClaim(new Claim("name", "Level 3 Actor")); - level3Actor.AddClaim(new Claim("exp", EpochTime.GetIntDate(DateTime.UtcNow.AddHours(1)).ToString())); - - - // Create level 2 actor with level 3 as its actor - var level2Actor = new CaseSensitiveClaimsIdentity("Level2Auth"); - level2Actor.AddClaim(new Claim("sub", "level2-actor")); - level2Actor.AddClaim(new Claim("name", "Level 2 Actor")); - level2Actor.Actor = level3Actor; - level2Actor.AddClaim(new Claim("exp", EpochTime.GetIntDate(DateTime.UtcNow.AddHours(1)).ToString())); - - // Create level 1 actor with level 2 as its actor - var level1Actor = new CaseSensitiveClaimsIdentity("Level1Auth"); - level1Actor.AddClaim(new Claim("sub", "level1-actor")); - level1Actor.AddClaim(new Claim("name", "Level 1 Actor")); - level1Actor.Actor = level2Actor; - level1Actor.AddClaim(new Claim("exp", EpochTime.GetIntDate(DateTime.UtcNow.AddHours(1)).ToString())); - - // Create main identity with level 1 as its actor - var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); - mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); - mainIdentity.AddClaim(new Claim("name", "Main User")); - mainIdentity.Actor = level1Actor; - - // Define audience - string audience = "https://api.example.com"; - string issuer = "https://example.com"; - - // Create token with actor chain - var tokenDescriptor = new SecurityTokenDescriptor - { - Subject = mainIdentity, - Issuer = issuer, - Audience = audience, - SigningCredentials = Default.AsymmetricSigningCredentials, - ActorClaimName = actorname, - MaxActorChainLength = 3 - }; - var token = handler.CreateToken(tokenDescriptor); - - // Configure validation parameters - var validationParameters = Default.AsymmetricSignTokenValidationParameters; - validationParameters.ValidIssuer = issuer; - validationParameters.ValidAudience = audience; - validationParameters.ValidateActor = true; - validationParameters.MaxActorChainLength = 3; - validationParameters.ActorClaimName = actorname; - validationParameters.ActorValidationParameters = validationParameters.Clone(); - - // Create actor validation parameters - var actorValidationParameters = validationParameters.Clone(); - actorValidationParameters.RequireSignedTokens = false; - actorValidationParameters.ValidateLifetime = false; - actorValidationParameters.ValidateAudience = false; - actorValidationParameters.ValidateIssuer = false; - - validationParameters.ActorValidationParameters = actorValidationParameters; - // Validate token - var result = await handler.ValidateTokenAsync(token, validationParameters); - if (!result.IsValid) - { - Console.WriteLine($"Validation failed: {result.Exception?.Message}"); - } - Assert.True(result.IsValid, "Token should be valid"); - - // Get the main JsonWebToken from the result - var mainToken = result.SecurityToken as JsonWebToken; - Assert.NotNull(mainToken); - Assert.Equal("main-subject-id", mainToken.Subject); - Console.WriteLine($"Main User Subject: {mainToken.Subject}"); - - // Follow and verify actor chain using JsonWebToken.Actor and ReadJsonWebToken - var currentToken = mainToken; - var actorLevels = new[] { "level1-actor", "level2-actor", "level3-actor" }; - - for (int i = 0; i < actorLevels.Length; i++) - { - // Get actor JWT string and convert it to JsonWebToken - var actorTokenString = currentToken.Actor; - Assert.False(string.IsNullOrEmpty(actorTokenString), $"Actor token at level {i} should not be null or empty"); - Console.WriteLine($"Here is the token for {i} iteration : {actorTokenString}"); - // Parse the actor token string into a JsonWebToken - var actorToken = handler.ReadJsonWebToken(actorTokenString); - Assert.NotNull(actorToken); - actorToken.ActorClaimName = actorname; - // Verify actor token claims - Assert.Equal(actorLevels[i], actorToken.Subject); - Assert.NotNull(actorToken.GetPayloadValue("name")); - Console.WriteLine($"Actor {i + 1}: Subject={actorToken.Subject}, Name={actorToken.GetPayloadValue("name")}"); - - // Move to next actor in the chain - currentToken = actorToken; - } - // Verify no more actors beyond max depth - Assert.True(string.IsNullOrEmpty(currentToken.Actor), "There should be no more actors beyond the specified depth"); - TestUtilities.AssertFailIfErrors(context); - } - finally - { - AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, false); - } - } } } From f3c6ed212ba2cefa9f2ab605f1f52c9b101435c5 Mon Sep 17 00:00:00 2001 From: saurabhsathe-ms Date: Sun, 18 May 2025 12:00:28 -0700 Subject: [PATCH 32/52] Removed the changes to PublicUnshipped files for each frameworks --- .../PublicAPI.Unshipped.txt | 7 ++++++- .../PublicAPI/net462/PublicAPI.Unshipped.txt | 6 ------ .../PublicAPI/net472/PublicAPI.Unshipped.txt | 6 ------ .../PublicAPI/net6.0/PublicAPI.Unshipped.txt | 6 ------ .../PublicAPI/net8.0/PublicAPI.Unshipped.txt | 6 ------ .../PublicAPI/net9.0/PublicAPI.Unshipped.txt | 6 ------ .../netstandard2.0/PublicAPI.Unshipped.txt | 6 ------ .../TokenValidationParameters.cs | 20 +++++++++++++++++++ 8 files changed, 26 insertions(+), 37 deletions(-) diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI.Unshipped.txt index 66583ccb96..ae72888e41 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI.Unshipped.txt @@ -3,4 +3,9 @@ Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActorChainDepth.set -> Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActorClaimName.get -> string Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActorClaimName.set -> void Microsoft.IdentityModel.Tokens.TokenValidationParameters.MaxActorChainLength.get -> int -Microsoft.IdentityModel.Tokens.TokenValidationParameters.MaxActorChainLength.set -> void \ No newline at end of file +Microsoft.IdentityModel.Tokens.TokenValidationParameters.MaxActorChainLength.set -> void +Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActClaimValidationDelegate +Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActorTokenValidator.get -> Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActClaimValidationDelegate +Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActorTokenValidator.set -> void +Microsoft.IdentityModel.Tokens.TokenValidationParameters.ValidateActorToken.get -> bool +Microsoft.IdentityModel.Tokens.TokenValidationParameters.ValidateActorToken.set -> void diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net462/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net462/PublicAPI.Unshipped.txt index 0f71b7fc56..e69de29bb2 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net462/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net462/PublicAPI.Unshipped.txt @@ -1,6 +0,0 @@ -Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorChainDepth.get -> int -Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorChainDepth.set -> void -Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimName.get -> string -Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimName.set -> void -Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.get -> int -Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.set -> void diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net472/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net472/PublicAPI.Unshipped.txt index 0f71b7fc56..e69de29bb2 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net472/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net472/PublicAPI.Unshipped.txt @@ -1,6 +0,0 @@ -Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorChainDepth.get -> int -Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorChainDepth.set -> void -Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimName.get -> string -Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimName.set -> void -Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.get -> int -Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.set -> void diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net6.0/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net6.0/PublicAPI.Unshipped.txt index 0f71b7fc56..e69de29bb2 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net6.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net6.0/PublicAPI.Unshipped.txt @@ -1,6 +0,0 @@ -Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorChainDepth.get -> int -Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorChainDepth.set -> void -Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimName.get -> string -Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimName.set -> void -Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.get -> int -Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.set -> void diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net8.0/PublicAPI.Unshipped.txt index 0f71b7fc56..e69de29bb2 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net8.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net8.0/PublicAPI.Unshipped.txt @@ -1,6 +0,0 @@ -Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorChainDepth.get -> int -Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorChainDepth.set -> void -Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimName.get -> string -Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimName.set -> void -Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.get -> int -Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.set -> void diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net9.0/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net9.0/PublicAPI.Unshipped.txt index 0f71b7fc56..e69de29bb2 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net9.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net9.0/PublicAPI.Unshipped.txt @@ -1,6 +0,0 @@ -Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorChainDepth.get -> int -Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorChainDepth.set -> void -Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimName.get -> string -Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimName.set -> void -Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.get -> int -Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.set -> void diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt index 0f71b7fc56..e69de29bb2 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt @@ -1,6 +0,0 @@ -Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorChainDepth.get -> int -Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorChainDepth.set -> void -Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimName.get -> string -Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimName.set -> void -Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.get -> int -Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.set -> void diff --git a/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs b/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs index 38ab2f8e4b..5e7aa30b90 100644 --- a/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs +++ b/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Security.Claims; +using System.Text.Json; using Microsoft.IdentityModel.Abstractions; using Microsoft.IdentityModel.Logging; @@ -828,5 +829,24 @@ public int ActorChainDepth _actorClainDepth = value; } } + /// + /// Delegate to validate the 'act' claim and create actor's ClaimsIdentity. + /// + /// The JSON element representing the 'act' claim. + /// The actor token string. + /// The token validation parameters. + /// A ClaimsIdentity representing the actor. + public delegate ClaimsIdentity ActClaimValidationDelegate(JsonElement actClaim, string actorToken, TokenValidationParameters validationParameters); + + // Add these properties to the TokenValidationParameters class + /// + /// Gets or sets the delegate that will be used to validate the 'act' claim and create actor's ClaimsIdentity. + /// + public ActClaimValidationDelegate ActorTokenValidator { get; set; } + + /// + /// Gets or sets a boolean to determine if actor token ('actort') validation is enabled. + /// + public bool ValidateActorToken { get; set; } } } From 4185c6f1314c51067be0ec63eca8f8b63cef6c29 Mon Sep 17 00:00:00 2001 From: saurabhsathe-ms Date: Mon, 19 May 2025 14:18:20 -0700 Subject: [PATCH 33/52] Added a delegate that users can use to validate their token --- .../JsonWebTokenHandler.ValidateToken.cs | 17 ++ .../LogMessages.cs | 2 +- .../Delegates.cs | 8 + .../PublicAPI.Unshipped.txt | 16 +- .../TokenValidationParameters.cs | 22 +-- .../ActorClaimsTests.cs | 149 ++++++++++++++++++ .../ValidationDelegates.cs | 5 + .../TokenValidationParametersTests.cs | 8 +- 8 files changed, 203 insertions(+), 24 deletions(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs index 8e3110db4c..cd106f82e4 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs @@ -12,6 +12,7 @@ using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Telemetry; using TokenLogMessages = Microsoft.IdentityModel.Tokens.LogMessages; +using System.Text.Json; namespace Microsoft.IdentityModel.JsonWebTokens { @@ -593,7 +594,23 @@ internal async ValueTask ValidateTokenPayloadAsync( if (!tokenValidationResult.IsValid) return tokenValidationResult; } + if (AppContextSwitches.SerializeDeserializeActorClaim && jsonWebToken.TryGetPayloadValue(validationParameters.ActorClaimName, out var actClaim)) + { + if (validationParameters.ActorTokenValidationDelegate != null) + { + // Fix for CS1929: The issue occurs because `ConfigureAwait` is being called on a `TokenValidationResult` object, which is not a `Task` or `ValueTask`. + // The `ConfigureAwait` method is only valid for `Task` or `ValueTask` types. + // To fix this, remove the `ConfigureAwait(false)` call from the `ActorTokenValidator` invocation. + var actorTokenValidationResult = validationParameters.ActorTokenValidationDelegate(actClaim, validationParameters.ActorValidationParameters); + if (!actorTokenValidationResult.IsValid) + return actorTokenValidationResult; + } + else + { + throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant(LogMessages.IDX14115))); + } + } string tokenType = Validators.ValidateTokenType(jsonWebToken.Typ, jsonWebToken, validationParameters); return new TokenValidationResult(jsonWebToken, this, validationParameters.Clone(), issuer, null) { diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/LogMessages.cs b/src/Microsoft.IdentityModel.JsonWebTokens/LogMessages.cs index 79333eaa37..6c0abfe03e 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/LogMessages.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/LogMessages.cs @@ -26,7 +26,7 @@ internal static class LogMessages internal const string IDX14112 = "IDX14112: Only a single 'Actor' is supported. Found second claim of type: '{0}'"; internal const string IDX14113 = "IDX14113: A duplicate value for 'SecurityTokenDescriptor.{0}' exists in 'SecurityTokenDescriptor.Claims'. \nThe value of 'SecurityTokenDescriptor.{0}' is used."; internal const string IDX14114 = "IDX14114: Both '{0}.{1}' and '{0}.{2}' are null or empty."; - // internal const string IDX14115 = "IDX14115:"; + internal const string IDX14115 = "IDX14115: Unable to validate Actor token as act claim. Act claim validation is enabled but ActorTokenValidationDelegate was not provided"; internal const string IDX14116 = "IDX14116: '{0}' cannot contain the following claims: '{1}'. These values are added by default (if necessary) during security token creation."; // number of sections 'dots' is not correct internal const string IDX14120 = "IDX14120: JWT is not well formed, there is only one dot (.).\nThe token needs to be in JWS or JWE Compact Serialization Format. (JWS): 'EncodedHeader.EncodedPayload.EncodedSignature'. (JWE): 'EncodedProtectedHeader.EncodedEncryptedKey.EncodedInitializationVector.EncodedCiphertext.EncodedAuthenticationTag'."; diff --git a/src/Microsoft.IdentityModel.Tokens/Delegates.cs b/src/Microsoft.IdentityModel.Tokens/Delegates.cs index 16a564d04a..7818a8179a 100644 --- a/src/Microsoft.IdentityModel.Tokens/Delegates.cs +++ b/src/Microsoft.IdentityModel.Tokens/Delegates.cs @@ -235,4 +235,12 @@ internal delegate ValidationResult SignatureValidationDelegate( /// The claim value that was read and parsed from the reader. /// True, if the claim value was read successfully; false otherwise. public delegate bool TryReadJwtClaim(ref Utf8JsonReader reader, JwtSegmentType jwtSegmentType, string claimName, out object claimValue); + + /// + /// Delegate to validate the 'act' claim and create actor's ClaimsIdentity. + /// + /// The JSON element representing the 'act' claim. + /// The token validation parameters. + /// A ClaimsIdentity representing the actor. + public delegate TokenValidationResult ActorTokenValidationDelegate(JsonElement actClaim, TokenValidationParameters validationParameters = null); } diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI.Unshipped.txt index ae72888e41..7441a3f93a 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI.Unshipped.txt @@ -4,8 +4,14 @@ Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActorClaimName.get -> s Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActorClaimName.set -> void Microsoft.IdentityModel.Tokens.TokenValidationParameters.MaxActorChainLength.get -> int Microsoft.IdentityModel.Tokens.TokenValidationParameters.MaxActorChainLength.set -> void -Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActClaimValidationDelegate -Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActorTokenValidator.get -> Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActClaimValidationDelegate -Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActorTokenValidator.set -> void -Microsoft.IdentityModel.Tokens.TokenValidationParameters.ValidateActorToken.get -> bool -Microsoft.IdentityModel.Tokens.TokenValidationParameters.ValidateActorToken.set -> void +Microsoft.IdentityModel.Tokens.ActorTokenValidationDelegate +Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActorTokenValidationDelegate.get -> Microsoft.IdentityModel.Tokens.ActorTokenValidationDelegate +Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActorTokenValidationDelegate.set -> void +Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActorTokenValidationParameters.get -> Microsoft.IdentityModel.Tokens.TokenValidationParameters +Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActorTokenValidationParameters.set -> void +Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorChainDepth.get -> int +Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorChainDepth.set -> void +Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimName.get -> string +Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimName.set -> void +Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.get -> int +Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.set -> void diff --git a/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs b/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs index 5e7aa30b90..958680060c 100644 --- a/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs +++ b/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.ComponentModel; using System.Security.Claims; -using System.Text.Json; using Microsoft.IdentityModel.Abstractions; using Microsoft.IdentityModel.Logging; @@ -211,7 +210,8 @@ public virtual TokenValidationParameters Clone() { return new(this) { - IsClone = true + IsClone = true, + ActorTokenValidationDelegate = this.ActorTokenValidationDelegate }; } @@ -829,24 +829,18 @@ public int ActorChainDepth _actorClainDepth = value; } } - /// - /// Delegate to validate the 'act' claim and create actor's ClaimsIdentity. - /// - /// The JSON element representing the 'act' claim. - /// The actor token string. - /// The token validation parameters. - /// A ClaimsIdentity representing the actor. - public delegate ClaimsIdentity ActClaimValidationDelegate(JsonElement actClaim, string actorToken, TokenValidationParameters validationParameters); - // Add these properties to the TokenValidationParameters class /// /// Gets or sets the delegate that will be used to validate the 'act' claim and create actor's ClaimsIdentity. /// - public ActClaimValidationDelegate ActorTokenValidator { get; set; } + public ActorTokenValidationDelegate ActorTokenValidationDelegate { get; set; } /// - /// Gets or sets a boolean to determine if actor token ('actort') validation is enabled. + /// Gets or sets the used to validate the actor claim. /// - public bool ValidateActorToken { get; set; } + /// + /// This property allows specifying custom validation parameters for the actor claim. + /// + public TokenValidationParameters ActorTokenValidationParameters { get; set; } } } diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs index 4ad40a6531..e3b1f87e9c 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs @@ -9,6 +9,7 @@ using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.JsonWebTokens; using Xunit; +using System.Threading.Tasks; namespace Microsoft.IdentityModel.Tests { public class ActorClaimsTests @@ -667,5 +668,153 @@ public void NestedClaimTokens_ExceedingMaxDepth_ThrowsException() AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, false); } } + + [ResetAppContextSwitches] + [Fact] + public async Task ActorValidatorShouldBeInvokedAndRespected() + { + var context = new CompareContext($"{this}.ActorValidatorShouldBeInvokedAndRespected"); + bool switchValue = false; + AppContext.TryGetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, out switchValue); + AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, true); + try + { + // Create actor identity + var actorIdentity = new CaseSensitiveClaimsIdentity("ActorAuth"); + actorIdentity.AddClaim(new Claim("sub", "actor-subject-id")); + actorIdentity.AddClaim(new Claim("name", "Actor Name")); + actorIdentity.AddClaim(new Claim("role", "admin")); + + // Create the main identity with Actor set + var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); + mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); + mainIdentity.AddClaim(new Claim("name", "Main User")); + mainIdentity.Actor = actorIdentity; + + // Create and sign a token + var tokenHandler = new JsonWebTokenHandler(); + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = mainIdentity, + Issuer = "https://example.com", + Audience = "https://api.example.com", + Expires = DateTime.UtcNow.AddHours(1), + SigningCredentials = Default.AsymmetricSigningCredentials + }; + var token = tokenHandler.CreateToken(tokenDescriptor); + + // Test Case 1: Successful actor validation + { + bool validatorCalled = false; + + // Create validation parameters with a custom actor validator + var validationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidIssuer = "https://example.com", + ValidateAudience = true, + ValidAudience = "https://api.example.com", + ValidateLifetime = true, + IssuerSigningKey = Default.AsymmetricSigningCredentials.Key, + ValidateIssuerSigningKey = true, + ActorValidationParameters = new TokenValidationParameters + { + ValidateIssuer = false, + ValidateAudience = false + }, + ActorTokenValidationDelegate = (actorElement, actorValidationParams) => + { + validatorCalled = true; + + // Verify the actor element structure + Assert.Equal("actor-subject-id", actorElement.GetProperty("sub").GetString()); + Assert.Equal("Actor Name", actorElement.GetProperty("name").GetString()); + Assert.Equal("admin", actorElement.GetProperty("role").GetString()); + + return new TokenValidationResult { IsValid = true }; + } + }; + + // Validate the token - should succeed + var result = await tokenHandler.ValidateTokenAsync(token, validationParameters); + + // Assert that validation succeeded and our validator was called + Assert.True(result.IsValid, "Token validation should succeed"); + Assert.True(validatorCalled, "Actor validator should be called"); + } + + // Test Case 2: Failing actor validation + { + bool validatorCalled = false; + string expectedErrorMessage = "IDX14115"; + + var validationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidIssuer = "https://example.com", + ValidateAudience = true, + ValidAudience = "https://api.example.com", + ValidateLifetime = true, + IssuerSigningKey = Default.AsymmetricSigningCredentials.Key, + ValidateIssuerSigningKey = true, + ActorTokenValidationDelegate = (actorElement, actorValidationParams) => + { + validatorCalled = true; + + // Return failed validation + return new TokenValidationResult + { + IsValid = false, + Exception = new SecurityTokenValidationException(expectedErrorMessage) + }; + } + }; + + // Validate the token - should fail + var result = await tokenHandler.ValidateTokenAsync(token, validationParameters); + + // Assert that validation failed and our validator was called + Assert.False(result.IsValid, "Token validation should fail"); + Assert.True(validatorCalled, "Actor validator should be called"); + Assert.NotNull(result.Exception); + Assert.Contains(expectedErrorMessage, result.Exception.Message); + } + + // Test Case 3: Missing actor validator throws exception + { + var validationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidIssuer = "https://example.com", + ValidateAudience = true, + ValidAudience = "https://api.example.com", + ValidateLifetime = true, + IssuerSigningKey = Default.AsymmetricSigningCredentials.Key, + ValidateIssuerSigningKey = true, + ActorTokenValidationDelegate = null + }; + + // Validate the token - should throw exception + var result = await tokenHandler.ValidateTokenAsync(token, validationParameters); + + Assert.False(result.IsValid); + Assert.NotNull(result.Exception); + Assert.True(result.Exception is SecurityTokenInvalidSignatureException, + $"Expected SecurityTokenInvalidSignatureException but got {result.Exception.GetType().Name}"); + Assert.Contains("IDX14115", result.Exception.Message); + } + + TestUtilities.AssertFailIfErrors(context); + } + catch (Exception ex) + { + context.Diffs.Add($"Exception: {ex}"); + TestUtilities.AssertFailIfErrors(context); + } + finally + { + AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, false); + } + } } } diff --git a/test/Microsoft.IdentityModel.TestUtils/ValidationDelegates.cs b/test/Microsoft.IdentityModel.TestUtils/ValidationDelegates.cs index 2ca873b58b..845e958a12 100644 --- a/test/Microsoft.IdentityModel.TestUtils/ValidationDelegates.cs +++ b/test/Microsoft.IdentityModel.TestUtils/ValidationDelegates.cs @@ -250,5 +250,10 @@ public static string TypeValidator(string type, SecurityToken securityToken, Tok { return type; } + + public static readonly ActorTokenValidationDelegate ActorTokenValidationDelegate = (actorClaim, validationParameters) => + { + return new TokenValidationResult { IsValid = true }; + }; } } diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/TokenValidationParametersTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/TokenValidationParametersTests.cs index 436eef9f5f..3dcbd0a85a 100644 --- a/test/Microsoft.IdentityModel.Tokens.Tests/TokenValidationParametersTests.cs +++ b/test/Microsoft.IdentityModel.Tokens.Tests/TokenValidationParametersTests.cs @@ -15,7 +15,7 @@ namespace Microsoft.IdentityModel.Tokens.Tests { public class TokenValidationParametersTests { - int ExpectedPropertyCount = 65; + int ExpectedPropertyCount = 67; // GetSets() compares the total property count which includes internal properties, against a list of public properties, minus delegates. // This allows us to keep track of any properties we are including in the total that are not public nor delegates. @@ -198,9 +198,10 @@ public void GetSets() { PropertyNamesAndSetGetValue = new List>> { - new KeyValuePair>("ActorValidationParameters", new List{(TokenValidationParameters)null, new TokenValidationParameters(), new TokenValidationParameters()}), new KeyValuePair>("ActorClaimName", new List{"actort"}), new KeyValuePair>("ActorChainDepth", new List{0,1}), + new KeyValuePair>("ActorTokenValidationParameters", new List{(TokenValidationParameters)null, new TokenValidationParameters(), new TokenValidationParameters()}), + new KeyValuePair>("ActorValidationParameters", new List{(TokenValidationParameters)null, new TokenValidationParameters(), new TokenValidationParameters()}), new KeyValuePair>("AuthenticationType", new List{(string)null, Guid.NewGuid().ToString(), Guid.NewGuid().ToString()}), new KeyValuePair>("ClockSkew", new List{TokenValidationParameters.DefaultClockSkew, TimeSpan.FromHours(2), TimeSpan.FromMinutes(1)}), new KeyValuePair>("ConfigurationManager", new List{(BaseConfigurationManager)null, new ConfigurationManager("http://127.0.0.1", new OpenIdConnectConfigurationRetriever()), new ConfigurationManager("http://127.0.0.1", new WsFederationConfigurationRetriever()) }), @@ -287,7 +288,6 @@ public void Clone() if (validationParametersClone.InstancePropertyBag.Count != 0) compareContext.AddDiff("validationParametersClone.InstancePropertyBag.Count != 0), should be empty."); - TestUtilities.AssertFailIfErrors(compareContext); } @@ -315,6 +315,7 @@ private TokenValidationParameters CreateTokenValidationParameters() validationParameters.TransformBeforeSignatureValidation = ValidationDelegates.TransformBeforeSignatureValidation; validationParameters.TryReadJwtClaim = ValidationDelegates.TryReadJwtClaim; validationParameters.TypeValidator = ValidationDelegates.TypeValidator; + validationParameters.ActorTokenValidationDelegate = ValidationDelegates.ActorTokenValidationDelegate; validationParameters.ActorValidationParameters = new TokenValidationParameters(); validationParameters.ClockSkew = TimeSpan.FromSeconds(42); @@ -355,7 +356,6 @@ private TokenValidationParameters CreateTokenValidationParameters() validationParameters.ValidateSignatureLast = !validationParametersDefault.ValidateSignatureLast; validationParameters.ValidateWithLKG = !validationParametersDefault.ValidateWithLKG; validationParameters.ValidateTokenReplay = !validationParametersDefault.ValidateTokenReplay; - return validationParameters; } From 4c57aaf34060945980ca45aa662d121870d83f33 Mon Sep 17 00:00:00 2001 From: saurabhsathe-ms Date: Mon, 19 May 2025 14:30:23 -0700 Subject: [PATCH 34/52] NIT repairs round 1 --- .../JsonWebTokenHandler.CreateToken.cs | 1 + .../JsonWebTokenHandler.ValidateToken.cs | 2 ++ .../TokenValidationParametersTests.cs | 2 ++ 3 files changed, 5 insertions(+) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs index c41afc8bc9..8a53414fa1 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs @@ -755,6 +755,7 @@ internal static void WriteJwsPayload( nbfSet = true; } + JsonPrimitives.WriteObject(ref writer, kvp.Key, kvp.Value); } } diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs index cd106f82e4..a18adf5805 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs @@ -453,6 +453,7 @@ public override async Task ValidateTokenAsync(SecurityTok var jwt = token as JsonWebToken; if (jwt == null) return new TokenValidationResult { Exception = LogHelper.LogArgumentException(nameof(token), $"{nameof(token)} must be a {nameof(JsonWebToken)}."), IsValid = false }; + try { return await ValidateTokenAsync(jwt, validationParameters).ConfigureAwait(false); @@ -496,6 +497,7 @@ internal async ValueTask ValidateTokenAsync( LogHelper.LogWarning(LogHelper.FormatInvariant(TokenLogMessages.IDX10261, validationParameters.ConfigurationManager.MetadataAddress, ex.ToString())); } } + TokenValidationResult tokenValidationResult = jsonWebToken.IsEncrypted ? await ValidateJWEAsync(jsonWebToken, validationParameters, currentConfiguration).ConfigureAwait(false) : await ValidateJWSAsync(jsonWebToken, validationParameters, currentConfiguration).ConfigureAwait(false); diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/TokenValidationParametersTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/TokenValidationParametersTests.cs index 3dcbd0a85a..097ddb3a33 100644 --- a/test/Microsoft.IdentityModel.Tokens.Tests/TokenValidationParametersTests.cs +++ b/test/Microsoft.IdentityModel.Tokens.Tests/TokenValidationParametersTests.cs @@ -288,6 +288,7 @@ public void Clone() if (validationParametersClone.InstancePropertyBag.Count != 0) compareContext.AddDiff("validationParametersClone.InstancePropertyBag.Count != 0), should be empty."); + TestUtilities.AssertFailIfErrors(compareContext); } @@ -356,6 +357,7 @@ private TokenValidationParameters CreateTokenValidationParameters() validationParameters.ValidateSignatureLast = !validationParametersDefault.ValidateSignatureLast; validationParameters.ValidateWithLKG = !validationParametersDefault.ValidateWithLKG; validationParameters.ValidateTokenReplay = !validationParametersDefault.ValidateTokenReplay; + return validationParameters; } From a5851123e5cdcee48f976a0eb20ea9fc851af5cd Mon Sep 17 00:00:00 2001 From: saurabhsathe-ms Date: Tue, 20 May 2025 12:11:52 -0700 Subject: [PATCH 35/52] Removed everything from TokenValidationParameters and the delegate --- .../JsonWebTokenHandler.ValidateToken.cs | 17 -- .../LogMessages.cs | 2 +- .../Delegates.cs | 4 +- .../PublicAPI.Unshipped.txt | 12 +- .../TokenValidationParameters.cs | 79 ---------- .../ActorClaimsTests.cs | 149 +----------------- .../ValidationDelegates.cs | 4 - .../TokenValidationParametersTests.cs | 8 +- 8 files changed, 6 insertions(+), 269 deletions(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs index a18adf5805..115e597b87 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs @@ -12,7 +12,6 @@ using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Telemetry; using TokenLogMessages = Microsoft.IdentityModel.Tokens.LogMessages; -using System.Text.Json; namespace Microsoft.IdentityModel.JsonWebTokens { @@ -596,23 +595,7 @@ internal async ValueTask ValidateTokenPayloadAsync( if (!tokenValidationResult.IsValid) return tokenValidationResult; } - if (AppContextSwitches.SerializeDeserializeActorClaim && jsonWebToken.TryGetPayloadValue(validationParameters.ActorClaimName, out var actClaim)) - { - if (validationParameters.ActorTokenValidationDelegate != null) - { - // Fix for CS1929: The issue occurs because `ConfigureAwait` is being called on a `TokenValidationResult` object, which is not a `Task` or `ValueTask`. - // The `ConfigureAwait` method is only valid for `Task` or `ValueTask` types. - // To fix this, remove the `ConfigureAwait(false)` call from the `ActorTokenValidator` invocation. - var actorTokenValidationResult = validationParameters.ActorTokenValidationDelegate(actClaim, validationParameters.ActorValidationParameters); - if (!actorTokenValidationResult.IsValid) - return actorTokenValidationResult; - } - else - { - throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant(LogMessages.IDX14115))); - } - } string tokenType = Validators.ValidateTokenType(jsonWebToken.Typ, jsonWebToken, validationParameters); return new TokenValidationResult(jsonWebToken, this, validationParameters.Clone(), issuer, null) { diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/LogMessages.cs b/src/Microsoft.IdentityModel.JsonWebTokens/LogMessages.cs index 6c0abfe03e..79333eaa37 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/LogMessages.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/LogMessages.cs @@ -26,7 +26,7 @@ internal static class LogMessages internal const string IDX14112 = "IDX14112: Only a single 'Actor' is supported. Found second claim of type: '{0}'"; internal const string IDX14113 = "IDX14113: A duplicate value for 'SecurityTokenDescriptor.{0}' exists in 'SecurityTokenDescriptor.Claims'. \nThe value of 'SecurityTokenDescriptor.{0}' is used."; internal const string IDX14114 = "IDX14114: Both '{0}.{1}' and '{0}.{2}' are null or empty."; - internal const string IDX14115 = "IDX14115: Unable to validate Actor token as act claim. Act claim validation is enabled but ActorTokenValidationDelegate was not provided"; + // internal const string IDX14115 = "IDX14115:"; internal const string IDX14116 = "IDX14116: '{0}' cannot contain the following claims: '{1}'. These values are added by default (if necessary) during security token creation."; // number of sections 'dots' is not correct internal const string IDX14120 = "IDX14120: JWT is not well formed, there is only one dot (.).\nThe token needs to be in JWS or JWE Compact Serialization Format. (JWS): 'EncodedHeader.EncodedPayload.EncodedSignature'. (JWE): 'EncodedProtectedHeader.EncodedEncryptedKey.EncodedInitializationVector.EncodedCiphertext.EncodedAuthenticationTag'."; diff --git a/src/Microsoft.IdentityModel.Tokens/Delegates.cs b/src/Microsoft.IdentityModel.Tokens/Delegates.cs index 7818a8179a..9fe7e4a2cb 100644 --- a/src/Microsoft.IdentityModel.Tokens/Delegates.cs +++ b/src/Microsoft.IdentityModel.Tokens/Delegates.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Security.Claims; using System.Text.Json; using System.Threading.Tasks; @@ -240,7 +241,6 @@ internal delegate ValidationResult SignatureValidationDelegate( /// Delegate to validate the 'act' claim and create actor's ClaimsIdentity. /// /// The JSON element representing the 'act' claim. - /// The token validation parameters. /// A ClaimsIdentity representing the actor. - public delegate TokenValidationResult ActorTokenValidationDelegate(JsonElement actClaim, TokenValidationParameters validationParameters = null); + public delegate ClaimsIdentity ActClaimRetrieverDelegate(JsonElement actClaim); } diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI.Unshipped.txt index 7441a3f93a..d9fe34b816 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI.Unshipped.txt @@ -1,17 +1,7 @@ -Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActorChainDepth.get -> int -Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActorChainDepth.set -> void -Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActorClaimName.get -> string -Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActorClaimName.set -> void -Microsoft.IdentityModel.Tokens.TokenValidationParameters.MaxActorChainLength.get -> int -Microsoft.IdentityModel.Tokens.TokenValidationParameters.MaxActorChainLength.set -> void -Microsoft.IdentityModel.Tokens.ActorTokenValidationDelegate -Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActorTokenValidationDelegate.get -> Microsoft.IdentityModel.Tokens.ActorTokenValidationDelegate -Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActorTokenValidationDelegate.set -> void -Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActorTokenValidationParameters.get -> Microsoft.IdentityModel.Tokens.TokenValidationParameters -Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActorTokenValidationParameters.set -> void Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorChainDepth.get -> int Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorChainDepth.set -> void Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimName.get -> string Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimName.set -> void Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.get -> int Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.set -> void +Microsoft.IdentityModel.Tokens.ActClaimRetrieverDelegate diff --git a/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs b/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs index 958680060c..184733eb34 100644 --- a/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs +++ b/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs @@ -211,7 +211,6 @@ public virtual TokenValidationParameters Clone() return new(this) { IsClone = true, - ActorTokenValidationDelegate = this.ActorTokenValidationDelegate }; } @@ -764,83 +763,5 @@ public string RoleClaimType /// public IEnumerable ValidTypes { get; set; } - - private int maxActorChainLength = 5; - /// - /// Gets or sets the maximum depth allowed when processing nested actor tokens. - /// This prevents excessive recursion when handling deeply nested actor tokens. - /// The value must be at least 0. Value 0 would mean that the actor token is not allowed to be nested. - /// Default value is 5. Max value is also 5 - /// - /// Thrown if the value is less than 0. - public int MaxActorChainLength - { - get => maxActorChainLength; - set - { - if (value < 0 || value > 5) - throw LogHelper.LogExceptionMessage( - new ArgumentOutOfRangeException( - LogHelper.FormatInvariant( - LogMessages.IDX11027, - LogHelper.MarkAsNonPII("MaxActorChainLength")) - + ". Permissible values are integers in range 0 to 5")); - - maxActorChainLength = value; - } - } - - private string actorClaimName = "act"; - /// - /// Gets or sets the claim type name for the actor claim. - /// Permissible values are 'act' or 'actort'. - /// - /// - /// Thrown if the value is null. - /// - /// - /// Thrown if the value is not 'act' or 'actort'. - /// - public string ActorClaimName - { - get => AppContextSwitches.SerializeDeserializeActorClaim ? actorClaimName : "actort"; - set - { - if (string.IsNullOrEmpty(value)) - throw LogHelper.LogExceptionMessage( - new ArgumentOutOfRangeException( - LogHelper.FormatInvariant( - LogMessages.IDX11027, - LogHelper.MarkAsNonPII("ActorClaimName")) - + ". ValidationParameters.ActorClaimName cannot be set to empty.")); - actorClaimName = value; - } - } - private int _actorClainDepth; - /// - /// Gets or sets the depth of the actor chain. - /// This value determines the maximum depth of nested actor tokens that can be processed. - /// - public int ActorChainDepth - { - get => _actorClainDepth; - set - { - _actorClainDepth = value; - } - } - - /// - /// Gets or sets the delegate that will be used to validate the 'act' claim and create actor's ClaimsIdentity. - /// - public ActorTokenValidationDelegate ActorTokenValidationDelegate { get; set; } - - /// - /// Gets or sets the used to validate the actor claim. - /// - /// - /// This property allows specifying custom validation parameters for the actor claim. - /// - public TokenValidationParameters ActorTokenValidationParameters { get; set; } } } diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs index e3b1f87e9c..29a809c8b6 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs @@ -9,7 +9,7 @@ using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.JsonWebTokens; using Xunit; -using System.Threading.Tasks; + namespace Microsoft.IdentityModel.Tests { public class ActorClaimsTests @@ -669,152 +669,5 @@ public void NestedClaimTokens_ExceedingMaxDepth_ThrowsException() } } - [ResetAppContextSwitches] - [Fact] - public async Task ActorValidatorShouldBeInvokedAndRespected() - { - var context = new CompareContext($"{this}.ActorValidatorShouldBeInvokedAndRespected"); - bool switchValue = false; - AppContext.TryGetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, out switchValue); - AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, true); - try - { - // Create actor identity - var actorIdentity = new CaseSensitiveClaimsIdentity("ActorAuth"); - actorIdentity.AddClaim(new Claim("sub", "actor-subject-id")); - actorIdentity.AddClaim(new Claim("name", "Actor Name")); - actorIdentity.AddClaim(new Claim("role", "admin")); - - // Create the main identity with Actor set - var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); - mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); - mainIdentity.AddClaim(new Claim("name", "Main User")); - mainIdentity.Actor = actorIdentity; - - // Create and sign a token - var tokenHandler = new JsonWebTokenHandler(); - var tokenDescriptor = new SecurityTokenDescriptor - { - Subject = mainIdentity, - Issuer = "https://example.com", - Audience = "https://api.example.com", - Expires = DateTime.UtcNow.AddHours(1), - SigningCredentials = Default.AsymmetricSigningCredentials - }; - var token = tokenHandler.CreateToken(tokenDescriptor); - - // Test Case 1: Successful actor validation - { - bool validatorCalled = false; - - // Create validation parameters with a custom actor validator - var validationParameters = new TokenValidationParameters - { - ValidateIssuer = true, - ValidIssuer = "https://example.com", - ValidateAudience = true, - ValidAudience = "https://api.example.com", - ValidateLifetime = true, - IssuerSigningKey = Default.AsymmetricSigningCredentials.Key, - ValidateIssuerSigningKey = true, - ActorValidationParameters = new TokenValidationParameters - { - ValidateIssuer = false, - ValidateAudience = false - }, - ActorTokenValidationDelegate = (actorElement, actorValidationParams) => - { - validatorCalled = true; - - // Verify the actor element structure - Assert.Equal("actor-subject-id", actorElement.GetProperty("sub").GetString()); - Assert.Equal("Actor Name", actorElement.GetProperty("name").GetString()); - Assert.Equal("admin", actorElement.GetProperty("role").GetString()); - - return new TokenValidationResult { IsValid = true }; - } - }; - - // Validate the token - should succeed - var result = await tokenHandler.ValidateTokenAsync(token, validationParameters); - - // Assert that validation succeeded and our validator was called - Assert.True(result.IsValid, "Token validation should succeed"); - Assert.True(validatorCalled, "Actor validator should be called"); - } - - // Test Case 2: Failing actor validation - { - bool validatorCalled = false; - string expectedErrorMessage = "IDX14115"; - - var validationParameters = new TokenValidationParameters - { - ValidateIssuer = true, - ValidIssuer = "https://example.com", - ValidateAudience = true, - ValidAudience = "https://api.example.com", - ValidateLifetime = true, - IssuerSigningKey = Default.AsymmetricSigningCredentials.Key, - ValidateIssuerSigningKey = true, - ActorTokenValidationDelegate = (actorElement, actorValidationParams) => - { - validatorCalled = true; - - // Return failed validation - return new TokenValidationResult - { - IsValid = false, - Exception = new SecurityTokenValidationException(expectedErrorMessage) - }; - } - }; - - // Validate the token - should fail - var result = await tokenHandler.ValidateTokenAsync(token, validationParameters); - - // Assert that validation failed and our validator was called - Assert.False(result.IsValid, "Token validation should fail"); - Assert.True(validatorCalled, "Actor validator should be called"); - Assert.NotNull(result.Exception); - Assert.Contains(expectedErrorMessage, result.Exception.Message); - } - - // Test Case 3: Missing actor validator throws exception - { - var validationParameters = new TokenValidationParameters - { - ValidateIssuer = true, - ValidIssuer = "https://example.com", - ValidateAudience = true, - ValidAudience = "https://api.example.com", - ValidateLifetime = true, - IssuerSigningKey = Default.AsymmetricSigningCredentials.Key, - ValidateIssuerSigningKey = true, - ActorTokenValidationDelegate = null - }; - - // Validate the token - should throw exception - var result = await tokenHandler.ValidateTokenAsync(token, validationParameters); - - Assert.False(result.IsValid); - Assert.NotNull(result.Exception); - Assert.True(result.Exception is SecurityTokenInvalidSignatureException, - $"Expected SecurityTokenInvalidSignatureException but got {result.Exception.GetType().Name}"); - Assert.Contains("IDX14115", result.Exception.Message); - } - - TestUtilities.AssertFailIfErrors(context); - } - catch (Exception ex) - { - context.Diffs.Add($"Exception: {ex}"); - TestUtilities.AssertFailIfErrors(context); - } - finally - { - AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, false); - } - } } } diff --git a/test/Microsoft.IdentityModel.TestUtils/ValidationDelegates.cs b/test/Microsoft.IdentityModel.TestUtils/ValidationDelegates.cs index 845e958a12..50a8b9ee40 100644 --- a/test/Microsoft.IdentityModel.TestUtils/ValidationDelegates.cs +++ b/test/Microsoft.IdentityModel.TestUtils/ValidationDelegates.cs @@ -251,9 +251,5 @@ public static string TypeValidator(string type, SecurityToken securityToken, Tok return type; } - public static readonly ActorTokenValidationDelegate ActorTokenValidationDelegate = (actorClaim, validationParameters) => - { - return new TokenValidationResult { IsValid = true }; - }; } } diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/TokenValidationParametersTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/TokenValidationParametersTests.cs index 097ddb3a33..5439499477 100644 --- a/test/Microsoft.IdentityModel.Tokens.Tests/TokenValidationParametersTests.cs +++ b/test/Microsoft.IdentityModel.Tokens.Tests/TokenValidationParametersTests.cs @@ -15,7 +15,7 @@ namespace Microsoft.IdentityModel.Tokens.Tests { public class TokenValidationParametersTests { - int ExpectedPropertyCount = 67; + int ExpectedPropertyCount = 62; // GetSets() compares the total property count which includes internal properties, against a list of public properties, minus delegates. // This allows us to keep track of any properties we are including in the total that are not public nor delegates. @@ -198,9 +198,6 @@ public void GetSets() { PropertyNamesAndSetGetValue = new List>> { - new KeyValuePair>("ActorClaimName", new List{"actort"}), - new KeyValuePair>("ActorChainDepth", new List{0,1}), - new KeyValuePair>("ActorTokenValidationParameters", new List{(TokenValidationParameters)null, new TokenValidationParameters(), new TokenValidationParameters()}), new KeyValuePair>("ActorValidationParameters", new List{(TokenValidationParameters)null, new TokenValidationParameters(), new TokenValidationParameters()}), new KeyValuePair>("AuthenticationType", new List{(string)null, Guid.NewGuid().ToString(), Guid.NewGuid().ToString()}), new KeyValuePair>("ClockSkew", new List{TokenValidationParameters.DefaultClockSkew, TimeSpan.FromHours(2), TimeSpan.FromMinutes(1)}), @@ -215,7 +212,6 @@ public void GetSets() new KeyValuePair>("IssuerSigningKeys", new List{(IEnumerable)null, new List{KeyingMaterial.DefaultX509Key_2048, KeyingMaterial.RsaSecurityKey_1024}, new List()}), new KeyValuePair>("LogTokenId", new List{true, false, true}), new KeyValuePair>("LogValidationExceptions", new List{true, false, true}), - new KeyValuePair>("MaxActorChainLength", new List{5,2}), new KeyValuePair>("NameClaimType", new List{ClaimsIdentity.DefaultNameClaimType, Guid.NewGuid().ToString(), Guid.NewGuid().ToString()}), new KeyValuePair>("PropertyBag", new List{(IDictionary)null, new Dictionary {{"CustomKey", "CustomValue"}}, new Dictionary()}), new KeyValuePair>("RefreshBeforeValidation", new List{false, true, false}), @@ -316,8 +312,6 @@ private TokenValidationParameters CreateTokenValidationParameters() validationParameters.TransformBeforeSignatureValidation = ValidationDelegates.TransformBeforeSignatureValidation; validationParameters.TryReadJwtClaim = ValidationDelegates.TryReadJwtClaim; validationParameters.TypeValidator = ValidationDelegates.TypeValidator; - validationParameters.ActorTokenValidationDelegate = ValidationDelegates.ActorTokenValidationDelegate; - validationParameters.ActorValidationParameters = new TokenValidationParameters(); validationParameters.ClockSkew = TimeSpan.FromSeconds(42); validationParameters.DebugId = Guid.NewGuid().ToString(); From e6638ae9533e543bbd18398967277aa4d5218a3d Mon Sep 17 00:00:00 2001 From: saurabhsathe-ms Date: Wed, 21 May 2025 11:10:49 -0700 Subject: [PATCH 36/52] Revert "Removed everything from TokenValidationParameters and the delegate" This reverts commit a5851123e5cdcee48f976a0eb20ea9fc851af5cd. --- .../JsonWebTokenHandler.ValidateToken.cs | 17 ++ .../LogMessages.cs | 2 +- .../Delegates.cs | 4 +- .../PublicAPI.Unshipped.txt | 12 +- .../TokenValidationParameters.cs | 79 ++++++++++ .../ActorClaimsTests.cs | 149 +++++++++++++++++- .../ValidationDelegates.cs | 4 + .../TokenValidationParametersTests.cs | 8 +- 8 files changed, 269 insertions(+), 6 deletions(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs index 115e597b87..a18adf5805 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs @@ -12,6 +12,7 @@ using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Telemetry; using TokenLogMessages = Microsoft.IdentityModel.Tokens.LogMessages; +using System.Text.Json; namespace Microsoft.IdentityModel.JsonWebTokens { @@ -595,7 +596,23 @@ internal async ValueTask ValidateTokenPayloadAsync( if (!tokenValidationResult.IsValid) return tokenValidationResult; } + if (AppContextSwitches.SerializeDeserializeActorClaim && jsonWebToken.TryGetPayloadValue(validationParameters.ActorClaimName, out var actClaim)) + { + if (validationParameters.ActorTokenValidationDelegate != null) + { + // Fix for CS1929: The issue occurs because `ConfigureAwait` is being called on a `TokenValidationResult` object, which is not a `Task` or `ValueTask`. + // The `ConfigureAwait` method is only valid for `Task` or `ValueTask` types. + // To fix this, remove the `ConfigureAwait(false)` call from the `ActorTokenValidator` invocation. + var actorTokenValidationResult = validationParameters.ActorTokenValidationDelegate(actClaim, validationParameters.ActorValidationParameters); + if (!actorTokenValidationResult.IsValid) + return actorTokenValidationResult; + } + else + { + throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant(LogMessages.IDX14115))); + } + } string tokenType = Validators.ValidateTokenType(jsonWebToken.Typ, jsonWebToken, validationParameters); return new TokenValidationResult(jsonWebToken, this, validationParameters.Clone(), issuer, null) { diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/LogMessages.cs b/src/Microsoft.IdentityModel.JsonWebTokens/LogMessages.cs index 79333eaa37..6c0abfe03e 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/LogMessages.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/LogMessages.cs @@ -26,7 +26,7 @@ internal static class LogMessages internal const string IDX14112 = "IDX14112: Only a single 'Actor' is supported. Found second claim of type: '{0}'"; internal const string IDX14113 = "IDX14113: A duplicate value for 'SecurityTokenDescriptor.{0}' exists in 'SecurityTokenDescriptor.Claims'. \nThe value of 'SecurityTokenDescriptor.{0}' is used."; internal const string IDX14114 = "IDX14114: Both '{0}.{1}' and '{0}.{2}' are null or empty."; - // internal const string IDX14115 = "IDX14115:"; + internal const string IDX14115 = "IDX14115: Unable to validate Actor token as act claim. Act claim validation is enabled but ActorTokenValidationDelegate was not provided"; internal const string IDX14116 = "IDX14116: '{0}' cannot contain the following claims: '{1}'. These values are added by default (if necessary) during security token creation."; // number of sections 'dots' is not correct internal const string IDX14120 = "IDX14120: JWT is not well formed, there is only one dot (.).\nThe token needs to be in JWS or JWE Compact Serialization Format. (JWS): 'EncodedHeader.EncodedPayload.EncodedSignature'. (JWE): 'EncodedProtectedHeader.EncodedEncryptedKey.EncodedInitializationVector.EncodedCiphertext.EncodedAuthenticationTag'."; diff --git a/src/Microsoft.IdentityModel.Tokens/Delegates.cs b/src/Microsoft.IdentityModel.Tokens/Delegates.cs index 9fe7e4a2cb..7818a8179a 100644 --- a/src/Microsoft.IdentityModel.Tokens/Delegates.cs +++ b/src/Microsoft.IdentityModel.Tokens/Delegates.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Security.Claims; using System.Text.Json; using System.Threading.Tasks; @@ -241,6 +240,7 @@ internal delegate ValidationResult SignatureValidationDelegate( /// Delegate to validate the 'act' claim and create actor's ClaimsIdentity. /// /// The JSON element representing the 'act' claim. + /// The token validation parameters. /// A ClaimsIdentity representing the actor. - public delegate ClaimsIdentity ActClaimRetrieverDelegate(JsonElement actClaim); + public delegate TokenValidationResult ActorTokenValidationDelegate(JsonElement actClaim, TokenValidationParameters validationParameters = null); } diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI.Unshipped.txt index d9fe34b816..7441a3f93a 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI.Unshipped.txt @@ -1,7 +1,17 @@ +Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActorChainDepth.get -> int +Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActorChainDepth.set -> void +Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActorClaimName.get -> string +Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActorClaimName.set -> void +Microsoft.IdentityModel.Tokens.TokenValidationParameters.MaxActorChainLength.get -> int +Microsoft.IdentityModel.Tokens.TokenValidationParameters.MaxActorChainLength.set -> void +Microsoft.IdentityModel.Tokens.ActorTokenValidationDelegate +Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActorTokenValidationDelegate.get -> Microsoft.IdentityModel.Tokens.ActorTokenValidationDelegate +Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActorTokenValidationDelegate.set -> void +Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActorTokenValidationParameters.get -> Microsoft.IdentityModel.Tokens.TokenValidationParameters +Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActorTokenValidationParameters.set -> void Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorChainDepth.get -> int Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorChainDepth.set -> void Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimName.get -> string Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimName.set -> void Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.get -> int Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.set -> void -Microsoft.IdentityModel.Tokens.ActClaimRetrieverDelegate diff --git a/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs b/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs index 184733eb34..958680060c 100644 --- a/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs +++ b/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs @@ -211,6 +211,7 @@ public virtual TokenValidationParameters Clone() return new(this) { IsClone = true, + ActorTokenValidationDelegate = this.ActorTokenValidationDelegate }; } @@ -763,5 +764,83 @@ public string RoleClaimType /// public IEnumerable ValidTypes { get; set; } + + private int maxActorChainLength = 5; + /// + /// Gets or sets the maximum depth allowed when processing nested actor tokens. + /// This prevents excessive recursion when handling deeply nested actor tokens. + /// The value must be at least 0. Value 0 would mean that the actor token is not allowed to be nested. + /// Default value is 5. Max value is also 5 + /// + /// Thrown if the value is less than 0. + public int MaxActorChainLength + { + get => maxActorChainLength; + set + { + if (value < 0 || value > 5) + throw LogHelper.LogExceptionMessage( + new ArgumentOutOfRangeException( + LogHelper.FormatInvariant( + LogMessages.IDX11027, + LogHelper.MarkAsNonPII("MaxActorChainLength")) + + ". Permissible values are integers in range 0 to 5")); + + maxActorChainLength = value; + } + } + + private string actorClaimName = "act"; + /// + /// Gets or sets the claim type name for the actor claim. + /// Permissible values are 'act' or 'actort'. + /// + /// + /// Thrown if the value is null. + /// + /// + /// Thrown if the value is not 'act' or 'actort'. + /// + public string ActorClaimName + { + get => AppContextSwitches.SerializeDeserializeActorClaim ? actorClaimName : "actort"; + set + { + if (string.IsNullOrEmpty(value)) + throw LogHelper.LogExceptionMessage( + new ArgumentOutOfRangeException( + LogHelper.FormatInvariant( + LogMessages.IDX11027, + LogHelper.MarkAsNonPII("ActorClaimName")) + + ". ValidationParameters.ActorClaimName cannot be set to empty.")); + actorClaimName = value; + } + } + private int _actorClainDepth; + /// + /// Gets or sets the depth of the actor chain. + /// This value determines the maximum depth of nested actor tokens that can be processed. + /// + public int ActorChainDepth + { + get => _actorClainDepth; + set + { + _actorClainDepth = value; + } + } + + /// + /// Gets or sets the delegate that will be used to validate the 'act' claim and create actor's ClaimsIdentity. + /// + public ActorTokenValidationDelegate ActorTokenValidationDelegate { get; set; } + + /// + /// Gets or sets the used to validate the actor claim. + /// + /// + /// This property allows specifying custom validation parameters for the actor claim. + /// + public TokenValidationParameters ActorTokenValidationParameters { get; set; } } } diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs index 29a809c8b6..e3b1f87e9c 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs @@ -9,7 +9,7 @@ using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.JsonWebTokens; using Xunit; - +using System.Threading.Tasks; namespace Microsoft.IdentityModel.Tests { public class ActorClaimsTests @@ -669,5 +669,152 @@ public void NestedClaimTokens_ExceedingMaxDepth_ThrowsException() } } + [ResetAppContextSwitches] + [Fact] + public async Task ActorValidatorShouldBeInvokedAndRespected() + { + var context = new CompareContext($"{this}.ActorValidatorShouldBeInvokedAndRespected"); + bool switchValue = false; + AppContext.TryGetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, out switchValue); + AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, true); + try + { + // Create actor identity + var actorIdentity = new CaseSensitiveClaimsIdentity("ActorAuth"); + actorIdentity.AddClaim(new Claim("sub", "actor-subject-id")); + actorIdentity.AddClaim(new Claim("name", "Actor Name")); + actorIdentity.AddClaim(new Claim("role", "admin")); + + // Create the main identity with Actor set + var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); + mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); + mainIdentity.AddClaim(new Claim("name", "Main User")); + mainIdentity.Actor = actorIdentity; + + // Create and sign a token + var tokenHandler = new JsonWebTokenHandler(); + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = mainIdentity, + Issuer = "https://example.com", + Audience = "https://api.example.com", + Expires = DateTime.UtcNow.AddHours(1), + SigningCredentials = Default.AsymmetricSigningCredentials + }; + var token = tokenHandler.CreateToken(tokenDescriptor); + + // Test Case 1: Successful actor validation + { + bool validatorCalled = false; + + // Create validation parameters with a custom actor validator + var validationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidIssuer = "https://example.com", + ValidateAudience = true, + ValidAudience = "https://api.example.com", + ValidateLifetime = true, + IssuerSigningKey = Default.AsymmetricSigningCredentials.Key, + ValidateIssuerSigningKey = true, + ActorValidationParameters = new TokenValidationParameters + { + ValidateIssuer = false, + ValidateAudience = false + }, + ActorTokenValidationDelegate = (actorElement, actorValidationParams) => + { + validatorCalled = true; + + // Verify the actor element structure + Assert.Equal("actor-subject-id", actorElement.GetProperty("sub").GetString()); + Assert.Equal("Actor Name", actorElement.GetProperty("name").GetString()); + Assert.Equal("admin", actorElement.GetProperty("role").GetString()); + + return new TokenValidationResult { IsValid = true }; + } + }; + + // Validate the token - should succeed + var result = await tokenHandler.ValidateTokenAsync(token, validationParameters); + + // Assert that validation succeeded and our validator was called + Assert.True(result.IsValid, "Token validation should succeed"); + Assert.True(validatorCalled, "Actor validator should be called"); + } + + // Test Case 2: Failing actor validation + { + bool validatorCalled = false; + string expectedErrorMessage = "IDX14115"; + + var validationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidIssuer = "https://example.com", + ValidateAudience = true, + ValidAudience = "https://api.example.com", + ValidateLifetime = true, + IssuerSigningKey = Default.AsymmetricSigningCredentials.Key, + ValidateIssuerSigningKey = true, + ActorTokenValidationDelegate = (actorElement, actorValidationParams) => + { + validatorCalled = true; + + // Return failed validation + return new TokenValidationResult + { + IsValid = false, + Exception = new SecurityTokenValidationException(expectedErrorMessage) + }; + } + }; + + // Validate the token - should fail + var result = await tokenHandler.ValidateTokenAsync(token, validationParameters); + + // Assert that validation failed and our validator was called + Assert.False(result.IsValid, "Token validation should fail"); + Assert.True(validatorCalled, "Actor validator should be called"); + Assert.NotNull(result.Exception); + Assert.Contains(expectedErrorMessage, result.Exception.Message); + } + + // Test Case 3: Missing actor validator throws exception + { + var validationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidIssuer = "https://example.com", + ValidateAudience = true, + ValidAudience = "https://api.example.com", + ValidateLifetime = true, + IssuerSigningKey = Default.AsymmetricSigningCredentials.Key, + ValidateIssuerSigningKey = true, + ActorTokenValidationDelegate = null + }; + + // Validate the token - should throw exception + var result = await tokenHandler.ValidateTokenAsync(token, validationParameters); + + Assert.False(result.IsValid); + Assert.NotNull(result.Exception); + Assert.True(result.Exception is SecurityTokenInvalidSignatureException, + $"Expected SecurityTokenInvalidSignatureException but got {result.Exception.GetType().Name}"); + Assert.Contains("IDX14115", result.Exception.Message); + } + + TestUtilities.AssertFailIfErrors(context); + } + catch (Exception ex) + { + context.Diffs.Add($"Exception: {ex}"); + TestUtilities.AssertFailIfErrors(context); + } + finally + { + AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, false); + } + } } } diff --git a/test/Microsoft.IdentityModel.TestUtils/ValidationDelegates.cs b/test/Microsoft.IdentityModel.TestUtils/ValidationDelegates.cs index 50a8b9ee40..845e958a12 100644 --- a/test/Microsoft.IdentityModel.TestUtils/ValidationDelegates.cs +++ b/test/Microsoft.IdentityModel.TestUtils/ValidationDelegates.cs @@ -251,5 +251,9 @@ public static string TypeValidator(string type, SecurityToken securityToken, Tok return type; } + public static readonly ActorTokenValidationDelegate ActorTokenValidationDelegate = (actorClaim, validationParameters) => + { + return new TokenValidationResult { IsValid = true }; + }; } } diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/TokenValidationParametersTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/TokenValidationParametersTests.cs index 5439499477..097ddb3a33 100644 --- a/test/Microsoft.IdentityModel.Tokens.Tests/TokenValidationParametersTests.cs +++ b/test/Microsoft.IdentityModel.Tokens.Tests/TokenValidationParametersTests.cs @@ -15,7 +15,7 @@ namespace Microsoft.IdentityModel.Tokens.Tests { public class TokenValidationParametersTests { - int ExpectedPropertyCount = 62; + int ExpectedPropertyCount = 67; // GetSets() compares the total property count which includes internal properties, against a list of public properties, minus delegates. // This allows us to keep track of any properties we are including in the total that are not public nor delegates. @@ -198,6 +198,9 @@ public void GetSets() { PropertyNamesAndSetGetValue = new List>> { + new KeyValuePair>("ActorClaimName", new List{"actort"}), + new KeyValuePair>("ActorChainDepth", new List{0,1}), + new KeyValuePair>("ActorTokenValidationParameters", new List{(TokenValidationParameters)null, new TokenValidationParameters(), new TokenValidationParameters()}), new KeyValuePair>("ActorValidationParameters", new List{(TokenValidationParameters)null, new TokenValidationParameters(), new TokenValidationParameters()}), new KeyValuePair>("AuthenticationType", new List{(string)null, Guid.NewGuid().ToString(), Guid.NewGuid().ToString()}), new KeyValuePair>("ClockSkew", new List{TokenValidationParameters.DefaultClockSkew, TimeSpan.FromHours(2), TimeSpan.FromMinutes(1)}), @@ -212,6 +215,7 @@ public void GetSets() new KeyValuePair>("IssuerSigningKeys", new List{(IEnumerable)null, new List{KeyingMaterial.DefaultX509Key_2048, KeyingMaterial.RsaSecurityKey_1024}, new List()}), new KeyValuePair>("LogTokenId", new List{true, false, true}), new KeyValuePair>("LogValidationExceptions", new List{true, false, true}), + new KeyValuePair>("MaxActorChainLength", new List{5,2}), new KeyValuePair>("NameClaimType", new List{ClaimsIdentity.DefaultNameClaimType, Guid.NewGuid().ToString(), Guid.NewGuid().ToString()}), new KeyValuePair>("PropertyBag", new List{(IDictionary)null, new Dictionary {{"CustomKey", "CustomValue"}}, new Dictionary()}), new KeyValuePair>("RefreshBeforeValidation", new List{false, true, false}), @@ -312,6 +316,8 @@ private TokenValidationParameters CreateTokenValidationParameters() validationParameters.TransformBeforeSignatureValidation = ValidationDelegates.TransformBeforeSignatureValidation; validationParameters.TryReadJwtClaim = ValidationDelegates.TryReadJwtClaim; validationParameters.TypeValidator = ValidationDelegates.TypeValidator; + validationParameters.ActorTokenValidationDelegate = ValidationDelegates.ActorTokenValidationDelegate; + validationParameters.ActorValidationParameters = new TokenValidationParameters(); validationParameters.ClockSkew = TimeSpan.FromSeconds(42); validationParameters.DebugId = Guid.NewGuid().ToString(); From 95db3101473d631e2637309a187dbafc06686e46 Mon Sep 17 00:00:00 2001 From: saurabhsathe-ms Date: Wed, 21 May 2025 11:47:51 -0700 Subject: [PATCH 37/52] Renamed the switch and removed the validation code --- .../JsonWebTokenHandler.CreateToken.cs | 5 +- .../JsonWebTokenHandler.ValidateToken.cs | 17 -- .../AppContextSwitches.cs | 12 +- .../InternalAPI.Unshipped.txt | 6 +- .../TokenValidationParameters.cs | 2 +- .../ActorClaimsTests.cs | 206 +++--------------- 6 files changed, 41 insertions(+), 207 deletions(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs index 8a53414fa1..6c00f8126d 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs @@ -673,8 +673,7 @@ internal static void WriteJwsPayload( { foreach (KeyValuePair kvp in tokenDescriptor.Claims) { - AppContext.TryGetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, out bool isnebaled); - if (kvp.Key.Equals(tokenDescriptor.ActorClaimName, StringComparison.Ordinal)) + if (AppContextSwitches.EnableActClaimSupport && kvp.Key.Equals(tokenDescriptor.ActorClaimName, StringComparison.Ordinal)) { continue; } @@ -759,7 +758,7 @@ internal static void WriteJwsPayload( JsonPrimitives.WriteObject(ref writer, kvp.Key, kvp.Value); } } - if (AppContextSwitches.SerializeDeserializeActorClaim) + if (AppContextSwitches.EnableActClaimSupport) WriteActorToken(writer, tokenDescriptor, setDefaultTimesOnTokenCreation, tokenLifetimeInMinutes); AddSubjectClaims(ref writer, tokenDescriptor, audienceSet, issuerSet, ref expSet, ref iatSet, ref nbfSet); diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs index a18adf5805..115e597b87 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs @@ -12,7 +12,6 @@ using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Telemetry; using TokenLogMessages = Microsoft.IdentityModel.Tokens.LogMessages; -using System.Text.Json; namespace Microsoft.IdentityModel.JsonWebTokens { @@ -596,23 +595,7 @@ internal async ValueTask ValidateTokenPayloadAsync( if (!tokenValidationResult.IsValid) return tokenValidationResult; } - if (AppContextSwitches.SerializeDeserializeActorClaim && jsonWebToken.TryGetPayloadValue(validationParameters.ActorClaimName, out var actClaim)) - { - if (validationParameters.ActorTokenValidationDelegate != null) - { - // Fix for CS1929: The issue occurs because `ConfigureAwait` is being called on a `TokenValidationResult` object, which is not a `Task` or `ValueTask`. - // The `ConfigureAwait` method is only valid for `Task` or `ValueTask` types. - // To fix this, remove the `ConfigureAwait(false)` call from the `ActorTokenValidator` invocation. - var actorTokenValidationResult = validationParameters.ActorTokenValidationDelegate(actClaim, validationParameters.ActorValidationParameters); - if (!actorTokenValidationResult.IsValid) - return actorTokenValidationResult; - } - else - { - throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant(LogMessages.IDX14115))); - } - } string tokenType = Validators.ValidateTokenType(jsonWebToken.Typ, jsonWebToken, validationParameters); return new TokenValidationResult(jsonWebToken, this, validationParameters.Clone(), issuer, null) { diff --git a/src/Microsoft.IdentityModel.Tokens/AppContextSwitches.cs b/src/Microsoft.IdentityModel.Tokens/AppContextSwitches.cs index a6634d5af3..73449d9d61 100644 --- a/src/Microsoft.IdentityModel.Tokens/AppContextSwitches.cs +++ b/src/Microsoft.IdentityModel.Tokens/AppContextSwitches.cs @@ -99,11 +99,11 @@ internal static class AppContextSwitches internal static bool UseCapitalizedXMLTypeAttr => _useCapitalizedXMLTypeAttr ??= (AppContext.TryGetSwitch(UseCapitalizedXMLTypeAttrSwitch, out bool useCapitalizedXMLTypeAttr) && useCapitalizedXMLTypeAttr); /// - /// Enable the legacy behavior of actor claims. The legacy behavior is to use "actort" as claim name and also not serialize actor claim. + /// This switch enables the support for act claim. When enabled the actor claim will be serialized and deserialized into JsonWebToken. /// - internal const string SerializeDeserializeActorClaimSwitch = "Switch.Microsoft.IdentityModel.SerializeDeserializeActorClaim"; - private static bool? _serializeDeserializeActorClaim; - internal static bool SerializeDeserializeActorClaim => _serializeDeserializeActorClaim ??= (AppContext.TryGetSwitch(SerializeDeserializeActorClaimSwitch, out bool SerializeDeserializeActorClaim) && SerializeDeserializeActorClaim); + internal const string EnableActClaimSupportSwitch = "Switch.Microsoft.IdentityModel.EnableActClaimSupportSwitch"; + private static bool? _enableActClaimSupport; + internal static bool EnableActClaimSupport => _enableActClaimSupport ??= (AppContext.TryGetSwitch(EnableActClaimSupportSwitch, out bool EnableActClaimSupport) && EnableActClaimSupport); /// /// Used for testing to reset all switches to its default value. /// @@ -130,8 +130,8 @@ internal static void ResetAllSwitches() _useCapitalizedXMLTypeAttr = null; AppContext.SetSwitch(UseCapitalizedXMLTypeAttrSwitch, false); - _serializeDeserializeActorClaim = null; - AppContext.SetSwitch(SerializeDeserializeActorClaimSwitch, false); + _enableActClaimSupport = null; + AppContext.SetSwitch(EnableActClaimSupportSwitch, false); } } } diff --git a/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt index 0ab5d08a66..a01c570b75 100644 --- a/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt @@ -1,5 +1,5 @@ -const Microsoft.IdentityModel.Tokens.AppContextSwitches.SerializeDeserializeActorClaimSwitch = "Switch.Microsoft.IdentityModel.SerializeDeserializeActorClaim" -> string +const Microsoft.IdentityModel.Tokens.AppContextSwitches.EnableActClaimSupportSwitch = "Switch.Microsoft.IdentityModel.EnableActClaimSupportSwitch" -> string const Microsoft.IdentityModel.Tokens.AppContextSwitches.UseCapitalizedXMLTypeAttrSwitch = "Switch.Microsoft.IdentityModel.UseCapitalizedXMLTypeAttr" -> string const Microsoft.IdentityModel.Tokens.LogMessages.IDX11027 = "IDX11027: Invalid JsonWebToken handler configuration parameter value provided for {0}" -> string -static Microsoft.IdentityModel.Tokens.AppContextSwitches.SerializeDeserializeActorClaim.get -> bool -static Microsoft.IdentityModel.Tokens.AppContextSwitches.UseCapitalizedXMLTypeAttr.get -> bool \ No newline at end of file +static Microsoft.IdentityModel.Tokens.AppContextSwitches.EnableActClaimSupport.get -> bool +static Microsoft.IdentityModel.Tokens.AppContextSwitches.UseCapitalizedXMLTypeAttr.get -> bool diff --git a/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs b/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs index 958680060c..8d77728a44 100644 --- a/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs +++ b/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs @@ -803,7 +803,7 @@ public int MaxActorChainLength /// public string ActorClaimName { - get => AppContextSwitches.SerializeDeserializeActorClaim ? actorClaimName : "actort"; + get => AppContextSwitches.EnableActClaimSupport ? actorClaimName : "actort"; set { if (string.IsNullOrEmpty(value)) diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs index e3b1f87e9c..5166726cb0 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs @@ -20,8 +20,8 @@ public void ActorTokenInClaimsDictionaryShouldBeProperlySerialized() { var context = new CompareContext($"{this}.ActorTokenInClaimsDictionaryShouldBeProperlySerialized"); bool switchValue = false; - AppContext.TryGetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, out switchValue); - AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, true); + AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); string actorname = "act"; try { @@ -71,7 +71,7 @@ public void ActorTokenInClaimsDictionaryShouldBeProperlySerialized() } finally { - AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, false); + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); } } @@ -81,8 +81,8 @@ public void ActorTokenAsSubjectShouldBeProperlySerialized() { var context = new CompareContext($"{this}.ActorTokenAsSubjectShouldBeProperlySerialized"); bool switchValue = false; - AppContext.TryGetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, out switchValue); - AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, true); + AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); try { // Create actor identity @@ -132,7 +132,7 @@ public void ActorTokenAsSubjectShouldBeProperlySerialized() } finally { - AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, false); + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); } } @@ -143,8 +143,8 @@ public void ActorTokenInBothClaimsAndSubjectShouldPreferClaimsValue() var context = new CompareContext($"{this}.ActorTokenInBothClaimsAndSubjectShouldPreferClaimsValue"); bool switchValue = false; string actorname = "act"; - AppContext.TryGetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, out switchValue); - AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, true); + AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); try { // Create actor identity for Subject.Actor (should be ignored) @@ -200,7 +200,7 @@ public void ActorTokenInBothClaimsAndSubjectShouldPreferClaimsValue() } finally { - AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, false); + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); } } @@ -210,8 +210,8 @@ public void NestedActorTokenInClaimsDictionaryShouldBeProperlySerialized() { var context = new CompareContext($"{this}.NestedActorTokenInClaimsDictionaryShouldBeProperlySerialized"); bool switchValue = false; - AppContext.TryGetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, out switchValue); - AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, true); + AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); try { // Create nested actor identity @@ -273,7 +273,7 @@ public void NestedActorTokenInClaimsDictionaryShouldBeProperlySerialized() } finally { - AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, false); + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); } } [ResetAppContextSwitches] @@ -282,7 +282,7 @@ public void NestedActorTokenAsSubjectShouldBeProperlySerialized() { var context = new CompareContext($"{this}.NestedActorTokenAsSubjectShouldBeProperlySerialized"); bool switchValue = false; - AppContext.TryGetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, out switchValue); + AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); try { // Create nested actor @@ -313,7 +313,7 @@ public void NestedActorTokenAsSubjectShouldBeProperlySerialized() SigningCredentials = Default.AsymmetricSigningCredentials, ActorClaimName = "act", }; - AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, true); + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); var token = tokenHandler.CreateToken(tokenDescriptor); JsonWebToken decodedToken = tokenHandler.ReadJsonWebToken(token); @@ -342,7 +342,7 @@ public void NestedActorTokenAsSubjectShouldBeProperlySerialized() } finally { - AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, false); + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); } } [ResetAppContextSwitches] @@ -350,7 +350,7 @@ public void NestedActorTokenAsSubjectShouldBeProperlySerialized() public void MaxActorChainLength_RejectsNegativeValues() { bool switchValue = false; - AppContext.TryGetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, out switchValue); + AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); // Arrange SecurityTokenDescriptor tokenDescriptor = new SecurityTokenDescriptor @@ -360,7 +360,7 @@ public void MaxActorChainLength_RejectsNegativeValues() Audience = "https://api.example.com", SigningCredentials = Default.AsymmetricSigningCredentials }; - AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, true); + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); tokenDescriptor.ActorClaimName = "act"; // Set the actor claim name to "act" for testing int originalValue = tokenDescriptor.MaxActorChainLength; try @@ -387,7 +387,7 @@ public void MaxActorChainLength_RejectsNegativeValues() { // Restore to original value tokenDescriptor.MaxActorChainLength = originalValue; - AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, false); + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); } } @@ -397,8 +397,8 @@ public void NestedSubjectActorTokens_ExceedingMaxDepth_ThrowsException() { var context = new CompareContext($"{this}.NestedActorTokens_ExceedingMaxDepth_ThrowsException"); bool switchValue = false; - AppContext.TryGetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, out switchValue); - AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, true); + AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); try { // Arrange @@ -457,7 +457,7 @@ public void NestedSubjectActorTokens_ExceedingMaxDepth_ThrowsException() } finally { - AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, false); + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); } } @@ -467,8 +467,8 @@ public void NestedClaimsDictionaryActorTokens_ExceedingMaxDepth_ThrowsException( { var context = new CompareContext($"{this}.NestedClaimsDictionaryActorTokens_ExceedingMaxDepth_ThrowsException"); bool switchValue = false; - AppContext.TryGetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, out switchValue); - AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, true); + AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); try { // Arrange @@ -525,14 +525,14 @@ public void NestedClaimsDictionaryActorTokens_ExceedingMaxDepth_ThrowsException( } finally { - AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, false); + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); } } [ResetAppContextSwitches] [Fact] public void ActorTokens_MixedSourceRespectMaxActorChainLength() { - AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, true); + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); try { // Arrange @@ -594,7 +594,7 @@ public void ActorTokens_MixedSourceRespectMaxActorChainLength() } finally { - AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, false); + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); } } [ResetAppContextSwitches] @@ -603,7 +603,7 @@ public void NestedClaimTokens_ExceedingMaxDepth_ThrowsException() { var context = new CompareContext($"{this}.NestedActorTokens_ExceedingMaxDepth_ThrowsException"); bool switchValue = false; - AppContext.TryGetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, out switchValue); + AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); var actorname = "act"; try { @@ -643,7 +643,7 @@ public void NestedClaimTokens_ExceedingMaxDepth_ThrowsException() ActorClaimName = actorname, MaxActorChainLength = 1 }; - AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, true); + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); // Act - This should throw a SecurityTokenException var token = handler.CreateToken(tokenDescriptor); @@ -665,155 +665,7 @@ public void NestedClaimTokens_ExceedingMaxDepth_ThrowsException() } finally { - AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, false); - } - } - - [ResetAppContextSwitches] - [Fact] - public async Task ActorValidatorShouldBeInvokedAndRespected() - { - var context = new CompareContext($"{this}.ActorValidatorShouldBeInvokedAndRespected"); - bool switchValue = false; - AppContext.TryGetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, out switchValue); - AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, true); - try - { - // Create actor identity - var actorIdentity = new CaseSensitiveClaimsIdentity("ActorAuth"); - actorIdentity.AddClaim(new Claim("sub", "actor-subject-id")); - actorIdentity.AddClaim(new Claim("name", "Actor Name")); - actorIdentity.AddClaim(new Claim("role", "admin")); - - // Create the main identity with Actor set - var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); - mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); - mainIdentity.AddClaim(new Claim("name", "Main User")); - mainIdentity.Actor = actorIdentity; - - // Create and sign a token - var tokenHandler = new JsonWebTokenHandler(); - var tokenDescriptor = new SecurityTokenDescriptor - { - Subject = mainIdentity, - Issuer = "https://example.com", - Audience = "https://api.example.com", - Expires = DateTime.UtcNow.AddHours(1), - SigningCredentials = Default.AsymmetricSigningCredentials - }; - var token = tokenHandler.CreateToken(tokenDescriptor); - - // Test Case 1: Successful actor validation - { - bool validatorCalled = false; - - // Create validation parameters with a custom actor validator - var validationParameters = new TokenValidationParameters - { - ValidateIssuer = true, - ValidIssuer = "https://example.com", - ValidateAudience = true, - ValidAudience = "https://api.example.com", - ValidateLifetime = true, - IssuerSigningKey = Default.AsymmetricSigningCredentials.Key, - ValidateIssuerSigningKey = true, - ActorValidationParameters = new TokenValidationParameters - { - ValidateIssuer = false, - ValidateAudience = false - }, - ActorTokenValidationDelegate = (actorElement, actorValidationParams) => - { - validatorCalled = true; - - // Verify the actor element structure - Assert.Equal("actor-subject-id", actorElement.GetProperty("sub").GetString()); - Assert.Equal("Actor Name", actorElement.GetProperty("name").GetString()); - Assert.Equal("admin", actorElement.GetProperty("role").GetString()); - - return new TokenValidationResult { IsValid = true }; - } - }; - - // Validate the token - should succeed - var result = await tokenHandler.ValidateTokenAsync(token, validationParameters); - - // Assert that validation succeeded and our validator was called - Assert.True(result.IsValid, "Token validation should succeed"); - Assert.True(validatorCalled, "Actor validator should be called"); - } - - // Test Case 2: Failing actor validation - { - bool validatorCalled = false; - string expectedErrorMessage = "IDX14115"; - - var validationParameters = new TokenValidationParameters - { - ValidateIssuer = true, - ValidIssuer = "https://example.com", - ValidateAudience = true, - ValidAudience = "https://api.example.com", - ValidateLifetime = true, - IssuerSigningKey = Default.AsymmetricSigningCredentials.Key, - ValidateIssuerSigningKey = true, - ActorTokenValidationDelegate = (actorElement, actorValidationParams) => - { - validatorCalled = true; - - // Return failed validation - return new TokenValidationResult - { - IsValid = false, - Exception = new SecurityTokenValidationException(expectedErrorMessage) - }; - } - }; - - // Validate the token - should fail - var result = await tokenHandler.ValidateTokenAsync(token, validationParameters); - - // Assert that validation failed and our validator was called - Assert.False(result.IsValid, "Token validation should fail"); - Assert.True(validatorCalled, "Actor validator should be called"); - Assert.NotNull(result.Exception); - Assert.Contains(expectedErrorMessage, result.Exception.Message); - } - - // Test Case 3: Missing actor validator throws exception - { - var validationParameters = new TokenValidationParameters - { - ValidateIssuer = true, - ValidIssuer = "https://example.com", - ValidateAudience = true, - ValidAudience = "https://api.example.com", - ValidateLifetime = true, - IssuerSigningKey = Default.AsymmetricSigningCredentials.Key, - ValidateIssuerSigningKey = true, - ActorTokenValidationDelegate = null - }; - - // Validate the token - should throw exception - var result = await tokenHandler.ValidateTokenAsync(token, validationParameters); - - Assert.False(result.IsValid); - Assert.NotNull(result.Exception); - Assert.True(result.Exception is SecurityTokenInvalidSignatureException, - $"Expected SecurityTokenInvalidSignatureException but got {result.Exception.GetType().Name}"); - Assert.Contains("IDX14115", result.Exception.Message); - } - - TestUtilities.AssertFailIfErrors(context); - } - catch (Exception ex) - { - context.Diffs.Add($"Exception: {ex}"); - TestUtilities.AssertFailIfErrors(context); - } - finally - { - AppContext.SetSwitch(AppContextSwitches.SerializeDeserializeActorClaimSwitch, false); + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); } } } From 9a1f6edbcf855f583a8649a7d0c342613a10dc54 Mon Sep 17 00:00:00 2001 From: saurabhsathe-ms Date: Wed, 21 May 2025 18:56:50 -0700 Subject: [PATCH 38/52] Changed delegate name and added the tests to test our new function? --- .../JsonWebTokenHandler.cs | 119 ++- .../LogMessages.cs | 2 +- .../PublicAPI.Unshipped.txt | 1 + .../Delegates.cs | 3 +- .../PublicAPI.Unshipped.txt | 6 +- .../TokenValidationParameters.cs | 27 +- .../ActorClaimsTests.cs | 680 ++++++++++++++++++ .../ValidationDelegates.cs | 5 +- .../TokenValidationParametersTests.cs | 2 +- 9 files changed, 830 insertions(+), 15 deletions(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs index 173c06fe80..572c66c48f 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Security.Claims; +using System.Text.Json; using System.Text.RegularExpressions; using Microsoft.IdentityModel.Abstractions; using Microsoft.IdentityModel.Logging; @@ -220,7 +221,7 @@ private ClaimsIdentity CreateClaimsIdentityWithMapping(JsonWebToken jwtToken, To if (!wasMapped) claimType = jwtClaim.Type; - if (claimType == ClaimTypes.Actor) + if (claimType == validationParameters.ActorClaimName && !AppContextSwitches.EnableActClaimSupport) { if (identity.Actor != null) throw LogHelper.LogExceptionMessage(new InvalidOperationException(LogHelper.FormatInvariant( @@ -254,7 +255,49 @@ private ClaimsIdentity CreateClaimsIdentityWithMapping(JsonWebToken jwtToken, To identity.AddClaim(jwtClaim); } } + if (AppContextSwitches.EnableActClaimSupport) + { + + if (jwtToken.TryGetPayloadValue(validationParameters.ActorClaimName, out JsonElement actClaim)) + { + if (identity.Actor != null) + throw LogHelper.LogExceptionMessage(new InvalidOperationException(LogHelper.FormatInvariant( + LogMessages.IDX14112, + LogHelper.MarkAsNonPII(JwtRegisteredClaimNames.Actort), + actClaim.ToString()))); + if (validationParameters.ActClaimRetrieverDelegate != null) + { + try + { + identity.Actor = validationParameters.ActClaimRetrieverDelegate(actClaim); + } + catch (Exception ex) + { + throw LogHelper.LogExceptionMessage(new SecurityTokenDecryptionFailedException(LogHelper.FormatInvariant( + LogMessages.IDX14313, + LogHelper.MarkAsNonPII(JwtRegisteredClaimNames.Actort), + actClaim.ToString(), + ex))); + } + } + else + { + try + { + identity.Actor = CreateActorClaimsIdentityFromJsonElement(actClaim, validationParameters); + } + catch (Exception ex) + { + throw LogHelper.LogExceptionMessage(new SecurityTokenDecryptionFailedException(LogHelper.FormatInvariant( + LogMessages.IDX14313, + LogHelper.MarkAsNonPII(JwtRegisteredClaimNames.Actort), + actClaim.ToString(), + ex))); + } + } + } + } return identity; } @@ -571,5 +614,79 @@ private static TokenValidationResult ReadToken(string token, TokenValidationPara IsValid = true }; } + + /// + /// Creates a ClaimsIdentity from a JsonElement that represents an actor token. + /// + /// The JsonElement containing actor claims. + /// These parameters have details like nested actor chain length and max permissible actor length + /// The issuer for the claims. + /// The authentication type for the identity. + /// A ClaimsIdentity containing claims from the JsonElement. + public static ClaimsIdentity CreateActorClaimsIdentityFromJsonElement( + JsonElement jsonElement, + TokenValidationParameters tokenValidationParameters, + string issuer = null, + string authenticationType = "Actor") + { + if (tokenValidationParameters == null) + throw LogHelper.LogArgumentNullException(nameof(tokenValidationParameters)); + + if (tokenValidationParameters.ActorChainDepth >= tokenValidationParameters.MaxActorChainLength) + { + throw LogHelper.LogExceptionMessage( + new SecurityTokenException(LogHelper.FormatInvariant( + LogMessages.IDX14313, + LogHelper.MarkAsNonPII(tokenValidationParameters.ActorChainDepth), + LogHelper.MarkAsNonPII(tokenValidationParameters.MaxActorChainLength)))); + } + + if (jsonElement.ValueKind != JsonValueKind.Object) + throw LogHelper.LogExceptionMessage(new ArgumentException("Actor token must be a JSON object")); + + // Use CaseSensitiveClaimsIdentity for consistent behavior with the rest of the library + var identity = new CaseSensitiveClaimsIdentity(authenticationType); + + issuer = issuer ?? ClaimsIdentity.DefaultIssuer; + + foreach (var property in jsonElement.EnumerateObject()) + { + string claimType = property.Name; + JsonElement value = property.Value; + + // Special handling for nested actor claim + if (claimType == tokenValidationParameters.ActorClaimName) + { + if (value.ValueKind == JsonValueKind.Object) + { + tokenValidationParameters.ActorChainDepth++; + // Recursively create nested actor identity + identity.Actor = CreateActorClaimsIdentityFromJsonElement( + value, tokenValidationParameters, issuer, authenticationType); + } + continue; + } + + // For all other claims, create and add them + if (value.ValueKind == JsonValueKind.Array) + { + foreach (JsonElement element in value.EnumerateArray()) + { + var claim = JsonClaimSet.CreateClaimFromJsonElement(claimType, issuer, element); + if (claim != null) + identity.AddClaim(claim); + } + } + else + { + var claim = JsonClaimSet.CreateClaimFromJsonElement(claimType, issuer, value); + if (claim != null) + identity.AddClaim(claim); + } + } + + return identity; + } + } } diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/LogMessages.cs b/src/Microsoft.IdentityModel.JsonWebTokens/LogMessages.cs index 6c0abfe03e..4fba1383ae 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/LogMessages.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/LogMessages.cs @@ -51,6 +51,6 @@ internal static class LogMessages internal const string IDX14310 = "IDX14310: JWE authentication tag is missing."; internal const string IDX14311 = "IDX14311: Unable to decode the authentication tag as a Base64Url encoded string."; internal const string IDX14312 = "IDX14312: Unable to decode the cipher text as a Base64Url encoded string."; - internal const string IDX14313 = "IDX14313: Unable to serialize actor token. Maximum actor token depth reached. Current nesting depth is {0} while max depth set is {1}"; + internal const string IDX14313 = "IDX14313: Unable to serialize/deserialize actor token. Maximum actor token depth reached. Current nesting depth is {0} while max depth set is {1}"; } } diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.JsonWebTokens/PublicAPI.Unshipped.txt index e69de29bb2..d8d9447078 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.JsonWebTokens/PublicAPI.Unshipped.txt @@ -0,0 +1 @@ +static Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.CreateActorClaimsIdentityFromJsonElement(System.Text.Json.JsonElement jsonElement, Microsoft.IdentityModel.Tokens.TokenValidationParameters tokenValidationParameters, string issuer = null, string authenticationType = "Actor") -> System.Security.Claims.ClaimsIdentity \ No newline at end of file diff --git a/src/Microsoft.IdentityModel.Tokens/Delegates.cs b/src/Microsoft.IdentityModel.Tokens/Delegates.cs index 7818a8179a..aca30eb2d3 100644 --- a/src/Microsoft.IdentityModel.Tokens/Delegates.cs +++ b/src/Microsoft.IdentityModel.Tokens/Delegates.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Security.Claims; using System.Text.Json; using System.Threading.Tasks; @@ -242,5 +243,5 @@ internal delegate ValidationResult SignatureValidationDelegate( /// The JSON element representing the 'act' claim. /// The token validation parameters. /// A ClaimsIdentity representing the actor. - public delegate TokenValidationResult ActorTokenValidationDelegate(JsonElement actClaim, TokenValidationParameters validationParameters = null); + public delegate ClaimsIdentity ActClaimRetrieverDelegate(JsonElement actClaim, TokenValidationParameters validationParameters = null); } diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI.Unshipped.txt index 7441a3f93a..a4ee940e07 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI.Unshipped.txt @@ -4,9 +4,9 @@ Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActorClaimName.get -> s Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActorClaimName.set -> void Microsoft.IdentityModel.Tokens.TokenValidationParameters.MaxActorChainLength.get -> int Microsoft.IdentityModel.Tokens.TokenValidationParameters.MaxActorChainLength.set -> void -Microsoft.IdentityModel.Tokens.ActorTokenValidationDelegate -Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActorTokenValidationDelegate.get -> Microsoft.IdentityModel.Tokens.ActorTokenValidationDelegate -Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActorTokenValidationDelegate.set -> void +Microsoft.IdentityModel.Tokens.ActClaimRetrieverDelegate +Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActClaimRetrieverDelegate.get -> Microsoft.IdentityModel.Tokens.ActClaimRetrieverDelegate +Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActClaimRetrieverDelegate.set -> void Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActorTokenValidationParameters.get -> Microsoft.IdentityModel.Tokens.TokenValidationParameters Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActorTokenValidationParameters.set -> void Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorChainDepth.get -> int diff --git a/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs b/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs index 8d77728a44..92b0a295b0 100644 --- a/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs +++ b/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs @@ -40,6 +40,24 @@ public partial class TokenValidationParameters /// 250 KB (kilobytes). public const int DefaultMaximumTokenSizeInBytes = 1024 * 250; + /// + /// Default for permissible max actor chain length. + /// + /// 5 as max level of nesting + private int maxActorChainLength = 5; + + /// + /// Default for actor claim name. + /// + /// If not explicitly set the default name for actor claim is 'act'. Only needed when EnableActClaimSupportSwitch is turned on + private string actorClaimName = "act"; + + /// + /// This variable is used during recursion calls that are needed for deserializing act claim. + /// + /// Default value is 0 + private int _actorClainDepth; + /// /// Copy constructor for . /// @@ -111,6 +129,7 @@ protected TokenValidationParameters(TokenValidationParameters other) ValidIssuer = other.ValidIssuer; ValidIssuers = other.ValidIssuers; ValidTypes = other.ValidTypes; + ActClaimRetrieverDelegate = other.ActClaimRetrieverDelegate; } /// @@ -211,7 +230,6 @@ public virtual TokenValidationParameters Clone() return new(this) { IsClone = true, - ActorTokenValidationDelegate = this.ActorTokenValidationDelegate }; } @@ -764,8 +782,6 @@ public string RoleClaimType /// public IEnumerable ValidTypes { get; set; } - - private int maxActorChainLength = 5; /// /// Gets or sets the maximum depth allowed when processing nested actor tokens. /// This prevents excessive recursion when handling deeply nested actor tokens. @@ -790,7 +806,6 @@ public int MaxActorChainLength } } - private string actorClaimName = "act"; /// /// Gets or sets the claim type name for the actor claim. /// Permissible values are 'act' or 'actort'. @@ -816,7 +831,7 @@ public string ActorClaimName actorClaimName = value; } } - private int _actorClainDepth; + /// /// Gets or sets the depth of the actor chain. /// This value determines the maximum depth of nested actor tokens that can be processed. @@ -833,7 +848,7 @@ public int ActorChainDepth /// /// Gets or sets the delegate that will be used to validate the 'act' claim and create actor's ClaimsIdentity. /// - public ActorTokenValidationDelegate ActorTokenValidationDelegate { get; set; } + public ActClaimRetrieverDelegate ActClaimRetrieverDelegate { get; set; } /// /// Gets or sets the used to validate the actor claim. diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs index 5166726cb0..edb300cd2c 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs @@ -10,6 +10,7 @@ using Microsoft.IdentityModel.JsonWebTokens; using Xunit; using System.Threading.Tasks; +using System.Linq; namespace Microsoft.IdentityModel.Tests { public class ActorClaimsTests @@ -668,5 +669,684 @@ public void NestedClaimTokens_ExceedingMaxDepth_ThrowsException() AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); } } + + // Tests for creating ClaimsIdentity from JsonElement + [ResetAppContextSwitches] + [Fact] + public void BasicJsonElementShouldCreateClaimsIdentityCorrectly() + { + var context = new CompareContext($"{this}.BasicJsonElementShouldCreateClaimsIdentityCorrectly"); + bool switchValue = false; + AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); + try + { + // Create a simple JSON Element that represents an actor token + string actorJson = @"{ + ""sub"": ""actor-subject-id"", + ""name"": ""Actor Name"", + ""role"": ""admin"" + }"; + + var jsonElement = JsonDocument.Parse(actorJson).RootElement; + var validationParameters = new TokenValidationParameters() + { + ActorClaimName = "act" + }; + + // Create ClaimsIdentity from JsonElement + var identity = JsonWebTokenHandler.CreateActorClaimsIdentityFromJsonElement( + jsonElement, + validationParameters); + + // Assert + Assert.NotNull(identity); + Assert.Equal("Actor", identity.AuthenticationType); + Assert.IsType(identity); + + // Verify claims values + Assert.Equal("actor-subject-id", identity.Claims.First(c => c.Type == "sub").Value); + Assert.Equal("Actor Name", identity.Claims.First(c => c.Type == "name").Value); + Assert.Equal("admin", identity.Claims.First(c => c.Type == "role").Value); + + TestUtilities.AssertFailIfErrors(context); + } + catch (Exception ex) + { + context.Diffs.Add($"Exception: {ex}"); + } + finally + { + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); + } + } + + [ResetAppContextSwitches] + [Fact] + public void NestedActorInJsonElementShouldCreateNestedClaimsIdentity() + { + var context = new CompareContext($"{this}.NestedActorInJsonElementShouldCreateNestedClaimsIdentity"); + bool switchValue = false; + AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); + try + { + // Create nested actor JSON structure + string actorJson = @"{ + ""sub"": ""actor-subject-id"", + ""name"": ""Actor Name"", + ""act"": { + ""sub"": ""nested-actor-id"", + ""name"": ""Nested Actor"" + } + }"; + + var jsonElement = JsonDocument.Parse(actorJson).RootElement; + var tokenValidationParameters = new TokenValidationParameters + { + ActorClaimName = "act" + }; + + // Create ClaimsIdentity from JsonElement + var identity = JsonWebTokenHandler.CreateActorClaimsIdentityFromJsonElement( + jsonElement, + tokenValidationParameters); + + // Verify main identity + Assert.NotNull(identity); + Assert.Equal("actor-subject-id", identity.Claims.First(c => c.Type == "sub").Value); + Assert.Equal("Actor Name", identity.Claims.First(c => c.Type == "name").Value); + + // Verify nested actor identity + Assert.NotNull(identity.Actor); + Assert.Equal("nested-actor-id", identity.Actor.Claims.First(c => c.Type == "sub").Value); + Assert.Equal("Nested Actor", identity.Actor.Claims.First(c => c.Type == "name").Value); + + TestUtilities.AssertFailIfErrors(context); + } + catch (Exception ex) + { + context.Diffs.Add($"Exception: {ex}"); + } + finally + { + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); + } + } + + [ResetAppContextSwitches] + [Fact] + public void MultiLevelNestedActorJsonShouldHandleProperDepth() + { + var context = new CompareContext($"{this}.MultiLevelNestedActorJsonShouldHandleProperDepth"); + bool switchValue = false; + AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); + try + { + // Create a three-level nested actor JSON structure + string actorJson = @"{ + ""sub"": ""level1-subject"", + ""name"": ""Level 1 Actor"", + ""act"": { + ""sub"": ""level2-subject"", + ""name"": ""Level 2 Actor"", + ""act"": { + ""sub"": ""level3-subject"", + ""name"": ""Level 3 Actor"" + } + } + }"; + + var jsonElement = JsonDocument.Parse(actorJson).RootElement; + var tokenValidationParameters = new TokenValidationParameters + { + ActorClaimName = "act", + MaxActorChainLength = 3 // Allow up to 3 levels + }; + + // Create ClaimsIdentity from JsonElement + var identity = JsonWebTokenHandler.CreateActorClaimsIdentityFromJsonElement( + jsonElement, + tokenValidationParameters); + + // Verify level 1 + Assert.NotNull(identity); + Assert.Equal("level1-subject", identity.Claims.First(c => c.Type == "sub").Value); + Assert.Equal("Level 1 Actor", identity.Claims.First(c => c.Type == "name").Value); + + // Verify level 2 + Assert.NotNull(identity.Actor); + Assert.Equal("level2-subject", identity.Actor.Claims.First(c => c.Type == "sub").Value); + Assert.Equal("Level 2 Actor", identity.Actor.Claims.First(c => c.Type == "name").Value); + + // Verify level 3 + Assert.NotNull(identity.Actor.Actor); + Assert.Equal("level3-subject", identity.Actor.Actor.Claims.First(c => c.Type == "sub").Value); + Assert.Equal("Level 3 Actor", identity.Actor.Actor.Claims.First(c => c.Type == "name").Value); + + // No level 4 + Assert.Null(identity.Actor.Actor.Actor); + + TestUtilities.AssertFailIfErrors(context); + } + catch (Exception ex) + { + context.Diffs.Add($"Exception: {ex}"); + } + finally + { + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); + } + } + + [ResetAppContextSwitches] + [Fact] + public void NestedActorExceedingMaxDepth_ThrowsException() + { + var context = new CompareContext($"{this}.NestedActorExceedingMaxDepth_ThrowsException"); + bool switchValue = false; + AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); + try + { + // Create a three-level nested actor but set max depth to 2 + string actorJson = @"{ + ""sub"": ""level1-subject"", + ""name"": ""Level 1 Actor"", + ""act"": { + ""sub"": ""level2-subject"", + ""name"": ""Level 2 Actor"", + ""act"": { + ""sub"": ""level3-subject"", + ""name"": ""Level 3 Actor"" + } + } + }"; + + var jsonElement = JsonDocument.Parse(actorJson).RootElement; + var tokenValidationParameters = new TokenValidationParameters + { + ActorClaimName = "act", + MaxActorChainLength = 2, // Only allow 2 levels, but JSON has 3 + ActorChainDepth = 1 // Start at depth 1 to simulate being in an ongoing chain + }; + + // Act - This should throw a SecurityTokenException + var identity = JsonWebTokenHandler.CreateActorClaimsIdentityFromJsonElement( + jsonElement, + tokenValidationParameters); + + context.Diffs.Add("Expected exception was not thrown."); + TestUtilities.AssertFailIfErrors(context); + } + catch (SecurityTokenException ex) + { + // Assert - Verify the exception message contains the expected content + if (!ex.Message.Contains("IDX14313")) + { + context.Diffs.Add($"Exception message does not contain expected content. Message: {ex.Message}"); + } + TestUtilities.AssertFailIfErrors(context); + } + catch (Exception ex) + { + // Unexpected exception type + context.Diffs.Add($"Unexpected exception type: {ex.GetType()}, Message: {ex.Message}"); + TestUtilities.AssertFailIfErrors(context); + } + finally + { + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); + } + } + + [ResetAppContextSwitches] + [Fact] + public void JsonElementWithArrayValuesShouldProcessCorrectly() + { + var context = new CompareContext($"{this}.JsonElementWithArrayValuesShouldProcessCorrectly"); + bool switchValue = false; + AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); + try + { + // Create JSON with array value + string actorJson = @"{ + ""sub"": ""actor-subject-id"", + ""name"": ""Actor Name"", + ""roles"": [""admin"", ""user"", ""manager""] + }"; + + var jsonElement = JsonDocument.Parse(actorJson).RootElement; + var tokenValidationParameters = new TokenValidationParameters + { + ActorClaimName = "act" + }; + + // Create ClaimsIdentity from JsonElement + var identity = JsonWebTokenHandler.CreateActorClaimsIdentityFromJsonElement( + jsonElement, + tokenValidationParameters); + + // Verify identity and simple claims + Assert.NotNull(identity); + Assert.Equal("actor-subject-id", identity.Claims.First(c => c.Type == "sub").Value); + Assert.Equal("Actor Name", identity.Claims.First(c => c.Type == "name").Value); + + // Verify array values were processed into multiple claims + var roleClaims = identity.Claims.Where(c => c.Type == "roles").ToList(); + Assert.Equal(3, roleClaims.Count); + Assert.Contains(roleClaims, c => c.Value == "admin"); + Assert.Contains(roleClaims, c => c.Value == "user"); + Assert.Contains(roleClaims, c => c.Value == "manager"); + + TestUtilities.AssertFailIfErrors(context); + } + catch (Exception ex) + { + context.Diffs.Add($"Exception: {ex}"); + } + finally + { + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); + } + } + + [ResetAppContextSwitches] + [Fact] + public void JsonElementWithComplexTypesShouldHandleCorrectly() + { + var context = new CompareContext($"{this}.JsonElementWithComplexTypesShouldHandleCorrectly"); + bool switchValue = false; + AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); + try + { + // Create JSON with complex types (objects) + string actorJson = @"{ + ""sub"": ""actor-subject-id"", + ""name"": ""Actor Name"", + ""metadata"": { + ""created"": ""2023-10-15"", + ""system"": ""test-system"" + }, + ""numbers"": [1, 2, 3] + }"; + + var jsonElement = JsonDocument.Parse(actorJson).RootElement; + var tokenValidationParameters = new TokenValidationParameters + { + ActorClaimName = "act" + }; + + // Create ClaimsIdentity from JsonElement + var identity = JsonWebTokenHandler.CreateActorClaimsIdentityFromJsonElement( + jsonElement, + tokenValidationParameters); + + // Verify identity and simple claims + Assert.NotNull(identity); + Assert.Equal("actor-subject-id", identity.Claims.First(c => c.Type == "sub").Value); + Assert.Equal("Actor Name", identity.Claims.First(c => c.Type == "name").Value); + + // Verify the JSON object was serialized to a claim + var metadataClaim = identity.Claims.First(c => c.Type == "metadata"); + Assert.NotNull(metadataClaim); + Assert.Contains("created", metadataClaim.Value); + Assert.Contains("test-system", metadataClaim.Value); + + // Verify number array was handled + var numberClaims = identity.Claims.Where(c => c.Type == "numbers").ToList(); + Assert.Equal(3, numberClaims.Count); + Assert.Contains(numberClaims, c => c.Value == "1"); + Assert.Contains(numberClaims, c => c.Value == "2"); + Assert.Contains(numberClaims, c => c.Value == "3"); + + TestUtilities.AssertFailIfErrors(context); + } + catch (Exception ex) + { + context.Diffs.Add($"Exception: {ex}"); + } + finally + { + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); + } + } + + [ResetAppContextSwitches] + [Fact] + public void NonObjectJsonElement_ThrowsException() + { + var context = new CompareContext($"{this}.NonObjectJsonElement_ThrowsException"); + bool switchValue = false; + AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); + try + { + // Create a non-object JSON Element (string) + string actorJson = @"""This is just a string, not an object"""; + var jsonElement = JsonDocument.Parse(actorJson).RootElement; + + var tokenValidationParameters = new TokenValidationParameters + { + ActorClaimName = "act" + }; + + // Act - This should throw an ArgumentException + var identity = JsonWebTokenHandler.CreateActorClaimsIdentityFromJsonElement( + jsonElement, + tokenValidationParameters); + + context.Diffs.Add("Expected exception was not thrown."); + TestUtilities.AssertFailIfErrors(context); + } + catch (ArgumentException ex) + { + // Expected exception type + Assert.Contains("Actor token must be a JSON object", ex.Message); + } + catch (Exception ex) + { + // Unexpected exception type + context.Diffs.Add($"Unexpected exception type: {ex.GetType()}, Message: {ex.Message}"); + TestUtilities.AssertFailIfErrors(context); + } + finally + { + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); + } + } + + [ResetAppContextSwitches] + [Fact] + public void NullValidationParameters_ThrowsException() + { + var context = new CompareContext($"{this}.NullValidationParameters_ThrowsException"); + bool switchValue = false; + AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); + try + { + // Create a simple JSON Element + string actorJson = @"{ ""sub"": ""actor-subject-id"" }"; + var jsonElement = JsonDocument.Parse(actorJson).RootElement; + + // Act - This should throw an ArgumentNullException + var identity = JsonWebTokenHandler.CreateActorClaimsIdentityFromJsonElement( + jsonElement, + null); // Null validation parameters + + context.Diffs.Add("Expected exception was not thrown."); + TestUtilities.AssertFailIfErrors(context); + } + catch (ArgumentNullException ex) + { + // Expected exception type + Assert.Equal("tokenValidationParameters", ex.ParamName); + } + catch (Exception ex) + { + // Unexpected exception type + context.Diffs.Add($"Unexpected exception type: {ex.GetType()}, Message: {ex.Message}"); + TestUtilities.AssertFailIfErrors(context); + } + finally + { + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); + } + } + + [ResetAppContextSwitches] + [Fact] + public void CustomActorClaimNameShouldBeRespected() + { + var context = new CompareContext($"{this}.CustomActorClaimNameShouldBeRespected"); + bool switchValue = false; + AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); + try + { + // Create JSON with custom actor claim name + string actorJson = @"{ + ""sub"": ""actor-subject-id"", + ""name"": ""Actor Name"", + ""actort"": { + ""sub"": ""nested-actor-id"", + ""name"": ""Nested Actor"" + } + }"; + + var jsonElement = JsonDocument.Parse(actorJson).RootElement; + var tokenValidationParameters = new TokenValidationParameters + { + ActorClaimName = "actort" // Custom actor claim name + }; + + // Create ClaimsIdentity from JsonElement + var identity = JsonWebTokenHandler.CreateActorClaimsIdentityFromJsonElement( + jsonElement, + tokenValidationParameters); + + // Verify main identity + Assert.NotNull(identity); + Assert.Equal("actor-subject-id", identity.Claims.First(c => c.Type == "sub").Value); + Assert.Equal("Actor Name", identity.Claims.First(c => c.Type == "name").Value); + + // Verify nested actor was found using custom claim name + Assert.NotNull(identity.Actor); + Assert.Equal("nested-actor-id", identity.Actor.Claims.First(c => c.Type == "sub").Value); + Assert.Equal("Nested Actor", identity.Actor.Claims.First(c => c.Type == "name").Value); + + TestUtilities.AssertFailIfErrors(context); + } + catch (Exception ex) + { + context.Diffs.Add($"Exception: {ex}"); + } + finally + { + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); + } + } + + [ResetAppContextSwitches] + [Fact] + public void DifferentIssuerShouldBeAppliedToAllClaims() + { + var context = new CompareContext($"{this}.DifferentIssuerShouldBeAppliedToAllClaims"); + bool switchValue = false; + AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); + try + { + // Create simple actor JSON + string actorJson = @"{ + ""sub"": ""actor-subject-id"", + ""name"": ""Actor Name"" + }"; + + var jsonElement = JsonDocument.Parse(actorJson).RootElement; + var tokenValidationParameters = new TokenValidationParameters + { + ActorClaimName = "act" + }; + + string customIssuer = "https://custom-issuer.example.com"; + + // Create ClaimsIdentity with custom issuer + var identity = JsonWebTokenHandler.CreateActorClaimsIdentityFromJsonElement( + jsonElement, + tokenValidationParameters, + customIssuer); + + // Verify all claims have custom issuer + foreach (var claim in identity.Claims) + { + Assert.Equal(customIssuer, claim.Issuer); + Assert.Equal(customIssuer, claim.OriginalIssuer); + } + + TestUtilities.AssertFailIfErrors(context); + } + catch (Exception ex) + { + context.Diffs.Add($"Exception: {ex}"); + } + finally + { + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); + } + } + + [ResetAppContextSwitches] + [Fact] + public void CustomAuthenticationTypeShouldBeRespected() + { + var context = new CompareContext($"{this}.CustomAuthenticationTypeShouldBeRespected"); + bool switchValue = false; + AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); + try + { + // Create simple actor JSON + string actorJson = @"{ + ""sub"": ""actor-subject-id"", + ""name"": ""Actor Name"" + }"; + + var jsonElement = JsonDocument.Parse(actorJson).RootElement; + var tokenValidationParameters = new TokenValidationParameters + { + ActorClaimName = "act" + }; + + string customAuthType = "CustomActorAuth"; + + // Create ClaimsIdentity with custom auth type + var identity = JsonWebTokenHandler.CreateActorClaimsIdentityFromJsonElement( + jsonElement, + tokenValidationParameters, + null, // Default issuer + customAuthType); + + // Verify custom auth type was applied + Assert.Equal(customAuthType, identity.AuthenticationType); + + TestUtilities.AssertFailIfErrors(context); + } + catch (Exception ex) + { + context.Diffs.Add($"Exception: {ex}"); + } + finally + { + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); + } + } + + [ResetAppContextSwitches] + [Fact] + public void ActorChainDepthShouldBeIncremented() + { + var context = new CompareContext($"{this}.ActorChainDepthShouldBeIncremented"); + bool switchValue = false; + AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); + try + { + // Create actor JSON with nested actor + string actorJson = @"{ + ""sub"": ""actor-subject-id"", + ""name"": ""Actor Name"", + ""act"": { + ""sub"": ""nested-actor-id"", + ""name"": ""Nested Actor"" + } + }"; + + var jsonElement = JsonDocument.Parse(actorJson).RootElement; + var tokenValidationParameters = new TokenValidationParameters + { + ActorClaimName = "act", + MaxActorChainLength = 5, + ActorChainDepth = 2 // Start at depth 2 + }; + + // Create ClaimsIdentity from JsonElement + var identity = JsonWebTokenHandler.CreateActorClaimsIdentityFromJsonElement( + jsonElement, + tokenValidationParameters); + + // Verify depth was incremented (2 + 1 = 3) + Assert.Equal(3, tokenValidationParameters.ActorChainDepth); + + // Verify both levels of actors exist + Assert.NotNull(identity); + Assert.NotNull(identity.Actor); + + TestUtilities.AssertFailIfErrors(context); + } + catch (Exception ex) + { + context.Diffs.Add($"Exception: {ex}"); + } + finally + { + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); + } + } + + [ResetAppContextSwitches] + [Fact] + public void WhenActClaimIsNotAnObject_ShouldBeAddedAsRegularClaim() + { + var context = new CompareContext($"{this}.WhenActClaimIsNotAnObject_ShouldBeAddedAsRegularClaim"); + bool switchValue = false; + AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); + try + { + // Create JSON with "act" claim that is a string, not an object + string actorJson = @"{ + ""sub"": ""actor-subject-id"", + ""name"": ""Actor Name"", + ""act"": ""some-actor-reference-string"" + }"; + + var jsonElement = JsonDocument.Parse(actorJson).RootElement; + var tokenValidationParameters = new TokenValidationParameters + { + ActorClaimName = "act" + }; + + // Create ClaimsIdentity from JsonElement + var identity = JsonWebTokenHandler.CreateActorClaimsIdentityFromJsonElement( + jsonElement, + tokenValidationParameters); + + // Verify identity claims + Assert.NotNull(identity); + Assert.Equal("actor-subject-id", identity.Claims.First(c => c.Type == "sub").Value); + Assert.Equal("Actor Name", identity.Claims.First(c => c.Type == "name").Value); + + // Verify "act" is a regular claim, not a nested actor + Assert.Null(identity.Actor); + Assert.Equal("some-actor-reference-string", identity.Claims.First(c => c.Type == "act").Value); + + TestUtilities.AssertFailIfErrors(context); + } + catch (Exception ex) + { + context.Diffs.Add($"Exception: {ex}"); + } + finally + { + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); + } + } + + } } diff --git a/test/Microsoft.IdentityModel.TestUtils/ValidationDelegates.cs b/test/Microsoft.IdentityModel.TestUtils/ValidationDelegates.cs index 845e958a12..5dfebe30ba 100644 --- a/test/Microsoft.IdentityModel.TestUtils/ValidationDelegates.cs +++ b/test/Microsoft.IdentityModel.TestUtils/ValidationDelegates.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; using System.Text.Json; using System.Threading.Tasks; using Microsoft.IdentityModel.JsonWebTokens; @@ -251,9 +252,9 @@ public static string TypeValidator(string type, SecurityToken securityToken, Tok return type; } - public static readonly ActorTokenValidationDelegate ActorTokenValidationDelegate = (actorClaim, validationParameters) => + public static readonly ActClaimRetrieverDelegate ActClaimRetrieverDelegate = (actorClaim, validationParameters) => { - return new TokenValidationResult { IsValid = true }; + return null; }; } } diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/TokenValidationParametersTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/TokenValidationParametersTests.cs index 097ddb3a33..a94158a6e0 100644 --- a/test/Microsoft.IdentityModel.Tokens.Tests/TokenValidationParametersTests.cs +++ b/test/Microsoft.IdentityModel.Tokens.Tests/TokenValidationParametersTests.cs @@ -316,7 +316,7 @@ private TokenValidationParameters CreateTokenValidationParameters() validationParameters.TransformBeforeSignatureValidation = ValidationDelegates.TransformBeforeSignatureValidation; validationParameters.TryReadJwtClaim = ValidationDelegates.TryReadJwtClaim; validationParameters.TypeValidator = ValidationDelegates.TypeValidator; - validationParameters.ActorTokenValidationDelegate = ValidationDelegates.ActorTokenValidationDelegate; + validationParameters.ActClaimRetrieverDelegate = ValidationDelegates.ActClaimRetrieverDelegate; validationParameters.ActorValidationParameters = new TokenValidationParameters(); validationParameters.ClockSkew = TimeSpan.FromSeconds(42); From 5e10aec9fbef5313654fc2df0dd87f7cbdf798d0 Mon Sep 17 00:00:00 2001 From: saurabhsathe-ms Date: Thu, 22 May 2025 11:39:52 -0700 Subject: [PATCH 39/52] Added a testcase to test if act claim was properly deserialized --- .../JsonWebTokenHandler.cs | 2 +- .../ActorClaimsTests.cs | 88 +++++++++++++++++++ 2 files changed, 89 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs index 572c66c48f..db12f15d36 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs @@ -263,7 +263,7 @@ private ClaimsIdentity CreateClaimsIdentityWithMapping(JsonWebToken jwtToken, To if (identity.Actor != null) throw LogHelper.LogExceptionMessage(new InvalidOperationException(LogHelper.FormatInvariant( LogMessages.IDX14112, - LogHelper.MarkAsNonPII(JwtRegisteredClaimNames.Actort), + LogHelper.MarkAsNonPII(validationParameters.ActorClaimName), actClaim.ToString()))); if (validationParameters.ActClaimRetrieverDelegate != null) { diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs index edb300cd2c..76e882f9b9 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs @@ -1346,7 +1346,95 @@ public void WhenActClaimIsNotAnObject_ShouldBeAddedAsRegularClaim() AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); } } + [ResetAppContextSwitches] + [Fact] + public async Task ValidateTokenAsync_WithActorInToken_ProvidesActorClaimsIdentity() + { + var context = new CompareContext($"{this}.ValidateTokenAsync_WithActorInToken_ProvidesActorClaimsIdentity"); + bool switchValue = false; + AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); + try + { + // Create a token with an actor claim + var handler = new JsonWebTokenHandler(); + + var actorIdentity = new CaseSensitiveClaimsIdentity("ActorAuth"); + actorIdentity.AddClaim(new Claim("sub", "actor-subject-id")); + actorIdentity.AddClaim(new Claim("name", "Actor Name")); + actorIdentity.AddClaim(new Claim("role", "admin")); + + var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); + mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); + mainIdentity.AddClaim(new Claim("name", "Main User")); + + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = mainIdentity, + Issuer = "https://example.com", + Audience = "https://api.example.com", + Expires = DateTime.UtcNow.AddHours(1), + SigningCredentials = Default.AsymmetricSigningCredentials, + Claims = new Dictionary + { + { "act", actorIdentity} + }, + ActorClaimName = "act", + MaxActorChainLength = 5 + }; + string token = handler.CreateToken(tokenDescriptor); + handler.MapInboundClaims = true; + + // Validate token + var validationParameters = new TokenValidationParameters + { + ValidateIssuer = false, + ValidateAudience = false, + ValidateLifetime = false, + IssuerSigningKey = Default.AsymmetricSigningKey, + ValidateIssuerSigningKey = true, + ActorClaimName = "act", + }; + + var result = await handler.ValidateTokenAsync(token, validationParameters); + + // Verify validation succeeded + Assert.True(result.IsValid); + Assert.NotNull(result.SecurityToken); + Assert.NotNull(result.ClaimsIdentity); + + // Verify main claims + var mainClaim = result.ClaimsIdentity.Claims.FirstOrDefault(c => c.Type == "name"); + Assert.NotNull(mainClaim); + Assert.Equal("Main User", mainClaim.Value); + Console.WriteLine($"Verified main claims"); + + // Verify actor claims identity + Assert.NotNull(result.ClaimsIdentity.Actor); + var actorSubClaim = result.ClaimsIdentity.Actor.Claims.FirstOrDefault(c => c.Type == "sub"); + var actorNameClaim = result.ClaimsIdentity.Actor.Claims.FirstOrDefault(c => c.Type == "name"); + var actorRoleClaim = result.ClaimsIdentity.Actor.Claims.FirstOrDefault(c => c.Type == "role"); + Assert.NotNull(actorSubClaim); + Assert.NotNull(actorNameClaim); + Assert.NotNull(actorRoleClaim); + Console.WriteLine($"Verified actor claim"); + + Assert.Equal("actor-subject-id", actorSubClaim.Value); + Assert.Equal("Actor Name", actorNameClaim.Value); + Assert.Equal("admin", actorRoleClaim.Value); + TestUtilities.AssertFailIfErrors(context); + } + catch (Exception ex) + { + context.Diffs.Add($"Exception: {ex}"); + TestUtilities.AssertFailIfErrors(context); + } + finally + { + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); + } + } } } From a7398a8b58b94463160b972a12b6197724957d85 Mon Sep 17 00:00:00 2001 From: saurabhsathe-ms Date: Fri, 23 May 2025 10:36:07 -0700 Subject: [PATCH 40/52] Created delegate and added testcases --- .../JsonWebTokenHandler.cs | 143 +++--- .../PublicAPI.Unshipped.txt | 2 +- .../Delegates.cs | 3 +- .../ActorClaimsTests.cs | 429 +++++++++++++++++- .../ValidationDelegates.cs | 3 +- 5 files changed, 509 insertions(+), 71 deletions(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs index db12f15d36..6c81f99f7c 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs @@ -212,7 +212,7 @@ protected virtual ClaimsIdentity CreateClaimsIdentity(JsonWebToken jwtToken, Tok private ClaimsIdentity CreateClaimsIdentityWithMapping(JsonWebToken jwtToken, TokenValidationParameters validationParameters, string issuer) { _ = validationParameters ?? throw LogHelper.LogArgumentNullException(nameof(validationParameters)); - + Console.WriteLine("We are inside writing actor"); ClaimsIdentity identity = validationParameters.CreateClaimsIdentity(jwtToken, issuer); foreach (Claim jwtClaim in jwtToken.Claims) { @@ -221,19 +221,15 @@ private ClaimsIdentity CreateClaimsIdentityWithMapping(JsonWebToken jwtToken, To if (!wasMapped) claimType = jwtClaim.Type; - if (claimType == validationParameters.ActorClaimName && !AppContextSwitches.EnableActClaimSupport) + if (claimType == validationParameters.ActorClaimName) { if (identity.Actor != null) throw LogHelper.LogExceptionMessage(new InvalidOperationException(LogHelper.FormatInvariant( LogMessages.IDX14112, LogHelper.MarkAsNonPII(JwtRegisteredClaimNames.Actort), jwtClaim.Value))); - - if (CanReadToken(jwtClaim.Value)) - { - JsonWebToken actor = ReadToken(jwtClaim.Value) as JsonWebToken; - identity.Actor = CreateClaimsIdentity(actor, validationParameters); - } + Console.WriteLine("CreateClaimsIdentityWithMapping"); + identity.Actor = CreateClaimsIdentityActor(jwtToken, jwtClaim.Value, validationParameters); } if (wasMapped) @@ -255,49 +251,6 @@ private ClaimsIdentity CreateClaimsIdentityWithMapping(JsonWebToken jwtToken, To identity.AddClaim(jwtClaim); } } - if (AppContextSwitches.EnableActClaimSupport) - { - - if (jwtToken.TryGetPayloadValue(validationParameters.ActorClaimName, out JsonElement actClaim)) - { - if (identity.Actor != null) - throw LogHelper.LogExceptionMessage(new InvalidOperationException(LogHelper.FormatInvariant( - LogMessages.IDX14112, - LogHelper.MarkAsNonPII(validationParameters.ActorClaimName), - actClaim.ToString()))); - if (validationParameters.ActClaimRetrieverDelegate != null) - { - try - { - identity.Actor = validationParameters.ActClaimRetrieverDelegate(actClaim); - } - catch (Exception ex) - { - throw LogHelper.LogExceptionMessage(new SecurityTokenDecryptionFailedException(LogHelper.FormatInvariant( - LogMessages.IDX14313, - LogHelper.MarkAsNonPII(JwtRegisteredClaimNames.Actort), - actClaim.ToString(), - ex))); - } - } - else - { - try - { - identity.Actor = CreateActorClaimsIdentityFromJsonElement(actClaim, validationParameters); - } - catch (Exception ex) - { - throw LogHelper.LogExceptionMessage(new SecurityTokenDecryptionFailedException(LogHelper.FormatInvariant( - LogMessages.IDX14313, - LogHelper.MarkAsNonPII(JwtRegisteredClaimNames.Actort), - actClaim.ToString(), - ex))); - } - - } - } - } return identity; } @@ -328,16 +281,12 @@ private ClaimsIdentity CreateClaimsIdentityPrivate(JsonWebToken jwtToken, TokenV foreach (Claim jwtClaim in jwtToken.Claims) { string claimType = jwtClaim.Type; - if (claimType == ClaimTypes.Actor) + if (claimType == validationParameters.ActorClaimName) { if (identity.Actor != null) throw LogHelper.LogExceptionMessage(new InvalidOperationException(LogHelper.FormatInvariant(LogMessages.IDX14112, LogHelper.MarkAsNonPII(JwtRegisteredClaimNames.Actort), jwtClaim.Value))); - - if (CanReadToken(jwtClaim.Value)) - { - JsonWebToken actor = ReadToken(jwtClaim.Value) as JsonWebToken; - identity.Actor = CreateClaimsIdentity(actor, validationParameters, issuer); - } + Console.WriteLine("CreateClaimsIdentityPrivate"); + identity.Actor = CreateClaimsIdentityActor(jwtToken, jwtClaim.Value, validationParameters); } if (jwtClaim.Properties.Count == 0) @@ -615,19 +564,86 @@ private static TokenValidationResult ReadToken(string token, TokenValidationPara }; } + /// + /// Creates a ClaimsIdentity from an actor claim string. + /// + /// + /// The actor claim string. + /// The token validation parameters. + /// A ClaimsIdentity representing the actor. + /// Thrown if or is null. + private ClaimsIdentity CreateClaimsIdentityActor( + JsonWebToken jwtToken, + string actorString, + TokenValidationParameters tokenValidationParameters) + { + if (string.IsNullOrEmpty(actorString)) + throw LogHelper.LogArgumentNullException(nameof(actorString)); + + if (tokenValidationParameters == null) + throw LogHelper.LogArgumentNullException(nameof(tokenValidationParameters)); + + if (AppContextSwitches.EnableActClaimSupport) + { + if (jwtToken.TryGetPayloadValue(tokenValidationParameters.ActorClaimName, out JsonElement actClaim)) + { + if (tokenValidationParameters.ActClaimRetrieverDelegate != null) + { + try + { + return tokenValidationParameters.ActClaimRetrieverDelegate(actClaim); + } + catch (Exception ex) + { + throw LogHelper.LogExceptionMessage(new SecurityTokenDecryptionFailedException(LogHelper.FormatInvariant( + LogMessages.IDX14313, + LogHelper.MarkAsNonPII(tokenValidationParameters.ActorClaimName), + actClaim.ToString(), + ex))); + } + } + else + { + try + { + return CreateActorClaimsIdentityFromJsonElement(actClaim, tokenValidationParameters); + } + catch (Exception ex) + { + throw LogHelper.LogExceptionMessage(new SecurityTokenDecryptionFailedException(LogHelper.FormatInvariant( + LogMessages.IDX14313, + LogHelper.MarkAsNonPII(tokenValidationParameters.ActorClaimName), + actClaim.ToString(), + ex))); + } + + } + } + + } + else + { + if (CanReadToken(actorString)) + { + JsonWebToken actor = ReadToken(actorString) as JsonWebToken; + return CreateClaimsIdentity(actor, tokenValidationParameters); + } + } + + return null; + } + /// /// Creates a ClaimsIdentity from a JsonElement that represents an actor token. /// /// The JsonElement containing actor claims. /// These parameters have details like nested actor chain length and max permissible actor length /// The issuer for the claims. - /// The authentication type for the identity. /// A ClaimsIdentity containing claims from the JsonElement. public static ClaimsIdentity CreateActorClaimsIdentityFromJsonElement( JsonElement jsonElement, TokenValidationParameters tokenValidationParameters, - string issuer = null, - string authenticationType = "Actor") + string issuer = null) { if (tokenValidationParameters == null) throw LogHelper.LogArgumentNullException(nameof(tokenValidationParameters)); @@ -645,7 +661,7 @@ public static ClaimsIdentity CreateActorClaimsIdentityFromJsonElement( throw LogHelper.LogExceptionMessage(new ArgumentException("Actor token must be a JSON object")); // Use CaseSensitiveClaimsIdentity for consistent behavior with the rest of the library - var identity = new CaseSensitiveClaimsIdentity(authenticationType); + var identity = new CaseSensitiveClaimsIdentity(); issuer = issuer ?? ClaimsIdentity.DefaultIssuer; @@ -662,7 +678,7 @@ public static ClaimsIdentity CreateActorClaimsIdentityFromJsonElement( tokenValidationParameters.ActorChainDepth++; // Recursively create nested actor identity identity.Actor = CreateActorClaimsIdentityFromJsonElement( - value, tokenValidationParameters, issuer, authenticationType); + value, tokenValidationParameters, issuer); } continue; } @@ -687,6 +703,5 @@ public static ClaimsIdentity CreateActorClaimsIdentityFromJsonElement( return identity; } - } } diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.JsonWebTokens/PublicAPI.Unshipped.txt index d8d9447078..aa5db6c656 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.JsonWebTokens/PublicAPI.Unshipped.txt @@ -1 +1 @@ -static Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.CreateActorClaimsIdentityFromJsonElement(System.Text.Json.JsonElement jsonElement, Microsoft.IdentityModel.Tokens.TokenValidationParameters tokenValidationParameters, string issuer = null, string authenticationType = "Actor") -> System.Security.Claims.ClaimsIdentity \ No newline at end of file +static Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.CreateActorClaimsIdentityFromJsonElement(System.Text.Json.JsonElement jsonElement, Microsoft.IdentityModel.Tokens.TokenValidationParameters tokenValidationParameters, string issuer = null) -> System.Security.Claims.ClaimsIdentity diff --git a/src/Microsoft.IdentityModel.Tokens/Delegates.cs b/src/Microsoft.IdentityModel.Tokens/Delegates.cs index aca30eb2d3..9fe7e4a2cb 100644 --- a/src/Microsoft.IdentityModel.Tokens/Delegates.cs +++ b/src/Microsoft.IdentityModel.Tokens/Delegates.cs @@ -241,7 +241,6 @@ internal delegate ValidationResult SignatureValidationDelegate( /// Delegate to validate the 'act' claim and create actor's ClaimsIdentity. /// /// The JSON element representing the 'act' claim. - /// The token validation parameters. /// A ClaimsIdentity representing the actor. - public delegate ClaimsIdentity ActClaimRetrieverDelegate(JsonElement actClaim, TokenValidationParameters validationParameters = null); + public delegate ClaimsIdentity ActClaimRetrieverDelegate(JsonElement actClaim); } diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs index 76e882f9b9..62bac291bd 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs @@ -1228,8 +1228,7 @@ public void CustomAuthenticationTypeShouldBeRespected() var identity = JsonWebTokenHandler.CreateActorClaimsIdentityFromJsonElement( jsonElement, tokenValidationParameters, - null, // Default issuer - customAuthType); + null); // Verify custom auth type was applied Assert.Equal(customAuthType, identity.AuthenticationType); @@ -1435,6 +1434,432 @@ public async Task ValidateTokenAsync_WithActorInToken_ProvidesActorClaimsIdentit AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); } } + [ResetAppContextSwitches] + [Fact] + public async Task ValidateTokenAsync_CustomDelegate_WorksWithSimpleAndNestedActors() + { + var context = new CompareContext($"{this}.ValidateTokenAsync_CustomDelegate_WorksWithSimpleAndNestedActors"); + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); + + int delegateCallCount = 0; + ClaimsIdentity CustomDelegate(JsonElement element) + { + delegateCallCount++; + var id = new CaseSensitiveClaimsIdentity("CustomActorAuth"); + if (element.TryGetProperty("sub", out var sub)) + id.AddClaim(new Claim("sub", sub.GetString())); + if (element.TryGetProperty("name", out var name)) + id.AddClaim(new Claim("name", name.GetString())); + if (element.TryGetProperty("act", out var nested) && nested.ValueKind == JsonValueKind.Object) + id.Actor = CustomDelegate(nested); + return id; + } + + try + { + // Nested actor + var nestedActor = new CaseSensitiveClaimsIdentity("NestedActorAuth"); + nestedActor.AddClaim(new Claim("sub", "nested-actor-id")); + nestedActor.AddClaim(new Claim("name", "Nested Actor")); + + var actor = new CaseSensitiveClaimsIdentity("ActorAuth"); + actor.AddClaim(new Claim("sub", "actor-subject-id")); + actor.AddClaim(new Claim("name", "Actor Name")); + actor.Actor = nestedActor; + + var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); + mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); + mainIdentity.AddClaim(new Claim("name", "Main User")); + + var handler = new JsonWebTokenHandler(); + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = mainIdentity, + Issuer = "https://example.com", + Audience = "https://api.example.com", + Expires = DateTime.UtcNow.AddHours(1), + SigningCredentials = Default.AsymmetricSigningCredentials, + Claims = new Dictionary { { "act", actor } } + }; + var token = handler.CreateToken(tokenDescriptor); + + var validationParameters = new TokenValidationParameters + { + ValidateIssuer = false, + ValidateAudience = false, + ValidateLifetime = false, + IssuerSigningKey = Default.AsymmetricSigningKey, + ValidateIssuerSigningKey = true, + ActorClaimName = "act", + MaxActorChainLength = 3, + ActClaimRetrieverDelegate = CustomDelegate + }; + + var result = await handler.ValidateTokenAsync(token, validationParameters); + Assert.True(result.IsValid); + Assert.NotNull(result.ClaimsIdentity.Actor); + Assert.Equal("actor-subject-id", result.ClaimsIdentity.Actor.Claims.First(c => c.Type == "sub").Value); + Assert.NotNull(result.ClaimsIdentity.Actor.Actor); + Assert.Equal("nested-actor-id", result.ClaimsIdentity.Actor.Actor.Claims.First(c => c.Type == "sub").Value); + Assert.True(delegateCallCount >= 2); + + TestUtilities.AssertFailIfErrors(context); + } + finally + { + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); + } + } + + [ResetAppContextSwitches] + [Fact] + public async Task ValidateTokenAsync_NestedActors_DefaultDelegate_CreatesProperClaimsIdentity() + { + var context = new CompareContext($"{this}.ValidateTokenAsync_NestedActors_DefaultDelegate_CreatesProperClaimsIdentity"); + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); + + try + { + var nestedActor = new CaseSensitiveClaimsIdentity("NestedActorAuth"); + nestedActor.AddClaim(new Claim("sub", "nested-actor-id")); + nestedActor.AddClaim(new Claim("name", "Nested Actor")); + + var actor = new CaseSensitiveClaimsIdentity("ActorAuth"); + actor.AddClaim(new Claim("sub", "actor-subject-id")); + actor.AddClaim(new Claim("name", "Actor Name")); + actor.Actor = nestedActor; + + var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); + mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); + mainIdentity.AddClaim(new Claim("name", "Main User")); + + var handler = new JsonWebTokenHandler(); + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = mainIdentity, + Issuer = "https://example.com", + Audience = "https://api.example.com", + Expires = DateTime.UtcNow.AddHours(1), + SigningCredentials = Default.AsymmetricSigningCredentials, + Claims = new Dictionary { { "act", actor } } + }; + var token = handler.CreateToken(tokenDescriptor); + + var validationParameters = new TokenValidationParameters + { + ValidateIssuer = false, + ValidateAudience = false, + ValidateLifetime = false, + IssuerSigningKey = Default.AsymmetricSigningKey, + ValidateIssuerSigningKey = true, + ActorClaimName = "act", + MaxActorChainLength = 2 + }; + + var result = await handler.ValidateTokenAsync(token, validationParameters); + Assert.True(result.IsValid); + Assert.NotNull(result.ClaimsIdentity.Actor); + Assert.Equal("actor-subject-id", result.ClaimsIdentity.Actor.Claims.First(c => c.Type == "sub").Value); + Assert.NotNull(result.ClaimsIdentity.Actor.Actor); + Assert.Equal("nested-actor-id", result.ClaimsIdentity.Actor.Actor.Claims.First(c => c.Type == "sub").Value); + + TestUtilities.AssertFailIfErrors(context); + } + finally + { + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); + } + } + + [ResetAppContextSwitches] + [Fact] + public async Task ValidateTokenAsync_NestingBeyondMaxActorChain_ThrowsException() + { + var context = new CompareContext($"{this}.ValidateTokenAsync_NestingBeyondMaxActorChain_ThrowsException"); + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); + + try + { + var level3Actor = new CaseSensitiveClaimsIdentity("Level3Auth"); + level3Actor.AddClaim(new Claim("sub", "level3-actor")); + + var level2Actor = new CaseSensitiveClaimsIdentity("Level2Auth"); + level2Actor.AddClaim(new Claim("sub", "level2-actor")); + level2Actor.Actor = level3Actor; + + var level1Actor = new CaseSensitiveClaimsIdentity("Level1Auth"); + level1Actor.AddClaim(new Claim("sub", "level1-actor")); + level1Actor.Actor = level2Actor; + + var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); + mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); + + var handler = new JsonWebTokenHandler(); + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = mainIdentity, + Issuer = "https://example.com", + Audience = "https://api.example.com", + Expires = DateTime.UtcNow.AddHours(1), + SigningCredentials = Default.AsymmetricSigningCredentials, + Claims = new Dictionary { { "act", level1Actor } } + }; + var token = handler.CreateToken(tokenDescriptor); + + var validationParameters = new TokenValidationParameters + { + ValidateIssuer = false, + ValidateAudience = false, + ValidateLifetime = false, + IssuerSigningKey = Default.AsymmetricSigningKey, + ValidateIssuerSigningKey = true, + ActorClaimName = "act", + MaxActorChainLength = 2 + }; + handler.MapInboundClaims = true; + var result = await handler.ValidateTokenAsync(token, validationParameters); + foreach (Claim claim in result.ClaimsIdentity.Claims) + { + Console.WriteLine($"Claim Type: {claim.Type}, Value: {claim.Value}"); + } + Assert.False(result.IsValid); + Assert.NotNull(result.Exception); + Assert.Contains("IDX14313", result.Exception.ToString()); + + TestUtilities.AssertFailIfErrors(context); + } + catch (Exception ex) + { + Console.WriteLine($"Exception: {ex}"); + } + finally + { + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); + } + } + + [ResetAppContextSwitches] + [Fact] + public async Task ValidateTokenAsync_NestingBeyondMaxActorChain_CustomDelegate_ThrowsException() + { + var context = new CompareContext($"{this}.ValidateTokenAsync_NestingBeyondMaxActorChain_CustomDelegate_ThrowsException"); + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); + + ClaimsIdentity CustomDelegate(JsonElement element) + { + var id = new CaseSensitiveClaimsIdentity("CustomActorAuth"); + if (element.TryGetProperty("sub", out var sub)) + id.AddClaim(new Claim("sub", sub.GetString())); + if (element.TryGetProperty("act", out var nested) && nested.ValueKind == System.Text.Json.JsonValueKind.Object) + id.Actor = CustomDelegate(nested); + return id; + } + + try + { + var level3Actor = new CaseSensitiveClaimsIdentity("Level3Auth"); + level3Actor.AddClaim(new Claim("sub", "level3-actor")); + + var level2Actor = new CaseSensitiveClaimsIdentity("Level2Auth"); + level2Actor.AddClaim(new Claim("sub", "level2-actor")); + level2Actor.Actor = level3Actor; + + var level1Actor = new CaseSensitiveClaimsIdentity("Level1Auth"); + level1Actor.AddClaim(new Claim("sub", "level1-actor")); + level1Actor.Actor = level2Actor; + + var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); + mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); + + var handler = new JsonWebTokenHandler(); + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = mainIdentity, + Issuer = "https://example.com", + Audience = "https://api.example.com", + Expires = DateTime.UtcNow.AddHours(1), + SigningCredentials = Default.AsymmetricSigningCredentials, + Claims = new Dictionary { { "act", level1Actor } } + }; + var token = handler.CreateToken(tokenDescriptor); + + var validationParameters = new TokenValidationParameters + { + ValidateIssuer = false, + ValidateAudience = false, + ValidateLifetime = false, + IssuerSigningKey = Default.AsymmetricSigningKey, + ValidateIssuerSigningKey = true, + ActorClaimName = "act", + MaxActorChainLength = 2, + //ActClaimRetrieverDelegate = CustomDelegate + }; + + var result = await handler.ValidateTokenAsync(token, validationParameters); + Assert.NotNull(result.ClaimsIdentity.Actor); + TestUtilities.AssertFailIfErrors(context); + } + catch (Exception ex) + { + Assert.Contains("IDX14313", ex.ToString()); ; + } + finally + { + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); + } + } + + [ResetAppContextSwitches] + [Fact] + public async Task ValidateTokenAsync_CustomDelegate_ThrowsExceptionIfDelegateFails() + { + var context = new CompareContext($"{this}.ValidateTokenAsync_CustomDelegate_ThrowsIfDelegateFails"); + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); + + ClaimsIdentity CustomDelegate(JsonElement element) + { + throw new InvalidOperationException("Delegate failure"); + } + + try + { + var actor = new CaseSensitiveClaimsIdentity("ActorAuth"); + actor.AddClaim(new Claim("sub", "actor-subject-id")); + + var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); + mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); + + var handler = new JsonWebTokenHandler(); + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = mainIdentity, + Issuer = "https://example.com", + Audience = "https://api.example.com", + Expires = DateTime.UtcNow.AddHours(1), + SigningCredentials = Default.AsymmetricSigningCredentials, + Claims = new Dictionary { { "act", actor } } + }; + var token = handler.CreateToken(tokenDescriptor); + + var validationParameters = new TokenValidationParameters + { + ValidateIssuer = false, + ValidateAudience = false, + ValidateLifetime = false, + IssuerSigningKey = Default.AsymmetricSigningKey, + ValidateIssuerSigningKey = true, + ActorClaimName = "act", + MaxActorChainLength = 2, + ActClaimRetrieverDelegate = CustomDelegate + }; + + var result = await handler.ValidateTokenAsync(token, validationParameters); + Assert.Null(result.ClaimsIdentity.Actor); + TestUtilities.AssertFailIfErrors(context); + } + catch (Exception ex) + { + Assert.Contains("IDX14313", ex.ToString()); ; + } + finally + { + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); + } + } + + [ResetAppContextSwitches] + [Fact] + public async Task ValidateTokenAsync_ActorAsSubjectAndClaimsDictionary_DefaultAndCustomDelegate() + { + var context = new CompareContext($"{this}.ValidateTokenAsync_ActorAsSubjectAndClaimsDictionary_DefaultAndCustomDelegate"); + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); + + ClaimsIdentity CustomDelegate(JsonElement element) + { + var id = new CaseSensitiveClaimsIdentity("CustomActorAuth"); + if (element.TryGetProperty("sub", out var sub)) + id.AddClaim(new Claim("sub", sub.GetString())); + return id; + } + + try + { + // Actor as Subject + var actorAsSubject = new CaseSensitiveClaimsIdentity("ActorAuth"); + actorAsSubject.AddClaim(new Claim("sub", "actor-subject-id")); + + var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); + mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); + mainIdentity.Actor = actorAsSubject; + + var handler = new JsonWebTokenHandler(); + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = mainIdentity, + Issuer = "https://example.com", + Audience = "https://api.example.com", + Expires = DateTime.UtcNow.AddHours(1), + SigningCredentials = Default.AsymmetricSigningCredentials + }; + var token = handler.CreateToken(tokenDescriptor); + + var validationParameters = new TokenValidationParameters + { + ValidateIssuer = false, + ValidateAudience = false, + ValidateLifetime = false, + IssuerSigningKey = Default.AsymmetricSigningKey, + ValidateIssuerSigningKey = true, + ActorClaimName = "act" + }; + + // Default delegate + var result = await handler.ValidateTokenAsync(token, validationParameters); + Assert.True(result.IsValid); + Assert.NotNull(result.ClaimsIdentity.Actor); + Assert.Equal("actor-subject-id", result.ClaimsIdentity.Actor.Claims.First(c => c.Type == "sub").Value); + + // Custom delegate + validationParameters.ActClaimRetrieverDelegate = CustomDelegate; + var result2 = await handler.ValidateTokenAsync(token, validationParameters); + Assert.True(result2.IsValid); + Assert.NotNull(result2.ClaimsIdentity.Actor); + Assert.Equal("actor-subject-id", result2.ClaimsIdentity.Actor.Claims.First(c => c.Type == "sub").Value); + + // Actor in both Subject and Claims dictionary, Claims dictionary should take precedence + var subjectActor = new CaseSensitiveClaimsIdentity("SubjectActorAuth"); + subjectActor.AddClaim(new Claim("sub", "subject-actor-id")); + + var claimsActor = new CaseSensitiveClaimsIdentity("ClaimsActorAuth"); + claimsActor.AddClaim(new Claim("sub", "claims-actor-id")); + + var mainIdentity2 = new CaseSensitiveClaimsIdentity("Bearer"); + mainIdentity2.AddClaim(new Claim("sub", "main-subject-id")); + mainIdentity2.Actor = subjectActor; + + var tokenDescriptor2 = new SecurityTokenDescriptor + { + Subject = mainIdentity2, + Issuer = "https://example.com", + Audience = "https://api.example.com", + Expires = DateTime.UtcNow.AddHours(1), + SigningCredentials = Default.AsymmetricSigningCredentials, + Claims = new Dictionary { { "act", claimsActor } } + }; + var token2 = handler.CreateToken(tokenDescriptor2); + + var result3 = await handler.ValidateTokenAsync(token2, validationParameters); + Assert.True(result3.IsValid); + Assert.NotNull(result3.ClaimsIdentity.Actor); + Assert.Equal("claims-actor-id", result3.ClaimsIdentity.Actor.Claims.First(c => c.Type == "sub").Value); + + TestUtilities.AssertFailIfErrors(context); + } + finally + { + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); + } + } } } diff --git a/test/Microsoft.IdentityModel.TestUtils/ValidationDelegates.cs b/test/Microsoft.IdentityModel.TestUtils/ValidationDelegates.cs index 5dfebe30ba..0b809d950b 100644 --- a/test/Microsoft.IdentityModel.TestUtils/ValidationDelegates.cs +++ b/test/Microsoft.IdentityModel.TestUtils/ValidationDelegates.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.IdentityModel.Tokens.Jwt; -using System.Security.Claims; using System.Text.Json; using System.Threading.Tasks; using Microsoft.IdentityModel.JsonWebTokens; @@ -252,7 +251,7 @@ public static string TypeValidator(string type, SecurityToken securityToken, Tok return type; } - public static readonly ActClaimRetrieverDelegate ActClaimRetrieverDelegate = (actorClaim, validationParameters) => + public static readonly ActClaimRetrieverDelegate ActClaimRetrieverDelegate = (actorClaim) => { return null; }; From a8efcb83474e643dc0c84c24d8b99e96c9cce06e Mon Sep 17 00:00:00 2001 From: saurabhsathe-ms Date: Fri, 23 May 2025 13:37:35 -0700 Subject: [PATCH 41/52] Fixed one bug in Deserialization. Divided all the testcases in 2 parts. Solved a major bug in TokenValidation --- .../JsonWebTokenHandler.cs | 23 +- .../LogMessages.cs | 3 +- .../TokenValidationParameters.cs | 3 + .../ActClaimDeserializationTests.cs} | 886 +----------------- .../ActClaimSerializationTests.cs | 670 +++++++++++++ 5 files changed, 683 insertions(+), 902 deletions(-) rename test/Microsoft.IdentityModel.JsonWebTokens.Tests/{ActorClaimsTests.cs => ActClaimTests/ActClaimDeserializationTests.cs} (50%) create mode 100644 test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActClaimTests/ActClaimSerializationTests.cs diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs index 6c81f99f7c..af746a63f1 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs @@ -212,7 +212,6 @@ protected virtual ClaimsIdentity CreateClaimsIdentity(JsonWebToken jwtToken, Tok private ClaimsIdentity CreateClaimsIdentityWithMapping(JsonWebToken jwtToken, TokenValidationParameters validationParameters, string issuer) { _ = validationParameters ?? throw LogHelper.LogArgumentNullException(nameof(validationParameters)); - Console.WriteLine("We are inside writing actor"); ClaimsIdentity identity = validationParameters.CreateClaimsIdentity(jwtToken, issuer); foreach (Claim jwtClaim in jwtToken.Claims) { @@ -228,7 +227,6 @@ private ClaimsIdentity CreateClaimsIdentityWithMapping(JsonWebToken jwtToken, To LogMessages.IDX14112, LogHelper.MarkAsNonPII(JwtRegisteredClaimNames.Actort), jwtClaim.Value))); - Console.WriteLine("CreateClaimsIdentityWithMapping"); identity.Actor = CreateClaimsIdentityActor(jwtToken, jwtClaim.Value, validationParameters); } @@ -285,7 +283,6 @@ private ClaimsIdentity CreateClaimsIdentityPrivate(JsonWebToken jwtToken, TokenV { if (identity.Actor != null) throw LogHelper.LogExceptionMessage(new InvalidOperationException(LogHelper.FormatInvariant(LogMessages.IDX14112, LogHelper.MarkAsNonPII(JwtRegisteredClaimNames.Actort), jwtClaim.Value))); - Console.WriteLine("CreateClaimsIdentityPrivate"); identity.Actor = CreateClaimsIdentityActor(jwtToken, jwtClaim.Value, validationParameters); } @@ -596,27 +593,13 @@ private ClaimsIdentity CreateClaimsIdentityActor( catch (Exception ex) { throw LogHelper.LogExceptionMessage(new SecurityTokenDecryptionFailedException(LogHelper.FormatInvariant( - LogMessages.IDX14313, - LogHelper.MarkAsNonPII(tokenValidationParameters.ActorClaimName), - actClaim.ToString(), - ex))); + LogMessages.IDX14314, + LogHelper.MarkAsNonPII(ex.ToString())))); } } else { - try - { - return CreateActorClaimsIdentityFromJsonElement(actClaim, tokenValidationParameters); - } - catch (Exception ex) - { - throw LogHelper.LogExceptionMessage(new SecurityTokenDecryptionFailedException(LogHelper.FormatInvariant( - LogMessages.IDX14313, - LogHelper.MarkAsNonPII(tokenValidationParameters.ActorClaimName), - actClaim.ToString(), - ex))); - } - + return CreateActorClaimsIdentityFromJsonElement(actClaim, tokenValidationParameters); } } diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/LogMessages.cs b/src/Microsoft.IdentityModel.JsonWebTokens/LogMessages.cs index 4fba1383ae..2c08c4baa6 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/LogMessages.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/LogMessages.cs @@ -51,6 +51,7 @@ internal static class LogMessages internal const string IDX14310 = "IDX14310: JWE authentication tag is missing."; internal const string IDX14311 = "IDX14311: Unable to decode the authentication tag as a Base64Url encoded string."; internal const string IDX14312 = "IDX14312: Unable to decode the cipher text as a Base64Url encoded string."; - internal const string IDX14313 = "IDX14313: Unable to serialize/deserialize actor token. Maximum actor token depth reached. Current nesting depth is {0} while max depth set is {1}"; + internal const string IDX14313 = "IDX14313: Unable to serialize/deserialize act claim. Maximum actor token depth reached. Current nesting depth is {0} while max depth set is {1}"; + internal const string IDX14314 = "IDX14314: Unable to deserialize act claim. Exception faced while using custom delegate to deserialize act claim. Nested exception is :{0}"; } } diff --git a/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs b/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs index 92b0a295b0..596a2ccec0 100644 --- a/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs +++ b/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs @@ -130,6 +130,9 @@ protected TokenValidationParameters(TokenValidationParameters other) ValidIssuers = other.ValidIssuers; ValidTypes = other.ValidTypes; ActClaimRetrieverDelegate = other.ActClaimRetrieverDelegate; + MaxActorChainLength = other.MaxActorChainLength; + ActorChainDepth = other.ActorChainDepth; + ActorClaimName = other.ActorClaimName; } /// diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActClaimTests/ActClaimDeserializationTests.cs similarity index 50% rename from test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs rename to test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActClaimTests/ActClaimDeserializationTests.cs index 62bac291bd..9a907ee53d 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActClaimTests/ActClaimDeserializationTests.cs @@ -7,669 +7,13 @@ using System.Text.Json; using Microsoft.IdentityModel.TestUtils; using Microsoft.IdentityModel.Tokens; -using Microsoft.IdentityModel.JsonWebTokens; using Xunit; using System.Threading.Tasks; using System.Linq; -namespace Microsoft.IdentityModel.Tests +namespace Microsoft.IdentityModel.JsonWebTokens.Tests.ActClaimTests { - public class ActorClaimsTests + public class ActClaimDeserializationTests { - [ResetAppContextSwitches] - [Fact] - public void ActorTokenInClaimsDictionaryShouldBeProperlySerialized() - { - var context = new CompareContext($"{this}.ActorTokenInClaimsDictionaryShouldBeProperlySerialized"); - bool switchValue = false; - AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); - string actorname = "act"; - try - { - // Create a ClaimsIdentity for the actor - var actorIdentity = new CaseSensitiveClaimsIdentity("ActorAuth"); - actorIdentity.AddClaim(new Claim("sub", "actor-subject-id")); - actorIdentity.AddClaim(new Claim("name", "Actor Name")); - actorIdentity.AddClaim(new Claim("role", "admin")); - - // Create the main identity - var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); - mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); - mainIdentity.AddClaim(new Claim("name", "Main User")); - - // Create a token with JsonWebTokenHandler where actor is in Claims dictionary - var tokenHandler = new JsonWebTokenHandler(); - var tokenDescriptor = new SecurityTokenDescriptor - { - Subject = mainIdentity, - Issuer = "https://example.com", - Audience = "https://api.example.com", - Expires = DateTime.UtcNow.AddHours(1), - SigningCredentials = Default.AsymmetricSigningCredentials, - Claims = new Dictionary - { - { actorname, actorIdentity } - } - }; - var token = tokenHandler.CreateToken(tokenDescriptor); - JsonWebToken decodedToken = tokenHandler.ReadJsonWebToken(token); - - // Verify actor claim exists in the token - Assert.True(decodedToken.Payload.HasClaim(tokenDescriptor.ActorClaimName), "JWT token should contain 'actort' claim"); - // Verify the actor object directly - var actorObject = decodedToken.Payload.GetValue(tokenDescriptor.ActorClaimName); - Assert.Equal(JsonValueKind.Object, actorObject.ValueKind); - - // Verify actor claims directly from the JSON object - Assert.Equal("actor-subject-id", actorObject.GetProperty("sub").GetString()); - Assert.Equal("Actor Name", actorObject.GetProperty("name").GetString()); - Assert.Equal("admin", actorObject.GetProperty("role").GetString()); - TestUtilities.AssertFailIfErrors(context); - } - catch (Exception ex) - { - context.Diffs.Add($"Exception: {ex}"); - } - finally - { - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); - } - } - - [ResetAppContextSwitches] - [Fact] - public void ActorTokenAsSubjectShouldBeProperlySerialized() - { - var context = new CompareContext($"{this}.ActorTokenAsSubjectShouldBeProperlySerialized"); - bool switchValue = false; - AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); - try - { - // Create actor identity - var actorIdentity = new CaseSensitiveClaimsIdentity("ActorAuth"); - actorIdentity.AddClaim(new Claim("sub", "actor-subject-id")); - actorIdentity.AddClaim(new Claim("name", "Actor Name")); - actorIdentity.AddClaim(new Claim("role", "admin")); - - // Create the main identity with Actor set via Identity.Actor - var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); - mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); - mainIdentity.AddClaim(new Claim("name", "Main User")); - mainIdentity.Actor = actorIdentity; - - // Create a token with JsonWebTokenHandler - var tokenHandler = new JsonWebTokenHandler(); - var tokenDescriptor = new SecurityTokenDescriptor - { - Subject = mainIdentity, - Issuer = "https://example.com", - Audience = "https://api.example.com", - Expires = DateTime.UtcNow.AddHours(1), - SigningCredentials = Default.AsymmetricSigningCredentials - }; - var token = tokenHandler.CreateToken(tokenDescriptor); - JsonWebToken decodedToken = tokenHandler.ReadJsonWebToken(token); - - // Verify actor claim exists in the token - Assert.True(decodedToken.Payload.HasClaim(tokenDescriptor.ActorClaimName), "JWT token should contain 'act' claim"); - - // Verify actor claim exists in the token - Assert.True(decodedToken.Payload.HasClaim(tokenDescriptor.ActorClaimName), "JWT token should contain actor claim"); - - // Verify the actor object directly - var actorObject = decodedToken.Payload.GetValue(tokenDescriptor.ActorClaimName); - Assert.Equal(JsonValueKind.Object, actorObject.ValueKind); - - // Verify actor claims directly from the JSON object - Assert.Equal("actor-subject-id", actorObject.GetProperty("sub").GetString()); - Assert.Equal("Actor Name", actorObject.GetProperty("name").GetString()); - Assert.Equal("admin", actorObject.GetProperty("role").GetString()); - TestUtilities.AssertFailIfErrors(context); - } - catch (Exception ex) - { - context.Diffs.Add($"Exception: {ex}"); - } - finally - { - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); - } - } - - [ResetAppContextSwitches] - [Fact] - public void ActorTokenInBothClaimsAndSubjectShouldPreferClaimsValue() - { - var context = new CompareContext($"{this}.ActorTokenInBothClaimsAndSubjectShouldPreferClaimsValue"); - bool switchValue = false; - string actorname = "act"; - AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); - try - { - // Create actor identity for Subject.Actor (should be ignored) - var subjectActorIdentity = new CaseSensitiveClaimsIdentity("ActorAuth"); - subjectActorIdentity.AddClaim(new Claim("sub", "subject-actor-id")); - subjectActorIdentity.AddClaim(new Claim("name", "Subject Actor")); - - // Create actor identity for Claims dictionary (should be used) - var claimsActorIdentity = new CaseSensitiveClaimsIdentity("ClaimsActorAuth"); - claimsActorIdentity.AddClaim(new Claim("sub", "claims-actor-id")); - claimsActorIdentity.AddClaim(new Claim("name", "Claims Actor")); - - // Create the main identity - var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); - mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); - mainIdentity.AddClaim(new Claim("name", "Main User")); - mainIdentity.Actor = subjectActorIdentity; // Set the actor that should be ignored - - // Create a token with JsonWebTokenHandler - var tokenHandler = new JsonWebTokenHandler(); - var tokenDescriptor = new SecurityTokenDescriptor - { - Subject = mainIdentity, - Issuer = "https://example.com", - Audience = "https://api.example.com", - Expires = DateTime.UtcNow.AddHours(1), - SigningCredentials = Default.AsymmetricSigningCredentials, - // Add Claims actor that should take precedence - Claims = new Dictionary - { - { actorname, claimsActorIdentity } - } - }; - var token = tokenHandler.CreateToken(tokenDescriptor); - JsonWebToken decodedToken = tokenHandler.ReadJsonWebToken(token); - - // Verify actor claim exists - Assert.True(decodedToken.Payload.HasClaim(actorname), "JWT token should contain actor claim"); - - // Verify actor claim exists and is a JSON object - var actorObject = decodedToken.Payload.GetValue("act"); - Assert.Equal(JsonValueKind.Object, actorObject.ValueKind); - - // Verify Claims dictionary actor was used, not Subject.Actor - Assert.Equal("claims-actor-id", actorObject.GetProperty("sub").GetString()); - Assert.Equal("Claims Actor", actorObject.GetProperty("name").GetString()); - Assert.NotEqual("subject-actor-id", actorObject.GetProperty("sub").GetString()); - TestUtilities.AssertFailIfErrors(context); - } - catch (Exception ex) - { - context.Diffs.Add($"Exception: {ex}"); - } - finally - { - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); - } - } - - [ResetAppContextSwitches] - [Fact] - public void NestedActorTokenInClaimsDictionaryShouldBeProperlySerialized() - { - var context = new CompareContext($"{this}.NestedActorTokenInClaimsDictionaryShouldBeProperlySerialized"); - bool switchValue = false; - AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); - try - { - // Create nested actor identity - var nestedActorIdentity = new CaseSensitiveClaimsIdentity("NestedActorAuth"); - nestedActorIdentity.AddClaim(new Claim("sub", "nested-actor-id")); - nestedActorIdentity.AddClaim(new Claim("name", "Nested Actor")); - - // Create actor identity with nested actor - var actorIdentity = new CaseSensitiveClaimsIdentity("ActorAuth"); - actorIdentity.AddClaim(new Claim("sub", "actor-subject-id")); - actorIdentity.AddClaim(new Claim("name", "Actor Name")); - actorIdentity.Actor = nestedActorIdentity; // Set nested actor - - // Create the main identity - var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); - mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); - mainIdentity.AddClaim(new Claim("name", "Main User")); - - // Create a token with JsonWebTokenHandler where actor is in Claims dictionary - var tokenHandler = new JsonWebTokenHandler(); - var tokenDescriptor = new SecurityTokenDescriptor - { - Subject = mainIdentity, - Issuer = "https://example.com", - Audience = "https://api.example.com", - Expires = DateTime.UtcNow.AddHours(1), - SigningCredentials = Default.AsymmetricSigningCredentials, - Claims = new Dictionary - { - { "act", actorIdentity } - } - }; - var token = tokenHandler.CreateToken(tokenDescriptor); - JsonWebToken decodedToken = tokenHandler.ReadJsonWebToken(token); - - // Verify actor claim exists - Assert.True(decodedToken.Payload.HasClaim("act"), "JWT token should contain 'actort' claim"); - - // Verify the actor object - var actorObject = decodedToken.Payload.GetValue("act"); - Assert.Equal(JsonValueKind.Object, actorObject.ValueKind); - - // Verify main actor claims directly from JSON object - Assert.Equal("actor-subject-id", actorObject.GetProperty("sub").GetString()); - Assert.Equal("Actor Name", actorObject.GetProperty("name").GetString()); - - // Verify nested actor exists and is a JSON object - Assert.True(actorObject.TryGetProperty(tokenDescriptor.ActorClaimName, out var nestedActorElement)); - Assert.Equal(JsonValueKind.Object, nestedActorElement.ValueKind); - - // Verify nested actor claims directly from JSON object - Assert.Equal("nested-actor-id", nestedActorElement.GetProperty("sub").GetString()); - Assert.Equal("Nested Actor", nestedActorElement.GetProperty("name").GetString()); - TestUtilities.AssertFailIfErrors(context); - } - catch (Exception ex) - { - context.Diffs.Add($"Exception: {ex}"); - } - finally - { - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); - } - } - [ResetAppContextSwitches] - [Fact] - public void NestedActorTokenAsSubjectShouldBeProperlySerialized() - { - var context = new CompareContext($"{this}.NestedActorTokenAsSubjectShouldBeProperlySerialized"); - bool switchValue = false; - AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); - try - { - // Create nested actor - var nestedActorIdentity = new CaseSensitiveClaimsIdentity("NestedActorAuth"); - nestedActorIdentity.AddClaim(new Claim("sub", "nested-actor-id")); - nestedActorIdentity.AddClaim(new Claim("name", "Nested Actor")); - - // Create actor identity with nested actor - var actorIdentity = new CaseSensitiveClaimsIdentity("ActorAuth"); - actorIdentity.AddClaim(new Claim("sub", "actor-subject-id")); - actorIdentity.AddClaim(new Claim("name", "Actor Name")); - actorIdentity.Actor = nestedActorIdentity; - - // Create the main identity with Actor set - var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); - mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); - mainIdentity.AddClaim(new Claim("name", "Main User")); - mainIdentity.Actor = actorIdentity; - - // Create a token with JsonWebTokenHandler - var tokenHandler = new JsonWebTokenHandler(); - var tokenDescriptor = new SecurityTokenDescriptor - { - Subject = mainIdentity, - Issuer = "https://example.com", - Audience = "https://api.example.com", - Expires = DateTime.UtcNow.AddHours(1), - SigningCredentials = Default.AsymmetricSigningCredentials, - ActorClaimName = "act", - }; - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); - var token = tokenHandler.CreateToken(tokenDescriptor); - JsonWebToken decodedToken = tokenHandler.ReadJsonWebToken(token); - - // Verify actor claim exists - Assert.True(decodedToken.Payload.HasClaim("act"), "JWT token should contain 'act' claim"); - - // Verify the actor object structure - var actorObject = decodedToken.Payload.GetValue("act"); - Console.WriteLine("actor token created: " + actorObject.ToString()); - - Assert.Equal(JsonValueKind.Object, actorObject.ValueKind); - - // Verify main actor claims directly from JSON object - Assert.Equal("actor-subject-id", actorObject.GetProperty("sub").GetString()); - Assert.Equal("Actor Name", actorObject.GetProperty("name").GetString()); - - // Verify nested actor exists and is a JSON object - Assert.True(actorObject.TryGetProperty(tokenDescriptor.ActorClaimName, out var nestedActorElement)); - Assert.Equal(JsonValueKind.Object, nestedActorElement.ValueKind); - Console.WriteLine("nested token created: " + nestedActorElement.ToString()); - - // Verify nested actor claims directly from JSON object - Assert.Equal("nested-actor-id", nestedActorElement.GetProperty("sub").GetString()); - Assert.Equal("Nested Actor", nestedActorElement.GetProperty("name").GetString()); - TestUtilities.AssertFailIfErrors(context); - } - finally - { - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); - } - } - [ResetAppContextSwitches] - [Fact] - public void MaxActorChainLength_RejectsNegativeValues() - { - bool switchValue = false; - AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); - - // Arrange - SecurityTokenDescriptor tokenDescriptor = new SecurityTokenDescriptor - { - Subject = null, - Issuer = "https://example.com", - Audience = "https://api.example.com", - SigningCredentials = Default.AsymmetricSigningCredentials - }; - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); - tokenDescriptor.ActorClaimName = "act"; // Set the actor claim name to "act" for testing - int originalValue = tokenDescriptor.MaxActorChainLength; - try - { - tokenDescriptor.ActorClaimName = "act"; // Set the actor claim name to "act" for testing - // Act & Assert - Valid value 0 should not throw - tokenDescriptor.MaxActorChainLength = 0; - Assert.Equal(0, tokenDescriptor.MaxActorChainLength); - - // Act & Assert - Negative value - var ex = Assert.Throws(() => - tokenDescriptor.MaxActorChainLength = -5); - Assert.Contains("IDX11027", ex.Message); - - // Act & Assert - Valid value 1 should not throw - tokenDescriptor.MaxActorChainLength = 1; - Assert.Equal(1, tokenDescriptor.MaxActorChainLength); - - ex = Assert.Throws(() => - tokenDescriptor.MaxActorChainLength = 10); - Assert.Contains("IDX11027", ex.Message); - } - finally - { - // Restore to original value - tokenDescriptor.MaxActorChainLength = originalValue; - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); - } - } - - [ResetAppContextSwitches] - [Fact] - public void NestedSubjectActorTokens_ExceedingMaxDepth_ThrowsException() - { - var context = new CompareContext($"{this}.NestedActorTokens_ExceedingMaxDepth_ThrowsException"); - bool switchValue = false; - AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); - try - { - // Arrange - var handler = new JsonWebTokenHandler(); - - // Create nested actor identities (3 levels, but we'll set MaxActorChainLength to 2) - var level3Actor = new CaseSensitiveClaimsIdentity("Level3Auth"); - level3Actor.AddClaim(new Claim("sub", "level3-actor")); - level3Actor.AddClaim(new Claim("name", "Level 3 Actor")); - - var level2Actor = new CaseSensitiveClaimsIdentity("Level2Auth"); - level2Actor.AddClaim(new Claim("sub", "level2-actor")); - level2Actor.AddClaim(new Claim("name", "Level 2 Actor")); - level2Actor.Actor = level3Actor; // This will cause exception due to MaxActorChainLength=2 - - var level1Actor = new CaseSensitiveClaimsIdentity("Level1Auth"); - level1Actor.AddClaim(new Claim("sub", "level1-actor")); - level1Actor.AddClaim(new Claim("name", "Level 1 Actor")); - level1Actor.Actor = level2Actor; - - // Create the main identity - var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); - mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); - mainIdentity.AddClaim(new Claim("name", "Main User")); - mainIdentity.Actor = level1Actor; - - // Create token descriptor - var tokenDescriptor = new SecurityTokenDescriptor - { - Subject = mainIdentity, - Issuer = "https://example.com", - Audience = "https://api.example.com", - SigningCredentials = Default.AsymmetricSigningCredentials, - ActorClaimName = "act", - MaxActorChainLength = 2 - }; - - // Act - This should throw a SecurityTokenException - var token = handler.CreateToken(tokenDescriptor); - context.Diffs.Add("Expected exception was not thrown."); - - TestUtilities.AssertFailIfErrors(context); - } - catch (SecurityTokenException ex) - { - // Assert - Verify the exception message contains the expected content - if (!ex.Message.Contains("IDX14313")) - { - context.Diffs.Add($"Exception message does not contain expected content. Message: {ex.Message}"); - } - } - catch (Exception ex) - { - // Unexpected exception type - context.Diffs.Add($"Unexpected exception type: {ex.GetType()}, Message: {ex.Message}"); - } - finally - { - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); - } - - } - - [Fact] - public void NestedClaimsDictionaryActorTokens_ExceedingMaxDepth_ThrowsException() - { - var context = new CompareContext($"{this}.NestedClaimsDictionaryActorTokens_ExceedingMaxDepth_ThrowsException"); - bool switchValue = false; - AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); - try - { - // Arrange - var handler = new JsonWebTokenHandler(); - string actorname = "act"; - // Create nested actor identities - var nestedActorIdentity = new CaseSensitiveClaimsIdentity("NestedActorAuth"); - nestedActorIdentity.AddClaim(new Claim("sub", "nested-actor-id")); - nestedActorIdentity.AddClaim(new Claim("name", "Nested Actor")); - - // Create actor identity with nested actor - var actorIdentity = new CaseSensitiveClaimsIdentity("ActorAuth"); - actorIdentity.AddClaim(new Claim("sub", "actor-subject-id")); - actorIdentity.AddClaim(new Claim("name", "Actor Name")); - actorIdentity.Actor = nestedActorIdentity; // This should be ignored due to MaxActorChainLength - - // Create the main identity - var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); - mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); - mainIdentity.AddClaim(new Claim("name", "Main User")); - - // Create token with actor in Claims dictionary - var tokenDescriptor = new SecurityTokenDescriptor - { - Subject = mainIdentity, - Issuer = "https://example.com", - Audience = "https://api.example.com", - SigningCredentials = Default.AsymmetricSigningCredentials, - Claims = new Dictionary - { - { actorname, actorIdentity } - }, - ActorClaimName = actorname, - MaxActorChainLength = 1 - }; - - // Act - var token = handler.CreateToken(tokenDescriptor); - context.Diffs.Add("Expected exception was not thrown."); - TestUtilities.AssertFailIfErrors(context); - } - catch (SecurityTokenException ex) - { - // Assert - Verify the exception message contains the expected content - if (!ex.Message.Contains("IDX14313")) - { - context.Diffs.Add($"Exception message does not contain expected content. Message: {ex.Message}"); - } - } - catch (Exception ex) - { - // Unexpected exception type - context.Diffs.Add($"Unexpected exception type: {ex.GetType()}, Message: {ex.Message}"); - } - finally - { - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); - } - } - [ResetAppContextSwitches] - [Fact] - public void ActorTokens_MixedSourceRespectMaxActorChainLength() - { - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); - try - { - // Arrange - var handler = new JsonWebTokenHandler(); - string actorname = "act"; - // Create level 2 actor (will be in claims dictionary) - var level2Actor = new CaseSensitiveClaimsIdentity("Level2Auth"); - level2Actor.AddClaim(new Claim("sub", "level2-actor")); - level2Actor.AddClaim(new Claim("name", "Level 2 Actor")); - - // Create nested actors that should be truncated - var level3Actor = new CaseSensitiveClaimsIdentity("Level3Auth"); - level3Actor.AddClaim(new Claim("sub", "level3-actor")); - level3Actor.AddClaim(new Claim("name", "Level 3 Actor")); - - // Create level 1 actor with nested actor - var level1Actor = new CaseSensitiveClaimsIdentity("Level1Auth"); - level1Actor.AddClaim(new Claim("sub", "level1-actor")); - level1Actor.AddClaim(new Claim("name", "Level 1 Actor")); - level1Actor.Actor = level3Actor; // This should be ignored due to MaxActorChainLength - - // Create the main identity - var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); - mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); - mainIdentity.AddClaim(new Claim("name", "Main User")); - mainIdentity.Actor = level1Actor; - - // Create a token with additional actor in Claims dictionary - var tokenDescriptor = new SecurityTokenDescriptor - { - Subject = mainIdentity, - Issuer = "https://example.com", - Audience = "https://api.example.com", - SigningCredentials = Default.AsymmetricSigningCredentials, - // Add level 2 actor in claims dictionary to replace level 1's actor - Claims = new Dictionary - { - { actorname, level2Actor } - }, - ActorClaimName = actorname, - MaxActorChainLength = 1 - }; - - var token = handler.CreateToken(tokenDescriptor); - var jwtToken = handler.ReadJsonWebToken(token); - - // Assert - Check actor object structure - Assert.True(jwtToken.Payload.HasClaim(actorname), "JWT token should contain 'act' claim"); - var actorObject = jwtToken.Payload.GetValue(tokenDescriptor.ActorClaimName); - - Assert.Equal(JsonValueKind.Object, actorObject.ValueKind); - - // Verify we get the actor from Claims dictionary (should be level2Actor) - Assert.Equal("level2-actor", actorObject.GetProperty("sub").GetString()); - Assert.Equal("Level 2 Actor", actorObject.GetProperty("name").GetString()); - - // There should be no nested actor because we're already at max depth - Assert.False(actorObject.TryGetProperty("act", out _), "There should be no nested actor claim due to MaxActorChainLength"); - } - finally - { - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); - } - } - [ResetAppContextSwitches] - [Fact] - public void NestedClaimTokens_ExceedingMaxDepth_ThrowsException() - { - var context = new CompareContext($"{this}.NestedActorTokens_ExceedingMaxDepth_ThrowsException"); - bool switchValue = false; - AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); - var actorname = "act"; - try - { - // Arrange - var handler = new JsonWebTokenHandler(); - - // Create nested actor identities (3 levels, but we'll set MaxActorChainLength to 2) - var level3Actor = new CaseSensitiveClaimsIdentity("Level3Auth"); - level3Actor.AddClaim(new Claim("sub", "level3-actor")); - level3Actor.AddClaim(new Claim("name", "Level 3 Actor")); - - var level2Actor = new CaseSensitiveClaimsIdentity("Level2Auth"); - level2Actor.AddClaim(new Claim("sub", "level2-actor")); - level2Actor.AddClaim(new Claim("name", "Level 2 Actor")); - level2Actor.Actor = level3Actor; // This will cause exception due to MaxActorChainLength=2 - - var level1Actor = new CaseSensitiveClaimsIdentity("Level1Auth"); - level1Actor.AddClaim(new Claim("sub", "level1-actor")); - level1Actor.AddClaim(new Claim("name", "Level 1 Actor")); - level1Actor.Actor = level2Actor; - - // Create the main identity - var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); - mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); - mainIdentity.AddClaim(new Claim("name", "Main User")); - // Create token descriptor - var tokenDescriptor = new SecurityTokenDescriptor - { - Subject = mainIdentity, - Issuer = "https://example.com", - Audience = "https://api.example.com", - SigningCredentials = Default.AsymmetricSigningCredentials, - Claims = new Dictionary - { - { "act", level1Actor } - }, - ActorClaimName = actorname, - MaxActorChainLength = 1 - }; - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); - - // Act - This should throw a SecurityTokenException - var token = handler.CreateToken(tokenDescriptor); - context.Diffs.Add("Expected exception was not thrown."); - TestUtilities.AssertFailIfErrors(context); - } - catch (SecurityTokenException ex) - { - // Assert - Verify the exception message contains the expected content - if (!ex.Message.Contains("IDX14313")) - { - context.Diffs.Add($"Exception message does not contain expected content. Message: {ex.Message}"); - } - } - catch (Exception ex) - { - // Unexpected exception type - context.Diffs.Add($"Unexpected exception type: {ex.GetType()}, Message: {ex.Message}"); - } - finally - { - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); - } - } - // Tests for creating ClaimsIdentity from JsonElement [ResetAppContextSwitches] [Fact] @@ -1151,100 +495,6 @@ public void CustomActorClaimNameShouldBeRespected() } } - [ResetAppContextSwitches] - [Fact] - public void DifferentIssuerShouldBeAppliedToAllClaims() - { - var context = new CompareContext($"{this}.DifferentIssuerShouldBeAppliedToAllClaims"); - bool switchValue = false; - AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); - try - { - // Create simple actor JSON - string actorJson = @"{ - ""sub"": ""actor-subject-id"", - ""name"": ""Actor Name"" - }"; - - var jsonElement = JsonDocument.Parse(actorJson).RootElement; - var tokenValidationParameters = new TokenValidationParameters - { - ActorClaimName = "act" - }; - - string customIssuer = "https://custom-issuer.example.com"; - - // Create ClaimsIdentity with custom issuer - var identity = JsonWebTokenHandler.CreateActorClaimsIdentityFromJsonElement( - jsonElement, - tokenValidationParameters, - customIssuer); - - // Verify all claims have custom issuer - foreach (var claim in identity.Claims) - { - Assert.Equal(customIssuer, claim.Issuer); - Assert.Equal(customIssuer, claim.OriginalIssuer); - } - - TestUtilities.AssertFailIfErrors(context); - } - catch (Exception ex) - { - context.Diffs.Add($"Exception: {ex}"); - } - finally - { - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); - } - } - - [ResetAppContextSwitches] - [Fact] - public void CustomAuthenticationTypeShouldBeRespected() - { - var context = new CompareContext($"{this}.CustomAuthenticationTypeShouldBeRespected"); - bool switchValue = false; - AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); - try - { - // Create simple actor JSON - string actorJson = @"{ - ""sub"": ""actor-subject-id"", - ""name"": ""Actor Name"" - }"; - - var jsonElement = JsonDocument.Parse(actorJson).RootElement; - var tokenValidationParameters = new TokenValidationParameters - { - ActorClaimName = "act" - }; - - string customAuthType = "CustomActorAuth"; - - // Create ClaimsIdentity with custom auth type - var identity = JsonWebTokenHandler.CreateActorClaimsIdentityFromJsonElement( - jsonElement, - tokenValidationParameters, - null); - - // Verify custom auth type was applied - Assert.Equal(customAuthType, identity.AuthenticationType); - - TestUtilities.AssertFailIfErrors(context); - } - catch (Exception ex) - { - context.Diffs.Add($"Exception: {ex}"); - } - finally - { - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); - } - } - [ResetAppContextSwitches] [Fact] public void ActorChainDepthShouldBeIncremented() @@ -1297,54 +547,6 @@ public void ActorChainDepthShouldBeIncremented() } } - [ResetAppContextSwitches] - [Fact] - public void WhenActClaimIsNotAnObject_ShouldBeAddedAsRegularClaim() - { - var context = new CompareContext($"{this}.WhenActClaimIsNotAnObject_ShouldBeAddedAsRegularClaim"); - bool switchValue = false; - AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); - try - { - // Create JSON with "act" claim that is a string, not an object - string actorJson = @"{ - ""sub"": ""actor-subject-id"", - ""name"": ""Actor Name"", - ""act"": ""some-actor-reference-string"" - }"; - - var jsonElement = JsonDocument.Parse(actorJson).RootElement; - var tokenValidationParameters = new TokenValidationParameters - { - ActorClaimName = "act" - }; - - // Create ClaimsIdentity from JsonElement - var identity = JsonWebTokenHandler.CreateActorClaimsIdentityFromJsonElement( - jsonElement, - tokenValidationParameters); - - // Verify identity claims - Assert.NotNull(identity); - Assert.Equal("actor-subject-id", identity.Claims.First(c => c.Type == "sub").Value); - Assert.Equal("Actor Name", identity.Claims.First(c => c.Type == "name").Value); - - // Verify "act" is a regular claim, not a nested actor - Assert.Null(identity.Actor); - Assert.Equal("some-actor-reference-string", identity.Claims.First(c => c.Type == "act").Value); - - TestUtilities.AssertFailIfErrors(context); - } - catch (Exception ex) - { - context.Diffs.Add($"Exception: {ex}"); - } - finally - { - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); - } - } [ResetAppContextSwitches] [Fact] public async Task ValidateTokenAsync_WithActorInToken_ProvidesActorClaimsIdentity() @@ -1618,90 +820,12 @@ public async Task ValidateTokenAsync_NestingBeyondMaxActorChain_ThrowsException( }; handler.MapInboundClaims = true; var result = await handler.ValidateTokenAsync(token, validationParameters); - foreach (Claim claim in result.ClaimsIdentity.Claims) - { - Console.WriteLine($"Claim Type: {claim.Type}, Value: {claim.Value}"); - } - Assert.False(result.IsValid); - Assert.NotNull(result.Exception); - Assert.Contains("IDX14313", result.Exception.ToString()); - - TestUtilities.AssertFailIfErrors(context); - } - catch (Exception ex) - { - Console.WriteLine($"Exception: {ex}"); - } - finally - { - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); - } - } - - [ResetAppContextSwitches] - [Fact] - public async Task ValidateTokenAsync_NestingBeyondMaxActorChain_CustomDelegate_ThrowsException() - { - var context = new CompareContext($"{this}.ValidateTokenAsync_NestingBeyondMaxActorChain_CustomDelegate_ThrowsException"); - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); - - ClaimsIdentity CustomDelegate(JsonElement element) - { - var id = new CaseSensitiveClaimsIdentity("CustomActorAuth"); - if (element.TryGetProperty("sub", out var sub)) - id.AddClaim(new Claim("sub", sub.GetString())); - if (element.TryGetProperty("act", out var nested) && nested.ValueKind == System.Text.Json.JsonValueKind.Object) - id.Actor = CustomDelegate(nested); - return id; - } - - try - { - var level3Actor = new CaseSensitiveClaimsIdentity("Level3Auth"); - level3Actor.AddClaim(new Claim("sub", "level3-actor")); - - var level2Actor = new CaseSensitiveClaimsIdentity("Level2Auth"); - level2Actor.AddClaim(new Claim("sub", "level2-actor")); - level2Actor.Actor = level3Actor; - - var level1Actor = new CaseSensitiveClaimsIdentity("Level1Auth"); - level1Actor.AddClaim(new Claim("sub", "level1-actor")); - level1Actor.Actor = level2Actor; - - var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); - mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); - - var handler = new JsonWebTokenHandler(); - var tokenDescriptor = new SecurityTokenDescriptor - { - Subject = mainIdentity, - Issuer = "https://example.com", - Audience = "https://api.example.com", - Expires = DateTime.UtcNow.AddHours(1), - SigningCredentials = Default.AsymmetricSigningCredentials, - Claims = new Dictionary { { "act", level1Actor } } - }; - var token = handler.CreateToken(tokenDescriptor); - - var validationParameters = new TokenValidationParameters - { - ValidateIssuer = false, - ValidateAudience = false, - ValidateLifetime = false, - IssuerSigningKey = Default.AsymmetricSigningKey, - ValidateIssuerSigningKey = true, - ActorClaimName = "act", - MaxActorChainLength = 2, - //ActClaimRetrieverDelegate = CustomDelegate - }; - - var result = await handler.ValidateTokenAsync(token, validationParameters); - Assert.NotNull(result.ClaimsIdentity.Actor); + Assert.Null(result.ClaimsIdentity.Actor); TestUtilities.AssertFailIfErrors(context); } catch (Exception ex) { - Assert.Contains("IDX14313", ex.ToString()); ; + Assert.Contains("IDX14313", ex.ToString()); } finally { @@ -1759,7 +883,7 @@ ClaimsIdentity CustomDelegate(JsonElement element) } catch (Exception ex) { - Assert.Contains("IDX14313", ex.ToString()); ; + Assert.Contains("IDX14314", ex.ToString()); } finally { diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActClaimTests/ActClaimSerializationTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActClaimTests/ActClaimSerializationTests.cs new file mode 100644 index 0000000000..d4f8a163c2 --- /dev/null +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActClaimTests/ActClaimSerializationTests.cs @@ -0,0 +1,670 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Security.Claims; +using System.Text.Json; +using Microsoft.IdentityModel.TestUtils; +using Microsoft.IdentityModel.Tokens; +using Xunit; +namespace Microsoft.IdentityModel.JsonWebTokens.Tests.ActClaimTests +{ + public class ActClaimSerializationTests + { + [ResetAppContextSwitches] + [Fact] + public void ActorTokenInClaimsDictionaryShouldBeProperlySerialized() + { + var context = new CompareContext($"{this}.ActorTokenInClaimsDictionaryShouldBeProperlySerialized"); + bool switchValue = false; + AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); + string actorname = "act"; + try + { + // Create a ClaimsIdentity for the actor + var actorIdentity = new CaseSensitiveClaimsIdentity("ActorAuth"); + actorIdentity.AddClaim(new Claim("sub", "actor-subject-id")); + actorIdentity.AddClaim(new Claim("name", "Actor Name")); + actorIdentity.AddClaim(new Claim("role", "admin")); + + // Create the main identity + var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); + mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); + mainIdentity.AddClaim(new Claim("name", "Main User")); + + // Create a token with JsonWebTokenHandler where actor is in Claims dictionary + var tokenHandler = new JsonWebTokenHandler(); + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = mainIdentity, + Issuer = "https://example.com", + Audience = "https://api.example.com", + Expires = DateTime.UtcNow.AddHours(1), + SigningCredentials = Default.AsymmetricSigningCredentials, + Claims = new Dictionary + { + { actorname, actorIdentity } + } + }; + var token = tokenHandler.CreateToken(tokenDescriptor); + JsonWebToken decodedToken = tokenHandler.ReadJsonWebToken(token); + + // Verify actor claim exists in the token + Assert.True(decodedToken.Payload.HasClaim(tokenDescriptor.ActorClaimName), "JWT token should contain 'actort' claim"); + // Verify the actor object directly + var actorObject = decodedToken.Payload.GetValue(tokenDescriptor.ActorClaimName); + Assert.Equal(JsonValueKind.Object, actorObject.ValueKind); + + // Verify actor claims directly from the JSON object + Assert.Equal("actor-subject-id", actorObject.GetProperty("sub").GetString()); + Assert.Equal("Actor Name", actorObject.GetProperty("name").GetString()); + Assert.Equal("admin", actorObject.GetProperty("role").GetString()); + TestUtilities.AssertFailIfErrors(context); + } + catch (Exception ex) + { + context.Diffs.Add($"Exception: {ex}"); + } + finally + { + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); + } + } + + [ResetAppContextSwitches] + [Fact] + public void ActorTokenAsSubjectShouldBeProperlySerialized() + { + var context = new CompareContext($"{this}.ActorTokenAsSubjectShouldBeProperlySerialized"); + bool switchValue = false; + AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); + try + { + // Create actor identity + var actorIdentity = new CaseSensitiveClaimsIdentity("ActorAuth"); + actorIdentity.AddClaim(new Claim("sub", "actor-subject-id")); + actorIdentity.AddClaim(new Claim("name", "Actor Name")); + actorIdentity.AddClaim(new Claim("role", "admin")); + + // Create the main identity with Actor set via Identity.Actor + var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); + mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); + mainIdentity.AddClaim(new Claim("name", "Main User")); + mainIdentity.Actor = actorIdentity; + + // Create a token with JsonWebTokenHandler + var tokenHandler = new JsonWebTokenHandler(); + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = mainIdentity, + Issuer = "https://example.com", + Audience = "https://api.example.com", + Expires = DateTime.UtcNow.AddHours(1), + SigningCredentials = Default.AsymmetricSigningCredentials + }; + var token = tokenHandler.CreateToken(tokenDescriptor); + JsonWebToken decodedToken = tokenHandler.ReadJsonWebToken(token); + + // Verify actor claim exists in the token + Assert.True(decodedToken.Payload.HasClaim(tokenDescriptor.ActorClaimName), "JWT token should contain 'act' claim"); + + // Verify actor claim exists in the token + Assert.True(decodedToken.Payload.HasClaim(tokenDescriptor.ActorClaimName), "JWT token should contain actor claim"); + + // Verify the actor object directly + var actorObject = decodedToken.Payload.GetValue(tokenDescriptor.ActorClaimName); + Assert.Equal(JsonValueKind.Object, actorObject.ValueKind); + + // Verify actor claims directly from the JSON object + Assert.Equal("actor-subject-id", actorObject.GetProperty("sub").GetString()); + Assert.Equal("Actor Name", actorObject.GetProperty("name").GetString()); + Assert.Equal("admin", actorObject.GetProperty("role").GetString()); + TestUtilities.AssertFailIfErrors(context); + } + catch (Exception ex) + { + context.Diffs.Add($"Exception: {ex}"); + } + finally + { + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); + } + } + + [ResetAppContextSwitches] + [Fact] + public void ActorTokenInBothClaimsAndSubjectShouldPreferClaimsValue() + { + var context = new CompareContext($"{this}.ActorTokenInBothClaimsAndSubjectShouldPreferClaimsValue"); + bool switchValue = false; + string actorname = "act"; + AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); + try + { + // Create actor identity for Subject.Actor (should be ignored) + var subjectActorIdentity = new CaseSensitiveClaimsIdentity("ActorAuth"); + subjectActorIdentity.AddClaim(new Claim("sub", "subject-actor-id")); + subjectActorIdentity.AddClaim(new Claim("name", "Subject Actor")); + + // Create actor identity for Claims dictionary (should be used) + var claimsActorIdentity = new CaseSensitiveClaimsIdentity("ClaimsActorAuth"); + claimsActorIdentity.AddClaim(new Claim("sub", "claims-actor-id")); + claimsActorIdentity.AddClaim(new Claim("name", "Claims Actor")); + + // Create the main identity + var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); + mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); + mainIdentity.AddClaim(new Claim("name", "Main User")); + mainIdentity.Actor = subjectActorIdentity; // Set the actor that should be ignored + + // Create a token with JsonWebTokenHandler + var tokenHandler = new JsonWebTokenHandler(); + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = mainIdentity, + Issuer = "https://example.com", + Audience = "https://api.example.com", + Expires = DateTime.UtcNow.AddHours(1), + SigningCredentials = Default.AsymmetricSigningCredentials, + // Add Claims actor that should take precedence + Claims = new Dictionary + { + { actorname, claimsActorIdentity } + } + }; + var token = tokenHandler.CreateToken(tokenDescriptor); + JsonWebToken decodedToken = tokenHandler.ReadJsonWebToken(token); + + // Verify actor claim exists + Assert.True(decodedToken.Payload.HasClaim(actorname), "JWT token should contain actor claim"); + + // Verify actor claim exists and is a JSON object + var actorObject = decodedToken.Payload.GetValue("act"); + Assert.Equal(JsonValueKind.Object, actorObject.ValueKind); + + // Verify Claims dictionary actor was used, not Subject.Actor + Assert.Equal("claims-actor-id", actorObject.GetProperty("sub").GetString()); + Assert.Equal("Claims Actor", actorObject.GetProperty("name").GetString()); + Assert.NotEqual("subject-actor-id", actorObject.GetProperty("sub").GetString()); + TestUtilities.AssertFailIfErrors(context); + } + catch (Exception ex) + { + context.Diffs.Add($"Exception: {ex}"); + } + finally + { + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); + } + } + + [ResetAppContextSwitches] + [Fact] + public void NestedActorTokenInClaimsDictionaryShouldBeProperlySerialized() + { + var context = new CompareContext($"{this}.NestedActorTokenInClaimsDictionaryShouldBeProperlySerialized"); + bool switchValue = false; + AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); + try + { + // Create nested actor identity + var nestedActorIdentity = new CaseSensitiveClaimsIdentity("NestedActorAuth"); + nestedActorIdentity.AddClaim(new Claim("sub", "nested-actor-id")); + nestedActorIdentity.AddClaim(new Claim("name", "Nested Actor")); + + // Create actor identity with nested actor + var actorIdentity = new CaseSensitiveClaimsIdentity("ActorAuth"); + actorIdentity.AddClaim(new Claim("sub", "actor-subject-id")); + actorIdentity.AddClaim(new Claim("name", "Actor Name")); + actorIdentity.Actor = nestedActorIdentity; // Set nested actor + + // Create the main identity + var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); + mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); + mainIdentity.AddClaim(new Claim("name", "Main User")); + + // Create a token with JsonWebTokenHandler where actor is in Claims dictionary + var tokenHandler = new JsonWebTokenHandler(); + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = mainIdentity, + Issuer = "https://example.com", + Audience = "https://api.example.com", + Expires = DateTime.UtcNow.AddHours(1), + SigningCredentials = Default.AsymmetricSigningCredentials, + Claims = new Dictionary + { + { "act", actorIdentity } + } + }; + var token = tokenHandler.CreateToken(tokenDescriptor); + JsonWebToken decodedToken = tokenHandler.ReadJsonWebToken(token); + + // Verify actor claim exists + Assert.True(decodedToken.Payload.HasClaim("act"), "JWT token should contain 'actort' claim"); + + // Verify the actor object + var actorObject = decodedToken.Payload.GetValue("act"); + Assert.Equal(JsonValueKind.Object, actorObject.ValueKind); + + // Verify main actor claims directly from JSON object + Assert.Equal("actor-subject-id", actorObject.GetProperty("sub").GetString()); + Assert.Equal("Actor Name", actorObject.GetProperty("name").GetString()); + + // Verify nested actor exists and is a JSON object + Assert.True(actorObject.TryGetProperty(tokenDescriptor.ActorClaimName, out var nestedActorElement)); + Assert.Equal(JsonValueKind.Object, nestedActorElement.ValueKind); + + // Verify nested actor claims directly from JSON object + Assert.Equal("nested-actor-id", nestedActorElement.GetProperty("sub").GetString()); + Assert.Equal("Nested Actor", nestedActorElement.GetProperty("name").GetString()); + TestUtilities.AssertFailIfErrors(context); + } + catch (Exception ex) + { + context.Diffs.Add($"Exception: {ex}"); + } + finally + { + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); + } + } + [ResetAppContextSwitches] + [Fact] + public void NestedActorTokenAsSubjectShouldBeProperlySerialized() + { + var context = new CompareContext($"{this}.NestedActorTokenAsSubjectShouldBeProperlySerialized"); + bool switchValue = false; + AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); + try + { + // Create nested actor + var nestedActorIdentity = new CaseSensitiveClaimsIdentity("NestedActorAuth"); + nestedActorIdentity.AddClaim(new Claim("sub", "nested-actor-id")); + nestedActorIdentity.AddClaim(new Claim("name", "Nested Actor")); + + // Create actor identity with nested actor + var actorIdentity = new CaseSensitiveClaimsIdentity("ActorAuth"); + actorIdentity.AddClaim(new Claim("sub", "actor-subject-id")); + actorIdentity.AddClaim(new Claim("name", "Actor Name")); + actorIdentity.Actor = nestedActorIdentity; + + // Create the main identity with Actor set + var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); + mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); + mainIdentity.AddClaim(new Claim("name", "Main User")); + mainIdentity.Actor = actorIdentity; + + // Create a token with JsonWebTokenHandler + var tokenHandler = new JsonWebTokenHandler(); + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = mainIdentity, + Issuer = "https://example.com", + Audience = "https://api.example.com", + Expires = DateTime.UtcNow.AddHours(1), + SigningCredentials = Default.AsymmetricSigningCredentials, + ActorClaimName = "act", + }; + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); + var token = tokenHandler.CreateToken(tokenDescriptor); + JsonWebToken decodedToken = tokenHandler.ReadJsonWebToken(token); + + // Verify actor claim exists + Assert.True(decodedToken.Payload.HasClaim("act"), "JWT token should contain 'act' claim"); + + // Verify the actor object structure + var actorObject = decodedToken.Payload.GetValue("act"); + Console.WriteLine("actor token created: " + actorObject.ToString()); + + Assert.Equal(JsonValueKind.Object, actorObject.ValueKind); + + // Verify main actor claims directly from JSON object + Assert.Equal("actor-subject-id", actorObject.GetProperty("sub").GetString()); + Assert.Equal("Actor Name", actorObject.GetProperty("name").GetString()); + + // Verify nested actor exists and is a JSON object + Assert.True(actorObject.TryGetProperty(tokenDescriptor.ActorClaimName, out var nestedActorElement)); + Assert.Equal(JsonValueKind.Object, nestedActorElement.ValueKind); + Console.WriteLine("nested token created: " + nestedActorElement.ToString()); + + // Verify nested actor claims directly from JSON object + Assert.Equal("nested-actor-id", nestedActorElement.GetProperty("sub").GetString()); + Assert.Equal("Nested Actor", nestedActorElement.GetProperty("name").GetString()); + TestUtilities.AssertFailIfErrors(context); + } + finally + { + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); + } + } + [ResetAppContextSwitches] + [Fact] + public void MaxActorChainLength_RejectsNegativeValues() + { + bool switchValue = false; + AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); + + // Arrange + SecurityTokenDescriptor tokenDescriptor = new SecurityTokenDescriptor + { + Subject = null, + Issuer = "https://example.com", + Audience = "https://api.example.com", + SigningCredentials = Default.AsymmetricSigningCredentials + }; + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); + tokenDescriptor.ActorClaimName = "act"; // Set the actor claim name to "act" for testing + int originalValue = tokenDescriptor.MaxActorChainLength; + try + { + tokenDescriptor.ActorClaimName = "act"; // Set the actor claim name to "act" for testing + // Act & Assert - Valid value 0 should not throw + tokenDescriptor.MaxActorChainLength = 0; + Assert.Equal(0, tokenDescriptor.MaxActorChainLength); + + // Act & Assert - Negative value + var ex = Assert.Throws(() => + tokenDescriptor.MaxActorChainLength = -5); + Assert.Contains("IDX11027", ex.Message); + + // Act & Assert - Valid value 1 should not throw + tokenDescriptor.MaxActorChainLength = 1; + Assert.Equal(1, tokenDescriptor.MaxActorChainLength); + + ex = Assert.Throws(() => + tokenDescriptor.MaxActorChainLength = 10); + Assert.Contains("IDX11027", ex.Message); + } + finally + { + // Restore to original value + tokenDescriptor.MaxActorChainLength = originalValue; + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); + } + } + + [ResetAppContextSwitches] + [Fact] + public void NestedSubjectActorTokens_ExceedingMaxDepth_ThrowsException() + { + var context = new CompareContext($"{this}.NestedActorTokens_ExceedingMaxDepth_ThrowsException"); + bool switchValue = false; + AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); + try + { + // Arrange + var handler = new JsonWebTokenHandler(); + + // Create nested actor identities (3 levels, but we'll set MaxActorChainLength to 2) + var level3Actor = new CaseSensitiveClaimsIdentity("Level3Auth"); + level3Actor.AddClaim(new Claim("sub", "level3-actor")); + level3Actor.AddClaim(new Claim("name", "Level 3 Actor")); + + var level2Actor = new CaseSensitiveClaimsIdentity("Level2Auth"); + level2Actor.AddClaim(new Claim("sub", "level2-actor")); + level2Actor.AddClaim(new Claim("name", "Level 2 Actor")); + level2Actor.Actor = level3Actor; // This will cause exception due to MaxActorChainLength=2 + + var level1Actor = new CaseSensitiveClaimsIdentity("Level1Auth"); + level1Actor.AddClaim(new Claim("sub", "level1-actor")); + level1Actor.AddClaim(new Claim("name", "Level 1 Actor")); + level1Actor.Actor = level2Actor; + + // Create the main identity + var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); + mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); + mainIdentity.AddClaim(new Claim("name", "Main User")); + mainIdentity.Actor = level1Actor; + + // Create token descriptor + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = mainIdentity, + Issuer = "https://example.com", + Audience = "https://api.example.com", + SigningCredentials = Default.AsymmetricSigningCredentials, + ActorClaimName = "act", + MaxActorChainLength = 2 + }; + + // Act - This should throw a SecurityTokenException + var token = handler.CreateToken(tokenDescriptor); + context.Diffs.Add("Expected exception was not thrown."); + + TestUtilities.AssertFailIfErrors(context); + } + catch (SecurityTokenException ex) + { + // Assert - Verify the exception message contains the expected content + if (!ex.Message.Contains("IDX14313")) + { + context.Diffs.Add($"Exception message does not contain expected content. Message: {ex.Message}"); + } + } + catch (Exception ex) + { + // Unexpected exception type + context.Diffs.Add($"Unexpected exception type: {ex.GetType()}, Message: {ex.Message}"); + } + finally + { + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); + } + + } + + [Fact] + public void NestedClaimsDictionaryActorTokens_ExceedingMaxDepth_ThrowsException() + { + var context = new CompareContext($"{this}.NestedClaimsDictionaryActorTokens_ExceedingMaxDepth_ThrowsException"); + bool switchValue = false; + AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); + try + { + // Arrange + var handler = new JsonWebTokenHandler(); + string actorname = "act"; + // Create nested actor identities + var nestedActorIdentity = new CaseSensitiveClaimsIdentity("NestedActorAuth"); + nestedActorIdentity.AddClaim(new Claim("sub", "nested-actor-id")); + nestedActorIdentity.AddClaim(new Claim("name", "Nested Actor")); + + // Create actor identity with nested actor + var actorIdentity = new CaseSensitiveClaimsIdentity("ActorAuth"); + actorIdentity.AddClaim(new Claim("sub", "actor-subject-id")); + actorIdentity.AddClaim(new Claim("name", "Actor Name")); + actorIdentity.Actor = nestedActorIdentity; // This should be ignored due to MaxActorChainLength + + // Create the main identity + var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); + mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); + mainIdentity.AddClaim(new Claim("name", "Main User")); + + // Create token with actor in Claims dictionary + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = mainIdentity, + Issuer = "https://example.com", + Audience = "https://api.example.com", + SigningCredentials = Default.AsymmetricSigningCredentials, + Claims = new Dictionary + { + { actorname, actorIdentity } + }, + ActorClaimName = actorname, + MaxActorChainLength = 1 + }; + + // Act + var token = handler.CreateToken(tokenDescriptor); + context.Diffs.Add("Expected exception was not thrown."); + TestUtilities.AssertFailIfErrors(context); + } + catch (SecurityTokenException ex) + { + // Assert - Verify the exception message contains the expected content + if (!ex.Message.Contains("IDX14313")) + { + context.Diffs.Add($"Exception message does not contain expected content. Message: {ex.Message}"); + } + } + catch (Exception ex) + { + // Unexpected exception type + context.Diffs.Add($"Unexpected exception type: {ex.GetType()}, Message: {ex.Message}"); + } + finally + { + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); + } + } + [ResetAppContextSwitches] + [Fact] + public void ActorTokens_MixedSourceRespectMaxActorChainLength() + { + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); + try + { + // Arrange + var handler = new JsonWebTokenHandler(); + string actorname = "act"; + // Create level 2 actor (will be in claims dictionary) + var level2Actor = new CaseSensitiveClaimsIdentity("Level2Auth"); + level2Actor.AddClaim(new Claim("sub", "level2-actor")); + level2Actor.AddClaim(new Claim("name", "Level 2 Actor")); + + // Create nested actors that should be truncated + var level3Actor = new CaseSensitiveClaimsIdentity("Level3Auth"); + level3Actor.AddClaim(new Claim("sub", "level3-actor")); + level3Actor.AddClaim(new Claim("name", "Level 3 Actor")); + + // Create level 1 actor with nested actor + var level1Actor = new CaseSensitiveClaimsIdentity("Level1Auth"); + level1Actor.AddClaim(new Claim("sub", "level1-actor")); + level1Actor.AddClaim(new Claim("name", "Level 1 Actor")); + level1Actor.Actor = level3Actor; // This should be ignored due to MaxActorChainLength + + // Create the main identity + var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); + mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); + mainIdentity.AddClaim(new Claim("name", "Main User")); + mainIdentity.Actor = level1Actor; + + // Create a token with additional actor in Claims dictionary + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = mainIdentity, + Issuer = "https://example.com", + Audience = "https://api.example.com", + SigningCredentials = Default.AsymmetricSigningCredentials, + // Add level 2 actor in claims dictionary to replace level 1's actor + Claims = new Dictionary + { + { actorname, level2Actor } + }, + ActorClaimName = actorname, + MaxActorChainLength = 1 + }; + + var token = handler.CreateToken(tokenDescriptor); + var jwtToken = handler.ReadJsonWebToken(token); + + // Assert - Check actor object structure + Assert.True(jwtToken.Payload.HasClaim(actorname), "JWT token should contain 'act' claim"); + var actorObject = jwtToken.Payload.GetValue(tokenDescriptor.ActorClaimName); + + Assert.Equal(JsonValueKind.Object, actorObject.ValueKind); + + // Verify we get the actor from Claims dictionary (should be level2Actor) + Assert.Equal("level2-actor", actorObject.GetProperty("sub").GetString()); + Assert.Equal("Level 2 Actor", actorObject.GetProperty("name").GetString()); + + // There should be no nested actor because we're already at max depth + Assert.False(actorObject.TryGetProperty("act", out _), "There should be no nested actor claim due to MaxActorChainLength"); + } + finally + { + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); + } + } + [ResetAppContextSwitches] + [Fact] + public void NestedClaimTokens_ExceedingMaxDepth_ThrowsException() + { + var context = new CompareContext($"{this}.NestedActorTokens_ExceedingMaxDepth_ThrowsException"); + bool switchValue = false; + AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); + var actorname = "act"; + try + { + // Arrange + var handler = new JsonWebTokenHandler(); + + // Create nested actor identities (3 levels, but we'll set MaxActorChainLength to 2) + var level3Actor = new CaseSensitiveClaimsIdentity("Level3Auth"); + level3Actor.AddClaim(new Claim("sub", "level3-actor")); + level3Actor.AddClaim(new Claim("name", "Level 3 Actor")); + + var level2Actor = new CaseSensitiveClaimsIdentity("Level2Auth"); + level2Actor.AddClaim(new Claim("sub", "level2-actor")); + level2Actor.AddClaim(new Claim("name", "Level 2 Actor")); + level2Actor.Actor = level3Actor; // This will cause exception due to MaxActorChainLength=2 + + var level1Actor = new CaseSensitiveClaimsIdentity("Level1Auth"); + level1Actor.AddClaim(new Claim("sub", "level1-actor")); + level1Actor.AddClaim(new Claim("name", "Level 1 Actor")); + level1Actor.Actor = level2Actor; + + // Create the main identity + var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); + mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); + mainIdentity.AddClaim(new Claim("name", "Main User")); + // Create token descriptor + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = mainIdentity, + Issuer = "https://example.com", + Audience = "https://api.example.com", + SigningCredentials = Default.AsymmetricSigningCredentials, + Claims = new Dictionary + { + { "act", level1Actor } + }, + ActorClaimName = actorname, + MaxActorChainLength = 1 + }; + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); + + // Act - This should throw a SecurityTokenException + var token = handler.CreateToken(tokenDescriptor); + context.Diffs.Add("Expected exception was not thrown."); + TestUtilities.AssertFailIfErrors(context); + } + catch (SecurityTokenException ex) + { + // Assert - Verify the exception message contains the expected content + if (!ex.Message.Contains("IDX14313")) + { + context.Diffs.Add($"Exception message does not contain expected content. Message: {ex.Message}"); + } + } + catch (Exception ex) + { + // Unexpected exception type + context.Diffs.Add($"Unexpected exception type: {ex.GetType()}, Message: {ex.Message}"); + } + finally + { + AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); + } + } + } +} From 4d2770ff38dfae180aa9dd2725f0d5d7f03f44b2 Mon Sep 17 00:00:00 2001 From: saurabhsathe-ms Date: Fri, 23 May 2025 14:58:26 -0700 Subject: [PATCH 42/52] NIT repairs, renamed some fields and adjusted some default values as per feedback. Updated the summaries to give more information about fields --- .../JsonWebTokenHandler.CreateToken.cs | 10 +- .../JsonWebTokenHandler.cs | 11 +- .../Delegates.cs | 3 +- .../PublicAPI.Unshipped.txt | 10 +- .../SecurityTokenDescriptor.cs | 65 ++++++---- .../TokenValidationParameters.cs | 116 +++++++++++------- .../ActClaimDeserializationTests.cs | 42 +++---- .../ActClaimSerializationTests.cs | 30 ++--- .../ValidationDelegates.cs | 2 +- .../TokenValidationParametersTests.cs | 7 +- 10 files changed, 172 insertions(+), 124 deletions(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs index 6c00f8126d..4efb65557d 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs @@ -673,7 +673,7 @@ internal static void WriteJwsPayload( { foreach (KeyValuePair kvp in tokenDescriptor.Claims) { - if (AppContextSwitches.EnableActClaimSupport && kvp.Key.Equals(tokenDescriptor.ActorClaimName, StringComparison.Ordinal)) + if (AppContextSwitches.EnableActClaimSupport && kvp.Key.Equals(tokenDescriptor.ActorClaimType, StringComparison.Ordinal)) { continue; } @@ -1089,7 +1089,7 @@ internal static void WriteActorToken( var actorTokenDescriptor = CreateActorTokenDescriptor(tokenDescriptor); if (actorTokenDescriptor == null || actorTokenDescriptor.Subject == null) return; - writer.WritePropertyName(tokenDescriptor.ActorClaimName); + writer.WritePropertyName(tokenDescriptor.ActorClaimType); WriteJwsPayload(ref writer, actorTokenDescriptor, setDefaultTimesOnTokenCreation, tokenLifetimeInMinutes); } @@ -1110,9 +1110,9 @@ private static SecurityTokenDescriptor CreateActorTokenDescriptor(SecurityTokenD SecurityTokenDescriptor actorTokenDescriptor = null; // Check for actor in claims first - if (tokenDescriptor.Claims?.ContainsKey(tokenDescriptor.ActorClaimName) == true) + if (tokenDescriptor.Claims?.ContainsKey(tokenDescriptor.ActorClaimType) == true) { - ClaimsIdentity actor = tokenDescriptor.Claims[tokenDescriptor.ActorClaimName] as ClaimsIdentity; + ClaimsIdentity actor = tokenDescriptor.Claims[tokenDescriptor.ActorClaimType] as ClaimsIdentity; actorTokenDescriptor = new SecurityTokenDescriptor { Subject = actor, @@ -1131,7 +1131,7 @@ private static SecurityTokenDescriptor CreateActorTokenDescriptor(SecurityTokenD ValidateActorChainDepth(tokenDescriptor); actorTokenDescriptor.MaxActorChainLength = tokenDescriptor.MaxActorChainLength; - actorTokenDescriptor.ActorClaimName = tokenDescriptor.ActorClaimName; + actorTokenDescriptor.ActorClaimType = tokenDescriptor.ActorClaimType; actorTokenDescriptor.ActorChainDepth = tokenDescriptor.ActorChainDepth + 1; } diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs index af746a63f1..6c339c9db9 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs @@ -211,6 +211,7 @@ protected virtual ClaimsIdentity CreateClaimsIdentity(JsonWebToken jwtToken, Tok private ClaimsIdentity CreateClaimsIdentityWithMapping(JsonWebToken jwtToken, TokenValidationParameters validationParameters, string issuer) { + _ = validationParameters ?? throw LogHelper.LogArgumentNullException(nameof(validationParameters)); ClaimsIdentity identity = validationParameters.CreateClaimsIdentity(jwtToken, issuer); foreach (Claim jwtClaim in jwtToken.Claims) @@ -220,13 +221,14 @@ private ClaimsIdentity CreateClaimsIdentityWithMapping(JsonWebToken jwtToken, To if (!wasMapped) claimType = jwtClaim.Type; - if (claimType == validationParameters.ActorClaimName) + if (claimType == validationParameters.ActorClaimType) { if (identity.Actor != null) throw LogHelper.LogExceptionMessage(new InvalidOperationException(LogHelper.FormatInvariant( LogMessages.IDX14112, LogHelper.MarkAsNonPII(JwtRegisteredClaimNames.Actort), jwtClaim.Value))); + identity.Actor = CreateClaimsIdentityActor(jwtToken, jwtClaim.Value, validationParameters); } @@ -249,6 +251,7 @@ private ClaimsIdentity CreateClaimsIdentityWithMapping(JsonWebToken jwtToken, To identity.AddClaim(jwtClaim); } } + return identity; } @@ -279,7 +282,7 @@ private ClaimsIdentity CreateClaimsIdentityPrivate(JsonWebToken jwtToken, TokenV foreach (Claim jwtClaim in jwtToken.Claims) { string claimType = jwtClaim.Type; - if (claimType == validationParameters.ActorClaimName) + if (claimType == validationParameters.ActorClaimType) { if (identity.Actor != null) throw LogHelper.LogExceptionMessage(new InvalidOperationException(LogHelper.FormatInvariant(LogMessages.IDX14112, LogHelper.MarkAsNonPII(JwtRegisteredClaimNames.Actort), jwtClaim.Value))); @@ -582,7 +585,7 @@ private ClaimsIdentity CreateClaimsIdentityActor( if (AppContextSwitches.EnableActClaimSupport) { - if (jwtToken.TryGetPayloadValue(tokenValidationParameters.ActorClaimName, out JsonElement actClaim)) + if (jwtToken.TryGetPayloadValue(tokenValidationParameters.ActorClaimType, out JsonElement actClaim)) { if (tokenValidationParameters.ActClaimRetrieverDelegate != null) { @@ -654,7 +657,7 @@ public static ClaimsIdentity CreateActorClaimsIdentityFromJsonElement( JsonElement value = property.Value; // Special handling for nested actor claim - if (claimType == tokenValidationParameters.ActorClaimName) + if (claimType == tokenValidationParameters.ActorClaimType) { if (value.ValueKind == JsonValueKind.Object) { diff --git a/src/Microsoft.IdentityModel.Tokens/Delegates.cs b/src/Microsoft.IdentityModel.Tokens/Delegates.cs index 9fe7e4a2cb..cc29f6f591 100644 --- a/src/Microsoft.IdentityModel.Tokens/Delegates.cs +++ b/src/Microsoft.IdentityModel.Tokens/Delegates.cs @@ -241,6 +241,7 @@ internal delegate ValidationResult SignatureValidationDelegate( /// Delegate to validate the 'act' claim and create actor's ClaimsIdentity. /// /// The JSON element representing the 'act' claim. + /// Opitonal validation parameters if needed /// A ClaimsIdentity representing the actor. - public delegate ClaimsIdentity ActClaimRetrieverDelegate(JsonElement actClaim); + public delegate ClaimsIdentity ActClaimRetrieverDelegate(JsonElement actClaim, TokenValidationParameters tokenValidationParameters = null); } diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI.Unshipped.txt index a4ee940e07..9524c45c50 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI.Unshipped.txt @@ -1,17 +1,15 @@ Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActorChainDepth.get -> int Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActorChainDepth.set -> void -Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActorClaimName.get -> string -Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActorClaimName.set -> void +Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActorClaimType.get -> string +Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActorClaimType.set -> void Microsoft.IdentityModel.Tokens.TokenValidationParameters.MaxActorChainLength.get -> int Microsoft.IdentityModel.Tokens.TokenValidationParameters.MaxActorChainLength.set -> void Microsoft.IdentityModel.Tokens.ActClaimRetrieverDelegate Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActClaimRetrieverDelegate.get -> Microsoft.IdentityModel.Tokens.ActClaimRetrieverDelegate Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActClaimRetrieverDelegate.set -> void -Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActorTokenValidationParameters.get -> Microsoft.IdentityModel.Tokens.TokenValidationParameters -Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActorTokenValidationParameters.set -> void Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorChainDepth.get -> int Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorChainDepth.set -> void -Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimName.get -> string -Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimName.set -> void +Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimType.get -> string +Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimType.set -> void Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.get -> int Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.set -> void diff --git a/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs b/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs index 352dc288dc..038d8b0255 100644 --- a/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs +++ b/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs @@ -16,7 +16,9 @@ namespace Microsoft.IdentityModel.Tokens public class SecurityTokenDescriptor { private List _audiences; - + private string _actorClaimType = "act"; + private int _actorClainDepth; + private int _maxActorChainLength = 4; /// /// Gets or sets the value of the {"": audience} claim. Will be combined with and any "Aud" claims in /// or when creating a token. @@ -118,45 +120,54 @@ public class SecurityTokenDescriptor [DefaultValue(true)] public bool IncludeKeyIdInHeader { get; set; } = true; - private int _maxActorChainLength = 5; /// /// Gets or sets the maximum depth allowed when processing nested actor tokens. - /// This prevents excessive recursion when handling deeply nested actor tokens. - /// The value must be at least 0. Value 0 would mean that the actor token is not allowed to be nested. - /// Default value is 5. Max value is also 5 + /// This prevents excessive recursion when handling deeply nested actor tokens. + /// The value must be at least 0. Value 0 would mean that no actor token nesting is allowed. + /// The maximum allowed value is 4 to prevent security issues with excessively deep actor chains. /// - /// Thrown if the value is less than 0. + /// + /// Default value is 4. + /// During token validation and creation, an exception will be thrown if the actor nesting exceeds this limit. + /// This limit applies to both token creation and validation processes. + /// + /// Thrown if the value is less than 0 or greater than 4. public int MaxActorChainLength { get => _maxActorChainLength; set { - if (value < 0 || value > 5) + if (value < 0 || value > 4) throw LogHelper.LogExceptionMessage( new ArgumentOutOfRangeException( LogHelper.FormatInvariant( LogMessages.IDX11027, LogHelper.MarkAsNonPII("MaxActorChainLength")) - + ". Permissible values are integers in range 0 to 5")); + + ". Permissible values are integers in range 0 to 4")); _maxActorChainLength = value; } } - private string _actorClaimName = "act"; /// - /// Gets or sets the claim type name for the actor claim. - /// Permissible values are 'act' or 'actort'. + /// Gets or sets the claim type that identifies the actor claim in tokens. + /// The default value is "actort" when is off + /// and "act" when the switch is on. + /// This property determines which claim in a token contains the actor information during token + /// validation and creation. + /// For JWT tokens, this is the claim name in the payload that holds the actor object. /// /// - /// Thrown if the value is null. - /// - /// - /// Thrown if the value is not 'act' or 'actort'. + /// Thrown if the value is null or empty. /// - public string ActorClaimName + /// + /// To use the newer JSON object-based actor format, set AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true) + /// and use "act" as the claim type. + /// To use the legacy string-based actor token format, leave the switch off and use "actort". + /// + public string ActorClaimType { - get => _actorClaimName; + get => _actorClaimType; set { if (string.IsNullOrEmpty(value)) @@ -164,16 +175,24 @@ public string ActorClaimName new ArgumentOutOfRangeException( LogHelper.FormatInvariant( LogMessages.IDX11027, - LogHelper.MarkAsNonPII("ActorClaimName")) - + ". ActorClaimName cannot be empty.")); - _actorClaimName = value; + LogHelper.MarkAsNonPII("ActorClaimType")) + + ". ActorClaimType cannot be empty.")); + _actorClaimType = value; } } - private int _actorClainDepth; + /// - /// Gets or sets the depth of the actor chain. - /// This value determines the maximum depth of nested actor tokens that can be processed. + /// Gets or sets the current depth in the actor chain being processed. + /// This is used internally to track the nesting level during recursive processing + /// of nested actor tokens. + /// The value starts at 0 and is incremented for each level of actor nesting. /// + /// + /// This value is compared against to prevent excessive + /// recursion or deeply nested actor tokens. + /// In most scenarios, users don't need to set this property as it's managed internally + /// by the token validation and creation process. + /// public int ActorChainDepth { get => _actorClainDepth; diff --git a/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs b/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs index 596a2ccec0..00356fd161 100644 --- a/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs +++ b/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs @@ -44,13 +44,13 @@ public partial class TokenValidationParameters /// Default for permissible max actor chain length. /// /// 5 as max level of nesting - private int maxActorChainLength = 5; + private int maxActorChainLength = 4; /// /// Default for actor claim name. /// /// If not explicitly set the default name for actor claim is 'act'. Only needed when EnableActClaimSupportSwitch is turned on - private string actorClaimName = "act"; + private string actorClaimType = "act"; /// /// This variable is used during recursion calls that are needed for deserializing act claim. @@ -132,7 +132,7 @@ protected TokenValidationParameters(TokenValidationParameters other) ActClaimRetrieverDelegate = other.ActClaimRetrieverDelegate; MaxActorChainLength = other.MaxActorChainLength; ActorChainDepth = other.ActorChainDepth; - ActorClaimName = other.ActorClaimName; + ActorClaimType = other.ActorClaimType; } /// @@ -786,42 +786,24 @@ public string RoleClaimType public IEnumerable ValidTypes { get; set; } /// - /// Gets or sets the maximum depth allowed when processing nested actor tokens. - /// This prevents excessive recursion when handling deeply nested actor tokens. - /// The value must be at least 0. Value 0 would mean that the actor token is not allowed to be nested. - /// Default value is 5. Max value is also 5 - /// - /// Thrown if the value is less than 0. - public int MaxActorChainLength - { - get => maxActorChainLength; - set - { - if (value < 0 || value > 5) - throw LogHelper.LogExceptionMessage( - new ArgumentOutOfRangeException( - LogHelper.FormatInvariant( - LogMessages.IDX11027, - LogHelper.MarkAsNonPII("MaxActorChainLength")) - + ". Permissible values are integers in range 0 to 5")); - - maxActorChainLength = value; - } - } - - /// - /// Gets or sets the claim type name for the actor claim. - /// Permissible values are 'act' or 'actort'. + /// Gets or sets the claim type that identifies the actor claim in tokens. + /// The default value is "actort" when is off + /// and "act" when the switch is on. + /// This property determines which claim in a token contains the actor information during token + /// validation and creation. + /// For JWT tokens, this is the claim name in the payload that holds the actor object. /// /// - /// Thrown if the value is null. + /// Thrown if the value is null or empty. /// - /// - /// Thrown if the value is not 'act' or 'actort'. - /// - public string ActorClaimName + /// + /// To use the newer JSON object-based actor format, set AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true) + /// and use "act" as the claim type. + /// To use the legacy string-based actor token format, leave the switch off and use "actort". + /// + public string ActorClaimType { - get => AppContextSwitches.EnableActClaimSupport ? actorClaimName : "actort"; + get => AppContextSwitches.EnableActClaimSupport ? actorClaimType : "actort"; set { if (string.IsNullOrEmpty(value)) @@ -829,16 +811,24 @@ public string ActorClaimName new ArgumentOutOfRangeException( LogHelper.FormatInvariant( LogMessages.IDX11027, - LogHelper.MarkAsNonPII("ActorClaimName")) - + ". ValidationParameters.ActorClaimName cannot be set to empty.")); - actorClaimName = value; + LogHelper.MarkAsNonPII("ActorClaimType")) + + ". ActorClaimType cannot be set to empty.")); + actorClaimType = value; } } /// - /// Gets or sets the depth of the actor chain. - /// This value determines the maximum depth of nested actor tokens that can be processed. + /// Gets or sets the current depth in the actor chain being processed. + /// This is used internally to track the nesting level during recursive processing + /// of nested actor tokens. + /// The value starts at 0 and is incremented for each level of actor nesting. /// + /// + /// This value is compared against to prevent excessive + /// recursion or deeply nested actor tokens. + /// In most scenarios, users don't need to set this property as it's managed internally + /// by the token validation and creation process. + /// public int ActorChainDepth { get => _actorClainDepth; @@ -849,16 +839,54 @@ public int ActorChainDepth } /// - /// Gets or sets the delegate that will be used to validate the 'act' claim and create actor's ClaimsIdentity. + /// Gets or sets the delegate that will be used to convert the 'act' claim JSON into a ClaimsIdentity. + /// This delegate is invoked during token validation when an actor claim is encountered in a token. + /// The delegate receives a representing the actor claim + /// and should return a that represents the actor. /// + /// + /// When this delegate is provided, it replaces the default actor claim processing logic. + /// This is useful for custom actor claim formats or when special processing is needed for the actor claims. + /// The delegate can also handle nested actors by recursively creating actor identities and setting the Actor property. + /// + /// validationParameters.ActClaimRetrieverDelegate = (JsonElement element,TokenValidationParameters tokenValidationParameters) => { + /// var identity = new ClaimsIdentity("CustomActor"); + /// // Extract claims from the JsonElement + /// if (element.TryGetProperty("sub", out var sub)) + /// identity.AddClaim(new Claim("sub", sub.GetString())); + /// return identity; + /// }; + /// + /// public ActClaimRetrieverDelegate ActClaimRetrieverDelegate { get; set; } /// - /// Gets or sets the used to validate the actor claim. + /// Gets or sets the maximum depth allowed when processing nested actor tokens. + /// This prevents excessive recursion when handling deeply nested actor tokens. + /// The value must be at least 0. Value 0 would mean that no actor token nesting is allowed. + /// The maximum allowed value is 4 to prevent security issues with excessively deep actor chains. /// /// - /// This property allows specifying custom validation parameters for the actor claim. + /// Default value is 4. + /// During token validation and creation, an exception will be thrown if the actor nesting exceeds this limit. + /// This limit applies to both token creation and validation processes. /// - public TokenValidationParameters ActorTokenValidationParameters { get; set; } + /// Thrown if the value is less than 0 or greater than 4. + public int MaxActorChainLength + { + get => maxActorChainLength; + set + { + if (value < 0 || value > 4) + throw LogHelper.LogExceptionMessage( + new ArgumentOutOfRangeException( + LogHelper.FormatInvariant( + LogMessages.IDX11027, + LogHelper.MarkAsNonPII("MaxActorChainLength")) + + ". Permissible values are integers in range 0 to 4")); + + maxActorChainLength = value; + } + } } } diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActClaimTests/ActClaimDeserializationTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActClaimTests/ActClaimDeserializationTests.cs index 9a907ee53d..21dba24543 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActClaimTests/ActClaimDeserializationTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActClaimTests/ActClaimDeserializationTests.cs @@ -35,7 +35,7 @@ public void BasicJsonElementShouldCreateClaimsIdentityCorrectly() var jsonElement = JsonDocument.Parse(actorJson).RootElement; var validationParameters = new TokenValidationParameters() { - ActorClaimName = "act" + ActorClaimType = "act" }; // Create ClaimsIdentity from JsonElement @@ -88,7 +88,7 @@ public void NestedActorInJsonElementShouldCreateNestedClaimsIdentity() var jsonElement = JsonDocument.Parse(actorJson).RootElement; var tokenValidationParameters = new TokenValidationParameters { - ActorClaimName = "act" + ActorClaimType = "act" }; // Create ClaimsIdentity from JsonElement @@ -145,7 +145,7 @@ public void MultiLevelNestedActorJsonShouldHandleProperDepth() var jsonElement = JsonDocument.Parse(actorJson).RootElement; var tokenValidationParameters = new TokenValidationParameters { - ActorClaimName = "act", + ActorClaimType = "act", MaxActorChainLength = 3 // Allow up to 3 levels }; @@ -211,7 +211,7 @@ public void NestedActorExceedingMaxDepth_ThrowsException() var jsonElement = JsonDocument.Parse(actorJson).RootElement; var tokenValidationParameters = new TokenValidationParameters { - ActorClaimName = "act", + ActorClaimType = "act", MaxActorChainLength = 2, // Only allow 2 levels, but JSON has 3 ActorChainDepth = 1 // Start at depth 1 to simulate being in an ongoing chain }; @@ -265,7 +265,7 @@ public void JsonElementWithArrayValuesShouldProcessCorrectly() var jsonElement = JsonDocument.Parse(actorJson).RootElement; var tokenValidationParameters = new TokenValidationParameters { - ActorClaimName = "act" + ActorClaimType = "act" }; // Create ClaimsIdentity from JsonElement @@ -321,7 +321,7 @@ public void JsonElementWithComplexTypesShouldHandleCorrectly() var jsonElement = JsonDocument.Parse(actorJson).RootElement; var tokenValidationParameters = new TokenValidationParameters { - ActorClaimName = "act" + ActorClaimType = "act" }; // Create ClaimsIdentity from JsonElement @@ -375,7 +375,7 @@ public void NonObjectJsonElement_ThrowsException() var tokenValidationParameters = new TokenValidationParameters { - ActorClaimName = "act" + ActorClaimType = "act" }; // Act - This should throw an ArgumentException @@ -465,7 +465,7 @@ public void CustomActorClaimNameShouldBeRespected() var jsonElement = JsonDocument.Parse(actorJson).RootElement; var tokenValidationParameters = new TokenValidationParameters { - ActorClaimName = "actort" // Custom actor claim name + ActorClaimType = "actort" // Custom actor claim name }; // Create ClaimsIdentity from JsonElement @@ -518,8 +518,8 @@ public void ActorChainDepthShouldBeIncremented() var jsonElement = JsonDocument.Parse(actorJson).RootElement; var tokenValidationParameters = new TokenValidationParameters { - ActorClaimName = "act", - MaxActorChainLength = 5, + ActorClaimType = "act", + MaxActorChainLength = 4, ActorChainDepth = 2 // Start at depth 2 }; @@ -580,8 +580,8 @@ public async Task ValidateTokenAsync_WithActorInToken_ProvidesActorClaimsIdentit { { "act", actorIdentity} }, - ActorClaimName = "act", - MaxActorChainLength = 5 + ActorClaimType = "act", + MaxActorChainLength = 4 }; string token = handler.CreateToken(tokenDescriptor); handler.MapInboundClaims = true; @@ -594,7 +594,7 @@ public async Task ValidateTokenAsync_WithActorInToken_ProvidesActorClaimsIdentit ValidateLifetime = false, IssuerSigningKey = Default.AsymmetricSigningKey, ValidateIssuerSigningKey = true, - ActorClaimName = "act", + ActorClaimType = "act", }; var result = await handler.ValidateTokenAsync(token, validationParameters); @@ -644,7 +644,7 @@ public async Task ValidateTokenAsync_CustomDelegate_WorksWithSimpleAndNestedActo AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); int delegateCallCount = 0; - ClaimsIdentity CustomDelegate(JsonElement element) + ClaimsIdentity CustomDelegate(JsonElement element, TokenValidationParameters tokenValidationParameters = null) { delegateCallCount++; var id = new CaseSensitiveClaimsIdentity("CustomActorAuth"); @@ -692,7 +692,7 @@ ClaimsIdentity CustomDelegate(JsonElement element) ValidateLifetime = false, IssuerSigningKey = Default.AsymmetricSigningKey, ValidateIssuerSigningKey = true, - ActorClaimName = "act", + ActorClaimType = "act", MaxActorChainLength = 3, ActClaimRetrieverDelegate = CustomDelegate }; @@ -754,7 +754,7 @@ public async Task ValidateTokenAsync_NestedActors_DefaultDelegate_CreatesProperC ValidateLifetime = false, IssuerSigningKey = Default.AsymmetricSigningKey, ValidateIssuerSigningKey = true, - ActorClaimName = "act", + ActorClaimType = "act", MaxActorChainLength = 2 }; @@ -815,7 +815,7 @@ public async Task ValidateTokenAsync_NestingBeyondMaxActorChain_ThrowsException( ValidateLifetime = false, IssuerSigningKey = Default.AsymmetricSigningKey, ValidateIssuerSigningKey = true, - ActorClaimName = "act", + ActorClaimType = "act", MaxActorChainLength = 2 }; handler.MapInboundClaims = true; @@ -840,7 +840,7 @@ public async Task ValidateTokenAsync_CustomDelegate_ThrowsExceptionIfDelegateFai var context = new CompareContext($"{this}.ValidateTokenAsync_CustomDelegate_ThrowsIfDelegateFails"); AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); - ClaimsIdentity CustomDelegate(JsonElement element) + ClaimsIdentity CustomDelegate(JsonElement element, TokenValidationParameters tokenValidationParameters = null) { throw new InvalidOperationException("Delegate failure"); } @@ -872,7 +872,7 @@ ClaimsIdentity CustomDelegate(JsonElement element) ValidateLifetime = false, IssuerSigningKey = Default.AsymmetricSigningKey, ValidateIssuerSigningKey = true, - ActorClaimName = "act", + ActorClaimType = "act", MaxActorChainLength = 2, ActClaimRetrieverDelegate = CustomDelegate }; @@ -898,7 +898,7 @@ public async Task ValidateTokenAsync_ActorAsSubjectAndClaimsDictionary_DefaultAn var context = new CompareContext($"{this}.ValidateTokenAsync_ActorAsSubjectAndClaimsDictionary_DefaultAndCustomDelegate"); AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); - ClaimsIdentity CustomDelegate(JsonElement element) + ClaimsIdentity CustomDelegate(JsonElement element, TokenValidationParameters tokenValidationParameters = null) { var id = new CaseSensitiveClaimsIdentity("CustomActorAuth"); if (element.TryGetProperty("sub", out var sub)) @@ -934,7 +934,7 @@ ClaimsIdentity CustomDelegate(JsonElement element) ValidateLifetime = false, IssuerSigningKey = Default.AsymmetricSigningKey, ValidateIssuerSigningKey = true, - ActorClaimName = "act" + ActorClaimType = "act" }; // Default delegate diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActClaimTests/ActClaimSerializationTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActClaimTests/ActClaimSerializationTests.cs index d4f8a163c2..6adfdb6a4a 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActClaimTests/ActClaimSerializationTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActClaimTests/ActClaimSerializationTests.cs @@ -52,9 +52,9 @@ public void ActorTokenInClaimsDictionaryShouldBeProperlySerialized() JsonWebToken decodedToken = tokenHandler.ReadJsonWebToken(token); // Verify actor claim exists in the token - Assert.True(decodedToken.Payload.HasClaim(tokenDescriptor.ActorClaimName), "JWT token should contain 'actort' claim"); + Assert.True(decodedToken.Payload.HasClaim(tokenDescriptor.ActorClaimType), "JWT token should contain 'actort' claim"); // Verify the actor object directly - var actorObject = decodedToken.Payload.GetValue(tokenDescriptor.ActorClaimName); + var actorObject = decodedToken.Payload.GetValue(tokenDescriptor.ActorClaimType); Assert.Equal(JsonValueKind.Object, actorObject.ValueKind); // Verify actor claims directly from the JSON object @@ -109,13 +109,13 @@ public void ActorTokenAsSubjectShouldBeProperlySerialized() JsonWebToken decodedToken = tokenHandler.ReadJsonWebToken(token); // Verify actor claim exists in the token - Assert.True(decodedToken.Payload.HasClaim(tokenDescriptor.ActorClaimName), "JWT token should contain 'act' claim"); + Assert.True(decodedToken.Payload.HasClaim(tokenDescriptor.ActorClaimType), "JWT token should contain 'act' claim"); // Verify actor claim exists in the token - Assert.True(decodedToken.Payload.HasClaim(tokenDescriptor.ActorClaimName), "JWT token should contain actor claim"); + Assert.True(decodedToken.Payload.HasClaim(tokenDescriptor.ActorClaimType), "JWT token should contain actor claim"); // Verify the actor object directly - var actorObject = decodedToken.Payload.GetValue(tokenDescriptor.ActorClaimName); + var actorObject = decodedToken.Payload.GetValue(tokenDescriptor.ActorClaimType); Assert.Equal(JsonValueKind.Object, actorObject.ValueKind); // Verify actor claims directly from the JSON object @@ -257,7 +257,7 @@ public void NestedActorTokenInClaimsDictionaryShouldBeProperlySerialized() Assert.Equal("Actor Name", actorObject.GetProperty("name").GetString()); // Verify nested actor exists and is a JSON object - Assert.True(actorObject.TryGetProperty(tokenDescriptor.ActorClaimName, out var nestedActorElement)); + Assert.True(actorObject.TryGetProperty(tokenDescriptor.ActorClaimType, out var nestedActorElement)); Assert.Equal(JsonValueKind.Object, nestedActorElement.ValueKind); // Verify nested actor claims directly from JSON object @@ -309,7 +309,7 @@ public void NestedActorTokenAsSubjectShouldBeProperlySerialized() Audience = "https://api.example.com", Expires = DateTime.UtcNow.AddHours(1), SigningCredentials = Default.AsymmetricSigningCredentials, - ActorClaimName = "act", + ActorClaimType = "act", }; AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); var token = tokenHandler.CreateToken(tokenDescriptor); @@ -329,7 +329,7 @@ public void NestedActorTokenAsSubjectShouldBeProperlySerialized() Assert.Equal("Actor Name", actorObject.GetProperty("name").GetString()); // Verify nested actor exists and is a JSON object - Assert.True(actorObject.TryGetProperty(tokenDescriptor.ActorClaimName, out var nestedActorElement)); + Assert.True(actorObject.TryGetProperty(tokenDescriptor.ActorClaimType, out var nestedActorElement)); Assert.Equal(JsonValueKind.Object, nestedActorElement.ValueKind); Console.WriteLine("nested token created: " + nestedActorElement.ToString()); @@ -359,11 +359,11 @@ public void MaxActorChainLength_RejectsNegativeValues() SigningCredentials = Default.AsymmetricSigningCredentials }; AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); - tokenDescriptor.ActorClaimName = "act"; // Set the actor claim name to "act" for testing + tokenDescriptor.ActorClaimType = "act"; // Set the actor claim name to "act" for testing int originalValue = tokenDescriptor.MaxActorChainLength; try { - tokenDescriptor.ActorClaimName = "act"; // Set the actor claim name to "act" for testing + tokenDescriptor.ActorClaimType = "act"; // Set the actor claim name to "act" for testing // Act & Assert - Valid value 0 should not throw tokenDescriptor.MaxActorChainLength = 0; Assert.Equal(0, tokenDescriptor.MaxActorChainLength); @@ -430,7 +430,7 @@ public void NestedSubjectActorTokens_ExceedingMaxDepth_ThrowsException() Issuer = "https://example.com", Audience = "https://api.example.com", SigningCredentials = Default.AsymmetricSigningCredentials, - ActorClaimName = "act", + ActorClaimType = "act", MaxActorChainLength = 2 }; @@ -499,7 +499,7 @@ public void NestedClaimsDictionaryActorTokens_ExceedingMaxDepth_ThrowsException( { { actorname, actorIdentity } }, - ActorClaimName = actorname, + ActorClaimType = actorname, MaxActorChainLength = 1 }; @@ -570,7 +570,7 @@ public void ActorTokens_MixedSourceRespectMaxActorChainLength() { { actorname, level2Actor } }, - ActorClaimName = actorname, + ActorClaimType = actorname, MaxActorChainLength = 1 }; @@ -579,7 +579,7 @@ public void ActorTokens_MixedSourceRespectMaxActorChainLength() // Assert - Check actor object structure Assert.True(jwtToken.Payload.HasClaim(actorname), "JWT token should contain 'act' claim"); - var actorObject = jwtToken.Payload.GetValue(tokenDescriptor.ActorClaimName); + var actorObject = jwtToken.Payload.GetValue(tokenDescriptor.ActorClaimType); Assert.Equal(JsonValueKind.Object, actorObject.ValueKind); @@ -638,7 +638,7 @@ public void NestedClaimTokens_ExceedingMaxDepth_ThrowsException() { { "act", level1Actor } }, - ActorClaimName = actorname, + ActorClaimType = actorname, MaxActorChainLength = 1 }; AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); diff --git a/test/Microsoft.IdentityModel.TestUtils/ValidationDelegates.cs b/test/Microsoft.IdentityModel.TestUtils/ValidationDelegates.cs index 0b809d950b..63d4b7083a 100644 --- a/test/Microsoft.IdentityModel.TestUtils/ValidationDelegates.cs +++ b/test/Microsoft.IdentityModel.TestUtils/ValidationDelegates.cs @@ -251,7 +251,7 @@ public static string TypeValidator(string type, SecurityToken securityToken, Tok return type; } - public static readonly ActClaimRetrieverDelegate ActClaimRetrieverDelegate = (actorClaim) => + public static readonly ActClaimRetrieverDelegate ActClaimRetrieverDelegate = (actorClaim, tokenValidationParameters) => { return null; }; diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/TokenValidationParametersTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/TokenValidationParametersTests.cs index a94158a6e0..b0ff1c1758 100644 --- a/test/Microsoft.IdentityModel.Tokens.Tests/TokenValidationParametersTests.cs +++ b/test/Microsoft.IdentityModel.Tokens.Tests/TokenValidationParametersTests.cs @@ -15,7 +15,7 @@ namespace Microsoft.IdentityModel.Tokens.Tests { public class TokenValidationParametersTests { - int ExpectedPropertyCount = 67; + int ExpectedPropertyCount = 66; // GetSets() compares the total property count which includes internal properties, against a list of public properties, minus delegates. // This allows us to keep track of any properties we are including in the total that are not public nor delegates. @@ -198,9 +198,8 @@ public void GetSets() { PropertyNamesAndSetGetValue = new List>> { - new KeyValuePair>("ActorClaimName", new List{"actort"}), + new KeyValuePair>("ActorClaimType", new List{"actort"}), new KeyValuePair>("ActorChainDepth", new List{0,1}), - new KeyValuePair>("ActorTokenValidationParameters", new List{(TokenValidationParameters)null, new TokenValidationParameters(), new TokenValidationParameters()}), new KeyValuePair>("ActorValidationParameters", new List{(TokenValidationParameters)null, new TokenValidationParameters(), new TokenValidationParameters()}), new KeyValuePair>("AuthenticationType", new List{(string)null, Guid.NewGuid().ToString(), Guid.NewGuid().ToString()}), new KeyValuePair>("ClockSkew", new List{TokenValidationParameters.DefaultClockSkew, TimeSpan.FromHours(2), TimeSpan.FromMinutes(1)}), @@ -215,7 +214,7 @@ public void GetSets() new KeyValuePair>("IssuerSigningKeys", new List{(IEnumerable)null, new List{KeyingMaterial.DefaultX509Key_2048, KeyingMaterial.RsaSecurityKey_1024}, new List()}), new KeyValuePair>("LogTokenId", new List{true, false, true}), new KeyValuePair>("LogValidationExceptions", new List{true, false, true}), - new KeyValuePair>("MaxActorChainLength", new List{5,2}), + new KeyValuePair>("MaxActorChainLength", new List{4,2}), new KeyValuePair>("NameClaimType", new List{ClaimsIdentity.DefaultNameClaimType, Guid.NewGuid().ToString(), Guid.NewGuid().ToString()}), new KeyValuePair>("PropertyBag", new List{(IDictionary)null, new Dictionary {{"CustomKey", "CustomValue"}}, new Dictionary()}), new KeyValuePair>("RefreshBeforeValidation", new List{false, true, false}), From bd78d2e16553148e8bc6891ed18e5d8140efc2b4 Mon Sep 17 00:00:00 2001 From: saurabhsathe-ms Date: Fri, 23 May 2025 15:23:35 -0700 Subject: [PATCH 43/52] Some more NIT repairs --- src/Microsoft.IdentityModel.JsonWebTokens/LogMessages.cs | 2 +- src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs | 1 + src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/LogMessages.cs b/src/Microsoft.IdentityModel.JsonWebTokens/LogMessages.cs index 2c08c4baa6..62696d684a 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/LogMessages.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/LogMessages.cs @@ -26,7 +26,7 @@ internal static class LogMessages internal const string IDX14112 = "IDX14112: Only a single 'Actor' is supported. Found second claim of type: '{0}'"; internal const string IDX14113 = "IDX14113: A duplicate value for 'SecurityTokenDescriptor.{0}' exists in 'SecurityTokenDescriptor.Claims'. \nThe value of 'SecurityTokenDescriptor.{0}' is used."; internal const string IDX14114 = "IDX14114: Both '{0}.{1}' and '{0}.{2}' are null or empty."; - internal const string IDX14115 = "IDX14115: Unable to validate Actor token as act claim. Act claim validation is enabled but ActorTokenValidationDelegate was not provided"; + // internal const string IDX14115 = "IDX14115:"; internal const string IDX14116 = "IDX14116: '{0}' cannot contain the following claims: '{1}'. These values are added by default (if necessary) during security token creation."; // number of sections 'dots' is not correct internal const string IDX14120 = "IDX14120: JWT is not well formed, there is only one dot (.).\nThe token needs to be in JWS or JWE Compact Serialization Format. (JWS): 'EncodedHeader.EncodedPayload.EncodedSignature'. (JWE): 'EncodedProtectedHeader.EncodedEncryptedKey.EncodedInitializationVector.EncodedCiphertext.EncodedAuthenticationTag'."; diff --git a/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs b/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs index 038d8b0255..8e26a8e610 100644 --- a/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs +++ b/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs @@ -19,6 +19,7 @@ public class SecurityTokenDescriptor private string _actorClaimType = "act"; private int _actorClainDepth; private int _maxActorChainLength = 4; + /// /// Gets or sets the value of the {"": audience} claim. Will be combined with and any "Aud" claims in /// or when creating a token. diff --git a/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs b/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs index 00356fd161..dc5ad96689 100644 --- a/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs +++ b/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs @@ -232,7 +232,7 @@ public virtual TokenValidationParameters Clone() { return new(this) { - IsClone = true, + IsClone = true }; } From 78cc3bc5fa3df593173dda2df73f493666686be7 Mon Sep 17 00:00:00 2001 From: saurabhsathe-ms Date: Tue, 27 May 2025 10:01:49 -0700 Subject: [PATCH 44/52] Updated the code custom actclaimretrievervalidator call with token validaiton parameters so that customer can use them --- .../JsonWebTokenHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs index 6335f744ac..a518d19c0c 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs @@ -641,7 +641,7 @@ private ClaimsIdentity CreateClaimsIdentityActor( { try { - return tokenValidationParameters.ActClaimRetrieverDelegate(actClaim); + return tokenValidationParameters.ActClaimRetrieverDelegate(actClaim, tokenValidationParameters); } catch (Exception ex) { From bbb2f02b0ae5c2937c3eb2ee17e89ae1c4c30438 Mon Sep 17 00:00:00 2001 From: saurabhsathe-ms Date: Tue, 27 May 2025 10:11:55 -0700 Subject: [PATCH 45/52] Updated summary for our new AppContextSwitch --- .../AppContextSwitches.cs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.IdentityModel.Tokens/AppContextSwitches.cs b/src/Microsoft.IdentityModel.Tokens/AppContextSwitches.cs index 73449d9d61..cbab50c585 100644 --- a/src/Microsoft.IdentityModel.Tokens/AppContextSwitches.cs +++ b/src/Microsoft.IdentityModel.Tokens/AppContextSwitches.cs @@ -99,7 +99,24 @@ internal static class AppContextSwitches internal static bool UseCapitalizedXMLTypeAttr => _useCapitalizedXMLTypeAttr ??= (AppContext.TryGetSwitch(UseCapitalizedXMLTypeAttrSwitch, out bool useCapitalizedXMLTypeAttr) && useCapitalizedXMLTypeAttr); /// - /// This switch enables the support for act claim. When enabled the actor claim will be serialized and deserialized into JsonWebToken. + /// Controls how JWT actor claims are handled in the Microsoft.IdentityModel libraries. + /// + /// When enabled (set to true): + /// - Actor claims use the "act" claim name by default + /// - Actor claims are serialized as JSON objects + /// - Nested actor claims (actor-within-actor) are supported + /// - Claims from actor tokens appear in the ClaimsIdentity.Actor property + /// - MaxActorChainLength and ActorChainDepth properties control nesting depth validation + /// - Custom ActClaimRetrieverDelegate can be used for custom actor claim handling + /// + /// When disabled (default setting): + /// - Actor claims use the legacy "actort" claim name + /// - Actor claims are expected to be string-encoded JWT tokens + /// - Only simple actor relationships are supported (no deep nesting) + /// + /// Usage: + /// AppContext.SetSwitch("Switch.Microsoft.IdentityModel.EnableActClaimSupportSwitch", true); + /// New functionality is only available when enabled. /// internal const string EnableActClaimSupportSwitch = "Switch.Microsoft.IdentityModel.EnableActClaimSupportSwitch"; private static bool? _enableActClaimSupport; From e7952d1bfb972fe9f25cec05f9084284ea5c9a23 Mon Sep 17 00:00:00 2001 From: saurabhsathe-ms Date: Wed, 4 Jun 2025 12:01:20 -0700 Subject: [PATCH 46/52] Removed App context switch replaced it with request based property --- .../JsonWebTokenHandler.CreateToken.cs | 7 +- .../JsonWebTokenHandler.cs | 4 +- .../AppContextSwitches.cs | 25 - .../PublicAPI.Unshipped.txt | 4 + .../SecurityTokenDescriptor.cs | 30 +- .../TokenValidationParameters.cs | 31 +- .../ActClaimDeserializationTests.cs | 504 +++++++----------- .../ActClaimSerializationTests.cs | 347 +++++------- .../TokenValidationParametersTests.cs | 3 +- 9 files changed, 404 insertions(+), 551 deletions(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs index 4efb65557d..ea06fcaf94 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs @@ -668,12 +668,11 @@ internal static void WriteJwsPayload( // Duplicates are resolved according to the following priority: // SecurityTokenDescriptor.{Audience/Audiences, Issuer, Expires, IssuedAt, NotBefore}, SecurityTokenDescriptor.Claims, SecurityTokenDescriptor.Subject.Claims // SecurityTokenDescriptor.Claims are KeyValuePairs, whereas SecurityTokenDescriptor.Subject.Claims are System.Security.Claims.Claim and are processed differently. - if (tokenDescriptor.Claims != null && tokenDescriptor.Claims.Count > 0) { foreach (KeyValuePair kvp in tokenDescriptor.Claims) { - if (AppContextSwitches.EnableActClaimSupport && kvp.Key.Equals(tokenDescriptor.ActorClaimType, StringComparison.Ordinal)) + if (tokenDescriptor.ActClaimSupportEnabled && kvp.Key.Equals(tokenDescriptor.ActorClaimType, StringComparison.Ordinal)) { continue; } @@ -758,7 +757,7 @@ internal static void WriteJwsPayload( JsonPrimitives.WriteObject(ref writer, kvp.Key, kvp.Value); } } - if (AppContextSwitches.EnableActClaimSupport) + if (tokenDescriptor.ActClaimSupportEnabled) WriteActorToken(writer, tokenDescriptor, setDefaultTimesOnTokenCreation, tokenLifetimeInMinutes); AddSubjectClaims(ref writer, tokenDescriptor, audienceSet, issuerSet, ref expSet, ref iatSet, ref nbfSet); @@ -1129,7 +1128,7 @@ private static SecurityTokenDescriptor CreateActorTokenDescriptor(SecurityTokenD if (actorTokenDescriptor != null) { ValidateActorChainDepth(tokenDescriptor); - + actorTokenDescriptor.ActClaimSupportEnabled = tokenDescriptor.ActClaimSupportEnabled; actorTokenDescriptor.MaxActorChainLength = tokenDescriptor.MaxActorChainLength; actorTokenDescriptor.ActorClaimType = tokenDescriptor.ActorClaimType; actorTokenDescriptor.ActorChainDepth = tokenDescriptor.ActorChainDepth + 1; diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs index a518d19c0c..8f576c9cf8 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs @@ -216,6 +216,7 @@ private ClaimsIdentity CreateClaimsIdentityWithMapping(JsonWebToken jwtToken, To _ = validationParameters ?? throw LogHelper.LogArgumentNullException(nameof(validationParameters)); ClaimsIdentity identity = validationParameters.CreateClaimsIdentity(jwtToken, issuer); + Console.WriteLine($"*********************************Here in this --- {validationParameters.ActorClaimType}?"); foreach (Claim jwtClaim in jwtToken.Claims) { bool wasMapped = _inboundClaimTypeMap.TryGetValue(jwtClaim.Type, out string claimType); @@ -230,7 +231,6 @@ private ClaimsIdentity CreateClaimsIdentityWithMapping(JsonWebToken jwtToken, To LogMessages.IDX14112, LogHelper.MarkAsNonPII(JwtRegisteredClaimNames.Actort), jwtClaim.Value))); - identity.Actor = CreateClaimsIdentityActor(jwtToken, jwtClaim.Value, validationParameters); } @@ -633,7 +633,7 @@ private ClaimsIdentity CreateClaimsIdentityActor( if (tokenValidationParameters == null) throw LogHelper.LogArgumentNullException(nameof(tokenValidationParameters)); - if (AppContextSwitches.EnableActClaimSupport) + if (tokenValidationParameters.ActClaimSupportEnabled) { if (jwtToken.TryGetPayloadValue(tokenValidationParameters.ActorClaimType, out JsonElement actClaim)) { diff --git a/src/Microsoft.IdentityModel.Tokens/AppContextSwitches.cs b/src/Microsoft.IdentityModel.Tokens/AppContextSwitches.cs index cbab50c585..bfc98f1c4e 100644 --- a/src/Microsoft.IdentityModel.Tokens/AppContextSwitches.cs +++ b/src/Microsoft.IdentityModel.Tokens/AppContextSwitches.cs @@ -98,29 +98,6 @@ internal static class AppContextSwitches private static bool? _useCapitalizedXMLTypeAttr; internal static bool UseCapitalizedXMLTypeAttr => _useCapitalizedXMLTypeAttr ??= (AppContext.TryGetSwitch(UseCapitalizedXMLTypeAttrSwitch, out bool useCapitalizedXMLTypeAttr) && useCapitalizedXMLTypeAttr); - /// - /// Controls how JWT actor claims are handled in the Microsoft.IdentityModel libraries. - /// - /// When enabled (set to true): - /// - Actor claims use the "act" claim name by default - /// - Actor claims are serialized as JSON objects - /// - Nested actor claims (actor-within-actor) are supported - /// - Claims from actor tokens appear in the ClaimsIdentity.Actor property - /// - MaxActorChainLength and ActorChainDepth properties control nesting depth validation - /// - Custom ActClaimRetrieverDelegate can be used for custom actor claim handling - /// - /// When disabled (default setting): - /// - Actor claims use the legacy "actort" claim name - /// - Actor claims are expected to be string-encoded JWT tokens - /// - Only simple actor relationships are supported (no deep nesting) - /// - /// Usage: - /// AppContext.SetSwitch("Switch.Microsoft.IdentityModel.EnableActClaimSupportSwitch", true); - /// New functionality is only available when enabled. - /// - internal const string EnableActClaimSupportSwitch = "Switch.Microsoft.IdentityModel.EnableActClaimSupportSwitch"; - private static bool? _enableActClaimSupport; - internal static bool EnableActClaimSupport => _enableActClaimSupport ??= (AppContext.TryGetSwitch(EnableActClaimSupportSwitch, out bool EnableActClaimSupport) && EnableActClaimSupport); /// /// Used for testing to reset all switches to its default value. /// @@ -147,8 +124,6 @@ internal static void ResetAllSwitches() _useCapitalizedXMLTypeAttr = null; AppContext.SetSwitch(UseCapitalizedXMLTypeAttrSwitch, false); - _enableActClaimSupport = null; - AppContext.SetSwitch(EnableActClaimSupportSwitch, false); } } } diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI.Unshipped.txt index 9524c45c50..43758f9a0e 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI.Unshipped.txt @@ -1,3 +1,7 @@ +Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActClaimSupportEnabled.get -> bool +Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActClaimSupportEnabled.set -> void +Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActClaimSupportEnabled.get -> bool +Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActClaimSupportEnabled.set -> void Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActorChainDepth.get -> int Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActorChainDepth.set -> void Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActorClaimType.get -> string diff --git a/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs b/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs index 8e26a8e610..401074839b 100644 --- a/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs +++ b/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs @@ -19,7 +19,7 @@ public class SecurityTokenDescriptor private string _actorClaimType = "act"; private int _actorClainDepth; private int _maxActorChainLength = 4; - + private bool _actClaimSupportEnabled; /// /// Gets or sets the value of the {"": audience} claim. Will be combined with and any "Aud" claims in /// or when creating a token. @@ -152,7 +152,7 @@ public int MaxActorChainLength /// /// Gets or sets the claim type that identifies the actor claim in tokens. - /// The default value is "actort" when is off + /// The default value is "actort" when is off /// and "act" when the switch is on. /// This property determines which claim in a token contains the actor information during token /// validation and creation. @@ -202,5 +202,31 @@ public int ActorChainDepth _actorClainDepth = value; } } + + + /// + /// Controls how JWT actor claims are handled in the Microsoft.IdentityModel libraries. + /// + /// When enabled (set to true): + /// - Actor claims use the "act" claim name by default + /// - Actor claims are serialized as JSON objects + /// - Nested actor claims (actor-within-actor) are supported + /// - Claims from actor tokens appear in the ClaimsIdentity.Actor property + /// - MaxActorChainLength and ActorChainDepth properties control nesting depth validation + /// - Custom ActClaimRetrieverDelegate can be used for custom actor claim handling + /// + /// When disabled (default setting): + /// - Actor claims use the legacy "actort" claim name + /// - Actor claims are expected to be string-encoded JWT tokens + /// - Only simple actor relationships are supported (no deep nesting) + /// + public bool ActClaimSupportEnabled + { + get => _actClaimSupportEnabled; + set + { + _actClaimSupportEnabled = value; + } + } } } diff --git a/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs b/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs index dc5ad96689..8e04d238d5 100644 --- a/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs +++ b/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs @@ -20,6 +20,7 @@ public partial class TokenValidationParameters private string _nameClaimType = ClaimsIdentity.DefaultNameClaimType; private string _roleClaimType = ClaimsIdentity.DefaultRoleClaimType; private Dictionary _instancePropertyBag; + private bool _actClaimSupportEnabled; /// /// This is the default value of when creating a . @@ -133,6 +134,7 @@ protected TokenValidationParameters(TokenValidationParameters other) MaxActorChainLength = other.MaxActorChainLength; ActorChainDepth = other.ActorChainDepth; ActorClaimType = other.ActorClaimType; + ActClaimSupportEnabled = other.ActClaimSupportEnabled; } /// @@ -787,7 +789,7 @@ public string RoleClaimType /// /// Gets or sets the claim type that identifies the actor claim in tokens. - /// The default value is "actort" when is off + /// The default value is "actort" when is off /// and "act" when the switch is on. /// This property determines which claim in a token contains the actor information during token /// validation and creation. @@ -803,7 +805,7 @@ public string RoleClaimType /// public string ActorClaimType { - get => AppContextSwitches.EnableActClaimSupport ? actorClaimType : "actort"; + get => _actClaimSupportEnabled ? actorClaimType : "actort"; set { if (string.IsNullOrEmpty(value)) @@ -888,5 +890,30 @@ public int MaxActorChainLength maxActorChainLength = value; } } + + /// + /// Controls how JWT actor claims are handled in the Microsoft.IdentityModel libraries. + /// + /// When enabled (set to true): + /// - Actor claims use the "act" claim name by default + /// - Actor claims are serialized as JSON objects + /// - Nested actor claims (actor-within-actor) are supported + /// - Claims from actor tokens appear in the ClaimsIdentity.Actor property + /// - MaxActorChainLength and ActorChainDepth properties control nesting depth validation + /// - Custom ActClaimRetrieverDelegate can be used for custom actor claim handling + /// + /// When disabled (default setting): + /// - Actor claims use the legacy "actort" claim name + /// - Actor claims are expected to be string-encoded JWT tokens + /// - Only simple actor relationships are supported (no deep nesting) + /// + public bool ActClaimSupportEnabled + { + get => _actClaimSupportEnabled; + set + { + _actClaimSupportEnabled = value; + } + } } } diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActClaimTests/ActClaimDeserializationTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActClaimTests/ActClaimDeserializationTests.cs index 21dba24543..c9b5046908 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActClaimTests/ActClaimDeserializationTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActClaimTests/ActClaimDeserializationTests.cs @@ -14,15 +14,10 @@ namespace Microsoft.IdentityModel.JsonWebTokens.Tests.ActClaimTests { public class ActClaimDeserializationTests { - // Tests for creating ClaimsIdentity from JsonElement - [ResetAppContextSwitches] [Fact] public void BasicJsonElementShouldCreateClaimsIdentityCorrectly() { var context = new CompareContext($"{this}.BasicJsonElementShouldCreateClaimsIdentityCorrectly"); - bool switchValue = false; - AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); try { // Create a simple JSON Element that represents an actor token @@ -35,7 +30,8 @@ public void BasicJsonElementShouldCreateClaimsIdentityCorrectly() var jsonElement = JsonDocument.Parse(actorJson).RootElement; var validationParameters = new TokenValidationParameters() { - ActorClaimType = "act" + ActorClaimType = "act", + ActClaimSupportEnabled = true, }; // Create ClaimsIdentity from JsonElement @@ -59,20 +55,12 @@ public void BasicJsonElementShouldCreateClaimsIdentityCorrectly() { context.Diffs.Add($"Exception: {ex}"); } - finally - { - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); - } } - [ResetAppContextSwitches] [Fact] public void NestedActorInJsonElementShouldCreateNestedClaimsIdentity() { var context = new CompareContext($"{this}.NestedActorInJsonElementShouldCreateNestedClaimsIdentity"); - bool switchValue = false; - AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); try { // Create nested actor JSON structure @@ -88,7 +76,8 @@ public void NestedActorInJsonElementShouldCreateNestedClaimsIdentity() var jsonElement = JsonDocument.Parse(actorJson).RootElement; var tokenValidationParameters = new TokenValidationParameters { - ActorClaimType = "act" + ActorClaimType = "act", + ActClaimSupportEnabled = true, }; // Create ClaimsIdentity from JsonElement @@ -112,20 +101,12 @@ public void NestedActorInJsonElementShouldCreateNestedClaimsIdentity() { context.Diffs.Add($"Exception: {ex}"); } - finally - { - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); - } } - [ResetAppContextSwitches] [Fact] public void MultiLevelNestedActorJsonShouldHandleProperDepth() { var context = new CompareContext($"{this}.MultiLevelNestedActorJsonShouldHandleProperDepth"); - bool switchValue = false; - AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); try { // Create a three-level nested actor JSON structure @@ -146,7 +127,8 @@ public void MultiLevelNestedActorJsonShouldHandleProperDepth() var tokenValidationParameters = new TokenValidationParameters { ActorClaimType = "act", - MaxActorChainLength = 3 // Allow up to 3 levels + MaxActorChainLength = 3, + ActClaimSupportEnabled = true }; // Create ClaimsIdentity from JsonElement @@ -178,20 +160,12 @@ public void MultiLevelNestedActorJsonShouldHandleProperDepth() { context.Diffs.Add($"Exception: {ex}"); } - finally - { - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); - } } - [ResetAppContextSwitches] [Fact] public void NestedActorExceedingMaxDepth_ThrowsException() { var context = new CompareContext($"{this}.NestedActorExceedingMaxDepth_ThrowsException"); - bool switchValue = false; - AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); try { // Create a three-level nested actor but set max depth to 2 @@ -212,8 +186,9 @@ public void NestedActorExceedingMaxDepth_ThrowsException() var tokenValidationParameters = new TokenValidationParameters { ActorClaimType = "act", - MaxActorChainLength = 2, // Only allow 2 levels, but JSON has 3 - ActorChainDepth = 1 // Start at depth 1 to simulate being in an ongoing chain + MaxActorChainLength = 2, + ActorChainDepth = 1, + ActClaimSupportEnabled = true }; // Act - This should throw a SecurityTokenException @@ -239,20 +214,12 @@ public void NestedActorExceedingMaxDepth_ThrowsException() context.Diffs.Add($"Unexpected exception type: {ex.GetType()}, Message: {ex.Message}"); TestUtilities.AssertFailIfErrors(context); } - finally - { - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); - } } - [ResetAppContextSwitches] [Fact] public void JsonElementWithArrayValuesShouldProcessCorrectly() { var context = new CompareContext($"{this}.JsonElementWithArrayValuesShouldProcessCorrectly"); - bool switchValue = false; - AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); try { // Create JSON with array value @@ -265,7 +232,8 @@ public void JsonElementWithArrayValuesShouldProcessCorrectly() var jsonElement = JsonDocument.Parse(actorJson).RootElement; var tokenValidationParameters = new TokenValidationParameters { - ActorClaimType = "act" + ActorClaimType = "act", + ActClaimSupportEnabled = true }; // Create ClaimsIdentity from JsonElement @@ -291,20 +259,12 @@ public void JsonElementWithArrayValuesShouldProcessCorrectly() { context.Diffs.Add($"Exception: {ex}"); } - finally - { - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); - } } - [ResetAppContextSwitches] [Fact] public void JsonElementWithComplexTypesShouldHandleCorrectly() { var context = new CompareContext($"{this}.JsonElementWithComplexTypesShouldHandleCorrectly"); - bool switchValue = false; - AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); try { // Create JSON with complex types (objects) @@ -321,7 +281,8 @@ public void JsonElementWithComplexTypesShouldHandleCorrectly() var jsonElement = JsonDocument.Parse(actorJson).RootElement; var tokenValidationParameters = new TokenValidationParameters { - ActorClaimType = "act" + ActorClaimType = "act", + ActClaimSupportEnabled = true, }; // Create ClaimsIdentity from JsonElement @@ -353,20 +314,12 @@ public void JsonElementWithComplexTypesShouldHandleCorrectly() { context.Diffs.Add($"Exception: {ex}"); } - finally - { - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); - } } - [ResetAppContextSwitches] [Fact] public void NonObjectJsonElement_ThrowsException() { var context = new CompareContext($"{this}.NonObjectJsonElement_ThrowsException"); - bool switchValue = false; - AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); try { // Create a non-object JSON Element (string) @@ -375,7 +328,8 @@ public void NonObjectJsonElement_ThrowsException() var tokenValidationParameters = new TokenValidationParameters { - ActorClaimType = "act" + ActorClaimType = "act", + ActClaimSupportEnabled = true, }; // Act - This should throw an ArgumentException @@ -397,20 +351,12 @@ public void NonObjectJsonElement_ThrowsException() context.Diffs.Add($"Unexpected exception type: {ex.GetType()}, Message: {ex.Message}"); TestUtilities.AssertFailIfErrors(context); } - finally - { - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); - } } - [ResetAppContextSwitches] [Fact] public void NullValidationParameters_ThrowsException() { var context = new CompareContext($"{this}.NullValidationParameters_ThrowsException"); - bool switchValue = false; - AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); try { // Create a simple JSON Element @@ -436,20 +382,12 @@ public void NullValidationParameters_ThrowsException() context.Diffs.Add($"Unexpected exception type: {ex.GetType()}, Message: {ex.Message}"); TestUtilities.AssertFailIfErrors(context); } - finally - { - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); - } } - [ResetAppContextSwitches] [Fact] public void CustomActorClaimNameShouldBeRespected() { var context = new CompareContext($"{this}.CustomActorClaimNameShouldBeRespected"); - bool switchValue = false; - AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); try { // Create JSON with custom actor claim name @@ -465,7 +403,8 @@ public void CustomActorClaimNameShouldBeRespected() var jsonElement = JsonDocument.Parse(actorJson).RootElement; var tokenValidationParameters = new TokenValidationParameters { - ActorClaimType = "actort" // Custom actor claim name + ActorClaimType = "actort", + ActClaimSupportEnabled = true, }; // Create ClaimsIdentity from JsonElement @@ -489,20 +428,12 @@ public void CustomActorClaimNameShouldBeRespected() { context.Diffs.Add($"Exception: {ex}"); } - finally - { - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); - } } - [ResetAppContextSwitches] [Fact] public void ActorChainDepthShouldBeIncremented() { var context = new CompareContext($"{this}.ActorChainDepthShouldBeIncremented"); - bool switchValue = false; - AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); try { // Create actor JSON with nested actor @@ -520,7 +451,8 @@ public void ActorChainDepthShouldBeIncremented() { ActorClaimType = "act", MaxActorChainLength = 4, - ActorChainDepth = 2 // Start at depth 2 + ActorChainDepth = 2, + ActClaimSupportEnabled = true, }; // Create ClaimsIdentity from JsonElement @@ -541,20 +473,12 @@ public void ActorChainDepthShouldBeIncremented() { context.Diffs.Add($"Exception: {ex}"); } - finally - { - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); - } } - [ResetAppContextSwitches] [Fact] public async Task ValidateTokenAsync_WithActorInToken_ProvidesActorClaimsIdentity() { var context = new CompareContext($"{this}.ValidateTokenAsync_WithActorInToken_ProvidesActorClaimsIdentity"); - bool switchValue = false; - AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); try { // Create a token with an actor claim @@ -581,7 +505,8 @@ public async Task ValidateTokenAsync_WithActorInToken_ProvidesActorClaimsIdentit { "act", actorIdentity} }, ActorClaimType = "act", - MaxActorChainLength = 4 + MaxActorChainLength = 4, + ActClaimSupportEnabled = true, }; string token = handler.CreateToken(tokenDescriptor); handler.MapInboundClaims = true; @@ -589,6 +514,7 @@ public async Task ValidateTokenAsync_WithActorInToken_ProvidesActorClaimsIdentit // Validate token var validationParameters = new TokenValidationParameters { + ActClaimSupportEnabled = true, ValidateIssuer = false, ValidateAudience = false, ValidateLifetime = false, @@ -631,17 +557,12 @@ public async Task ValidateTokenAsync_WithActorInToken_ProvidesActorClaimsIdentit context.Diffs.Add($"Exception: {ex}"); TestUtilities.AssertFailIfErrors(context); } - finally - { - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); - } } - [ResetAppContextSwitches] + [Fact] public async Task ValidateTokenAsync_CustomDelegate_WorksWithSimpleAndNestedActors() { var context = new CompareContext($"{this}.ValidateTokenAsync_CustomDelegate_WorksWithSimpleAndNestedActors"); - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); int delegateCallCount = 0; ClaimsIdentity CustomDelegate(JsonElement element, TokenValidationParameters tokenValidationParameters = null) @@ -657,128 +578,112 @@ ClaimsIdentity CustomDelegate(JsonElement element, TokenValidationParameters tok return id; } - try - { - // Nested actor - var nestedActor = new CaseSensitiveClaimsIdentity("NestedActorAuth"); - nestedActor.AddClaim(new Claim("sub", "nested-actor-id")); - nestedActor.AddClaim(new Claim("name", "Nested Actor")); - - var actor = new CaseSensitiveClaimsIdentity("ActorAuth"); - actor.AddClaim(new Claim("sub", "actor-subject-id")); - actor.AddClaim(new Claim("name", "Actor Name")); - actor.Actor = nestedActor; - - var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); - mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); - mainIdentity.AddClaim(new Claim("name", "Main User")); - - var handler = new JsonWebTokenHandler(); - var tokenDescriptor = new SecurityTokenDescriptor - { - Subject = mainIdentity, - Issuer = "https://example.com", - Audience = "https://api.example.com", - Expires = DateTime.UtcNow.AddHours(1), - SigningCredentials = Default.AsymmetricSigningCredentials, - Claims = new Dictionary { { "act", actor } } - }; - var token = handler.CreateToken(tokenDescriptor); - - var validationParameters = new TokenValidationParameters - { - ValidateIssuer = false, - ValidateAudience = false, - ValidateLifetime = false, - IssuerSigningKey = Default.AsymmetricSigningKey, - ValidateIssuerSigningKey = true, - ActorClaimType = "act", - MaxActorChainLength = 3, - ActClaimRetrieverDelegate = CustomDelegate - }; - - var result = await handler.ValidateTokenAsync(token, validationParameters); - Assert.True(result.IsValid); - Assert.NotNull(result.ClaimsIdentity.Actor); - Assert.Equal("actor-subject-id", result.ClaimsIdentity.Actor.Claims.First(c => c.Type == "sub").Value); - Assert.NotNull(result.ClaimsIdentity.Actor.Actor); - Assert.Equal("nested-actor-id", result.ClaimsIdentity.Actor.Actor.Claims.First(c => c.Type == "sub").Value); - Assert.True(delegateCallCount >= 2); - - TestUtilities.AssertFailIfErrors(context); - } - finally - { - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); - } + var nestedActor = new CaseSensitiveClaimsIdentity("NestedActorAuth"); + nestedActor.AddClaim(new Claim("sub", "nested-actor-id")); + nestedActor.AddClaim(new Claim("name", "Nested Actor")); + + var actor = new CaseSensitiveClaimsIdentity("ActorAuth"); + actor.AddClaim(new Claim("sub", "actor-subject-id")); + actor.AddClaim(new Claim("name", "Actor Name")); + actor.Actor = nestedActor; + + var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); + mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); + mainIdentity.AddClaim(new Claim("name", "Main User")); + + var handler = new JsonWebTokenHandler(); + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = mainIdentity, + Issuer = "https://example.com", + Audience = "https://api.example.com", + Expires = DateTime.UtcNow.AddHours(1), + SigningCredentials = Default.AsymmetricSigningCredentials, + Claims = new Dictionary { { "act", actor } }, + ActClaimSupportEnabled = true, + }; + var token = handler.CreateToken(tokenDescriptor); + + var validationParameters = new TokenValidationParameters + { + ValidateIssuer = false, + ValidateAudience = false, + ValidateLifetime = false, + IssuerSigningKey = Default.AsymmetricSigningKey, + ValidateIssuerSigningKey = true, + ActorClaimType = "act", + MaxActorChainLength = 3, + ActClaimRetrieverDelegate = CustomDelegate, + ActClaimSupportEnabled = true, + }; + + var result = await handler.ValidateTokenAsync(token, validationParameters); + Assert.True(result.IsValid); + Assert.NotNull(result.ClaimsIdentity.Actor); + Assert.Equal("actor-subject-id", result.ClaimsIdentity.Actor.Claims.First(c => c.Type == "sub").Value); + Assert.NotNull(result.ClaimsIdentity.Actor.Actor); + Assert.Equal("nested-actor-id", result.ClaimsIdentity.Actor.Actor.Claims.First(c => c.Type == "sub").Value); + Assert.True(delegateCallCount >= 2); + + TestUtilities.AssertFailIfErrors(context); } - [ResetAppContextSwitches] [Fact] public async Task ValidateTokenAsync_NestedActors_DefaultDelegate_CreatesProperClaimsIdentity() { var context = new CompareContext($"{this}.ValidateTokenAsync_NestedActors_DefaultDelegate_CreatesProperClaimsIdentity"); - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); - - try - { - var nestedActor = new CaseSensitiveClaimsIdentity("NestedActorAuth"); - nestedActor.AddClaim(new Claim("sub", "nested-actor-id")); - nestedActor.AddClaim(new Claim("name", "Nested Actor")); - - var actor = new CaseSensitiveClaimsIdentity("ActorAuth"); - actor.AddClaim(new Claim("sub", "actor-subject-id")); - actor.AddClaim(new Claim("name", "Actor Name")); - actor.Actor = nestedActor; - - var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); - mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); - mainIdentity.AddClaim(new Claim("name", "Main User")); - - var handler = new JsonWebTokenHandler(); - var tokenDescriptor = new SecurityTokenDescriptor - { - Subject = mainIdentity, - Issuer = "https://example.com", - Audience = "https://api.example.com", - Expires = DateTime.UtcNow.AddHours(1), - SigningCredentials = Default.AsymmetricSigningCredentials, - Claims = new Dictionary { { "act", actor } } - }; - var token = handler.CreateToken(tokenDescriptor); - - var validationParameters = new TokenValidationParameters - { - ValidateIssuer = false, - ValidateAudience = false, - ValidateLifetime = false, - IssuerSigningKey = Default.AsymmetricSigningKey, - ValidateIssuerSigningKey = true, - ActorClaimType = "act", - MaxActorChainLength = 2 - }; - - var result = await handler.ValidateTokenAsync(token, validationParameters); - Assert.True(result.IsValid); - Assert.NotNull(result.ClaimsIdentity.Actor); - Assert.Equal("actor-subject-id", result.ClaimsIdentity.Actor.Claims.First(c => c.Type == "sub").Value); - Assert.NotNull(result.ClaimsIdentity.Actor.Actor); - Assert.Equal("nested-actor-id", result.ClaimsIdentity.Actor.Actor.Claims.First(c => c.Type == "sub").Value); - - TestUtilities.AssertFailIfErrors(context); - } - finally - { - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); - } + var nestedActor = new CaseSensitiveClaimsIdentity("NestedActorAuth"); + nestedActor.AddClaim(new Claim("sub", "nested-actor-id")); + nestedActor.AddClaim(new Claim("name", "Nested Actor")); + + var actor = new CaseSensitiveClaimsIdentity("ActorAuth"); + actor.AddClaim(new Claim("sub", "actor-subject-id")); + actor.AddClaim(new Claim("name", "Actor Name")); + actor.Actor = nestedActor; + + var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); + mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); + mainIdentity.AddClaim(new Claim("name", "Main User")); + + var handler = new JsonWebTokenHandler(); + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = mainIdentity, + Issuer = "https://example.com", + Audience = "https://api.example.com", + Expires = DateTime.UtcNow.AddHours(1), + SigningCredentials = Default.AsymmetricSigningCredentials, + Claims = new Dictionary { { "act", actor } }, + ActClaimSupportEnabled = true, + }; + var token = handler.CreateToken(tokenDescriptor); + + var validationParameters = new TokenValidationParameters + { + ValidateIssuer = false, + ValidateAudience = false, + ValidateLifetime = false, + IssuerSigningKey = Default.AsymmetricSigningKey, + ValidateIssuerSigningKey = true, + ActorClaimType = "act", + MaxActorChainLength = 2, + ActClaimSupportEnabled = true, + }; + + var result = await handler.ValidateTokenAsync(token, validationParameters); + Assert.True(result.IsValid); + Assert.NotNull(result.ClaimsIdentity.Actor); + Assert.Equal("actor-subject-id", result.ClaimsIdentity.Actor.Claims.First(c => c.Type == "sub").Value); + Assert.NotNull(result.ClaimsIdentity.Actor.Actor); + Assert.Equal("nested-actor-id", result.ClaimsIdentity.Actor.Actor.Claims.First(c => c.Type == "sub").Value); + + TestUtilities.AssertFailIfErrors(context); } - [ResetAppContextSwitches] [Fact] public async Task ValidateTokenAsync_NestingBeyondMaxActorChain_ThrowsException() { var context = new CompareContext($"{this}.ValidateTokenAsync_NestingBeyondMaxActorChain_ThrowsException"); - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); try { @@ -804,7 +709,8 @@ public async Task ValidateTokenAsync_NestingBeyondMaxActorChain_ThrowsException( Audience = "https://api.example.com", Expires = DateTime.UtcNow.AddHours(1), SigningCredentials = Default.AsymmetricSigningCredentials, - Claims = new Dictionary { { "act", level1Actor } } + Claims = new Dictionary { { "act", level1Actor } }, + ActClaimSupportEnabled = true, }; var token = handler.CreateToken(tokenDescriptor); @@ -816,7 +722,8 @@ public async Task ValidateTokenAsync_NestingBeyondMaxActorChain_ThrowsException( IssuerSigningKey = Default.AsymmetricSigningKey, ValidateIssuerSigningKey = true, ActorClaimType = "act", - MaxActorChainLength = 2 + MaxActorChainLength = 2, + ActClaimSupportEnabled = true, }; handler.MapInboundClaims = true; var result = await handler.ValidateTokenAsync(token, validationParameters); @@ -827,18 +734,12 @@ public async Task ValidateTokenAsync_NestingBeyondMaxActorChain_ThrowsException( { Assert.Contains("IDX14313", ex.ToString()); } - finally - { - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); - } } - [ResetAppContextSwitches] [Fact] public async Task ValidateTokenAsync_CustomDelegate_ThrowsExceptionIfDelegateFails() { var context = new CompareContext($"{this}.ValidateTokenAsync_CustomDelegate_ThrowsIfDelegateFails"); - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); ClaimsIdentity CustomDelegate(JsonElement element, TokenValidationParameters tokenValidationParameters = null) { @@ -861,7 +762,8 @@ ClaimsIdentity CustomDelegate(JsonElement element, TokenValidationParameters tok Audience = "https://api.example.com", Expires = DateTime.UtcNow.AddHours(1), SigningCredentials = Default.AsymmetricSigningCredentials, - Claims = new Dictionary { { "act", actor } } + Claims = new Dictionary { { "act", actor } }, + ActClaimSupportEnabled = true, }; var token = handler.CreateToken(tokenDescriptor); @@ -874,7 +776,8 @@ ClaimsIdentity CustomDelegate(JsonElement element, TokenValidationParameters tok ValidateIssuerSigningKey = true, ActorClaimType = "act", MaxActorChainLength = 2, - ActClaimRetrieverDelegate = CustomDelegate + ActClaimRetrieverDelegate = CustomDelegate, + ActClaimSupportEnabled = true, }; var result = await handler.ValidateTokenAsync(token, validationParameters); @@ -885,18 +788,12 @@ ClaimsIdentity CustomDelegate(JsonElement element, TokenValidationParameters tok { Assert.Contains("IDX14314", ex.ToString()); } - finally - { - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); - } } - [ResetAppContextSwitches] [Fact] public async Task ValidateTokenAsync_ActorAsSubjectAndClaimsDictionary_DefaultAndCustomDelegate() { var context = new CompareContext($"{this}.ValidateTokenAsync_ActorAsSubjectAndClaimsDictionary_DefaultAndCustomDelegate"); - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); ClaimsIdentity CustomDelegate(JsonElement element, TokenValidationParameters tokenValidationParameters = null) { @@ -906,84 +803,79 @@ ClaimsIdentity CustomDelegate(JsonElement element, TokenValidationParameters tok return id; } - try - { - // Actor as Subject - var actorAsSubject = new CaseSensitiveClaimsIdentity("ActorAuth"); - actorAsSubject.AddClaim(new Claim("sub", "actor-subject-id")); - - var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); - mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); - mainIdentity.Actor = actorAsSubject; - - var handler = new JsonWebTokenHandler(); - var tokenDescriptor = new SecurityTokenDescriptor - { - Subject = mainIdentity, - Issuer = "https://example.com", - Audience = "https://api.example.com", - Expires = DateTime.UtcNow.AddHours(1), - SigningCredentials = Default.AsymmetricSigningCredentials - }; - var token = handler.CreateToken(tokenDescriptor); - - var validationParameters = new TokenValidationParameters - { - ValidateIssuer = false, - ValidateAudience = false, - ValidateLifetime = false, - IssuerSigningKey = Default.AsymmetricSigningKey, - ValidateIssuerSigningKey = true, - ActorClaimType = "act" - }; - - // Default delegate - var result = await handler.ValidateTokenAsync(token, validationParameters); - Assert.True(result.IsValid); - Assert.NotNull(result.ClaimsIdentity.Actor); - Assert.Equal("actor-subject-id", result.ClaimsIdentity.Actor.Claims.First(c => c.Type == "sub").Value); - - // Custom delegate - validationParameters.ActClaimRetrieverDelegate = CustomDelegate; - var result2 = await handler.ValidateTokenAsync(token, validationParameters); - Assert.True(result2.IsValid); - Assert.NotNull(result2.ClaimsIdentity.Actor); - Assert.Equal("actor-subject-id", result2.ClaimsIdentity.Actor.Claims.First(c => c.Type == "sub").Value); - - // Actor in both Subject and Claims dictionary, Claims dictionary should take precedence - var subjectActor = new CaseSensitiveClaimsIdentity("SubjectActorAuth"); - subjectActor.AddClaim(new Claim("sub", "subject-actor-id")); - - var claimsActor = new CaseSensitiveClaimsIdentity("ClaimsActorAuth"); - claimsActor.AddClaim(new Claim("sub", "claims-actor-id")); - - var mainIdentity2 = new CaseSensitiveClaimsIdentity("Bearer"); - mainIdentity2.AddClaim(new Claim("sub", "main-subject-id")); - mainIdentity2.Actor = subjectActor; - - var tokenDescriptor2 = new SecurityTokenDescriptor - { - Subject = mainIdentity2, - Issuer = "https://example.com", - Audience = "https://api.example.com", - Expires = DateTime.UtcNow.AddHours(1), - SigningCredentials = Default.AsymmetricSigningCredentials, - Claims = new Dictionary { { "act", claimsActor } } - }; - var token2 = handler.CreateToken(tokenDescriptor2); - - var result3 = await handler.ValidateTokenAsync(token2, validationParameters); - Assert.True(result3.IsValid); - Assert.NotNull(result3.ClaimsIdentity.Actor); - Assert.Equal("claims-actor-id", result3.ClaimsIdentity.Actor.Claims.First(c => c.Type == "sub").Value); - - TestUtilities.AssertFailIfErrors(context); - } - finally - { - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); - } + // Actor as Subject + var actorAsSubject = new CaseSensitiveClaimsIdentity("ActorAuth"); + actorAsSubject.AddClaim(new Claim("sub", "actor-subject-id")); + + var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); + mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); + mainIdentity.Actor = actorAsSubject; + + var handler = new JsonWebTokenHandler(); + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = mainIdentity, + Issuer = "https://example.com", + Audience = "https://api.example.com", + Expires = DateTime.UtcNow.AddHours(1), + SigningCredentials = Default.AsymmetricSigningCredentials, + ActClaimSupportEnabled = true, + }; + var token = handler.CreateToken(tokenDescriptor); + + var validationParameters = new TokenValidationParameters + { + ValidateIssuer = false, + ValidateAudience = false, + ValidateLifetime = false, + IssuerSigningKey = Default.AsymmetricSigningKey, + ValidateIssuerSigningKey = true, + ActorClaimType = "act", + ActClaimSupportEnabled = true, + }; + + // Default delegate + var result = await handler.ValidateTokenAsync(token, validationParameters); + Assert.True(result.IsValid); + Assert.NotNull(result.ClaimsIdentity.Actor); + Assert.Equal("actor-subject-id", result.ClaimsIdentity.Actor.Claims.First(c => c.Type == "sub").Value); + + // Custom delegate + validationParameters.ActClaimRetrieverDelegate = CustomDelegate; + var result2 = await handler.ValidateTokenAsync(token, validationParameters); + Assert.True(result2.IsValid); + Assert.NotNull(result2.ClaimsIdentity.Actor); + Assert.Equal("actor-subject-id", result2.ClaimsIdentity.Actor.Claims.First(c => c.Type == "sub").Value); + + // Actor in both Subject and Claims dictionary, Claims dictionary should take precedence + var subjectActor = new CaseSensitiveClaimsIdentity("SubjectActorAuth"); + subjectActor.AddClaim(new Claim("sub", "subject-actor-id")); + + var claimsActor = new CaseSensitiveClaimsIdentity("ClaimsActorAuth"); + claimsActor.AddClaim(new Claim("sub", "claims-actor-id")); + + var mainIdentity2 = new CaseSensitiveClaimsIdentity("Bearer"); + mainIdentity2.AddClaim(new Claim("sub", "main-subject-id")); + mainIdentity2.Actor = subjectActor; + + var tokenDescriptor2 = new SecurityTokenDescriptor + { + Subject = mainIdentity2, + Issuer = "https://example.com", + Audience = "https://api.example.com", + Expires = DateTime.UtcNow.AddHours(1), + SigningCredentials = Default.AsymmetricSigningCredentials, + Claims = new Dictionary { { "act", claimsActor } }, + ActClaimSupportEnabled = true, + }; + var token2 = handler.CreateToken(tokenDescriptor2); + + var result3 = await handler.ValidateTokenAsync(token2, validationParameters); + Assert.True(result3.IsValid); + Assert.NotNull(result3.ClaimsIdentity.Actor); + Assert.Equal("claims-actor-id", result3.ClaimsIdentity.Actor.Claims.First(c => c.Type == "sub").Value); + + TestUtilities.AssertFailIfErrors(context); } - } } diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActClaimTests/ActClaimSerializationTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActClaimTests/ActClaimSerializationTests.cs index 6adfdb6a4a..167da46710 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActClaimTests/ActClaimSerializationTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActClaimTests/ActClaimSerializationTests.cs @@ -12,14 +12,10 @@ namespace Microsoft.IdentityModel.JsonWebTokens.Tests.ActClaimTests { public class ActClaimSerializationTests { - [ResetAppContextSwitches] [Fact] public void ActorTokenInClaimsDictionaryShouldBeProperlySerialized() { var context = new CompareContext($"{this}.ActorTokenInClaimsDictionaryShouldBeProperlySerialized"); - bool switchValue = false; - AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); string actorname = "act"; try { @@ -46,7 +42,8 @@ public void ActorTokenInClaimsDictionaryShouldBeProperlySerialized() Claims = new Dictionary { { actorname, actorIdentity } - } + }, + ActClaimSupportEnabled = true, }; var token = tokenHandler.CreateToken(tokenDescriptor); JsonWebToken decodedToken = tokenHandler.ReadJsonWebToken(token); @@ -67,20 +64,12 @@ public void ActorTokenInClaimsDictionaryShouldBeProperlySerialized() { context.Diffs.Add($"Exception: {ex}"); } - finally - { - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); - } } - [ResetAppContextSwitches] [Fact] public void ActorTokenAsSubjectShouldBeProperlySerialized() { var context = new CompareContext($"{this}.ActorTokenAsSubjectShouldBeProperlySerialized"); - bool switchValue = false; - AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); try { // Create actor identity @@ -103,7 +92,8 @@ public void ActorTokenAsSubjectShouldBeProperlySerialized() Issuer = "https://example.com", Audience = "https://api.example.com", Expires = DateTime.UtcNow.AddHours(1), - SigningCredentials = Default.AsymmetricSigningCredentials + SigningCredentials = Default.AsymmetricSigningCredentials, + ActClaimSupportEnabled = true, }; var token = tokenHandler.CreateToken(tokenDescriptor); JsonWebToken decodedToken = tokenHandler.ReadJsonWebToken(token); @@ -128,21 +118,14 @@ public void ActorTokenAsSubjectShouldBeProperlySerialized() { context.Diffs.Add($"Exception: {ex}"); } - finally - { - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); - } } - [ResetAppContextSwitches] [Fact] public void ActorTokenInBothClaimsAndSubjectShouldPreferClaimsValue() { var context = new CompareContext($"{this}.ActorTokenInBothClaimsAndSubjectShouldPreferClaimsValue"); - bool switchValue = false; string actorname = "act"; - AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); + try { // Create actor identity for Subject.Actor (should be ignored) @@ -174,7 +157,8 @@ public void ActorTokenInBothClaimsAndSubjectShouldPreferClaimsValue() Claims = new Dictionary { { actorname, claimsActorIdentity } - } + }, + ActClaimSupportEnabled = true, }; var token = tokenHandler.CreateToken(tokenDescriptor); JsonWebToken decodedToken = tokenHandler.ReadJsonWebToken(token); @@ -196,20 +180,13 @@ public void ActorTokenInBothClaimsAndSubjectShouldPreferClaimsValue() { context.Diffs.Add($"Exception: {ex}"); } - finally - { - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); - } } - [ResetAppContextSwitches] [Fact] public void NestedActorTokenInClaimsDictionaryShouldBeProperlySerialized() { var context = new CompareContext($"{this}.NestedActorTokenInClaimsDictionaryShouldBeProperlySerialized"); - bool switchValue = false; - AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); + try { // Create nested actor identity @@ -240,7 +217,8 @@ public void NestedActorTokenInClaimsDictionaryShouldBeProperlySerialized() Claims = new Dictionary { { "act", actorIdentity } - } + }, + ActClaimSupportEnabled = true, }; var token = tokenHandler.CreateToken(tokenDescriptor); JsonWebToken decodedToken = tokenHandler.ReadJsonWebToken(token); @@ -269,134 +247,108 @@ public void NestedActorTokenInClaimsDictionaryShouldBeProperlySerialized() { context.Diffs.Add($"Exception: {ex}"); } - finally - { - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); - } } - [ResetAppContextSwitches] + [Fact] public void NestedActorTokenAsSubjectShouldBeProperlySerialized() { var context = new CompareContext($"{this}.NestedActorTokenAsSubjectShouldBeProperlySerialized"); - bool switchValue = false; - AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); - try - { - // Create nested actor - var nestedActorIdentity = new CaseSensitiveClaimsIdentity("NestedActorAuth"); - nestedActorIdentity.AddClaim(new Claim("sub", "nested-actor-id")); - nestedActorIdentity.AddClaim(new Claim("name", "Nested Actor")); - - // Create actor identity with nested actor - var actorIdentity = new CaseSensitiveClaimsIdentity("ActorAuth"); - actorIdentity.AddClaim(new Claim("sub", "actor-subject-id")); - actorIdentity.AddClaim(new Claim("name", "Actor Name")); - actorIdentity.Actor = nestedActorIdentity; - - // Create the main identity with Actor set - var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); - mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); - mainIdentity.AddClaim(new Claim("name", "Main User")); - mainIdentity.Actor = actorIdentity; - // Create a token with JsonWebTokenHandler - var tokenHandler = new JsonWebTokenHandler(); - var tokenDescriptor = new SecurityTokenDescriptor - { - Subject = mainIdentity, - Issuer = "https://example.com", - Audience = "https://api.example.com", - Expires = DateTime.UtcNow.AddHours(1), - SigningCredentials = Default.AsymmetricSigningCredentials, - ActorClaimType = "act", - }; - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); - var token = tokenHandler.CreateToken(tokenDescriptor); - JsonWebToken decodedToken = tokenHandler.ReadJsonWebToken(token); + // Create nested actor + var nestedActorIdentity = new CaseSensitiveClaimsIdentity("NestedActorAuth"); + nestedActorIdentity.AddClaim(new Claim("sub", "nested-actor-id")); + nestedActorIdentity.AddClaim(new Claim("name", "Nested Actor")); + + // Create actor identity with nested actor + var actorIdentity = new CaseSensitiveClaimsIdentity("ActorAuth"); + actorIdentity.AddClaim(new Claim("sub", "actor-subject-id")); + actorIdentity.AddClaim(new Claim("name", "Actor Name")); + actorIdentity.Actor = nestedActorIdentity; + + // Create the main identity with Actor set + var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); + mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); + mainIdentity.AddClaim(new Claim("name", "Main User")); + mainIdentity.Actor = actorIdentity; + + // Create a token with JsonWebTokenHandler + var tokenHandler = new JsonWebTokenHandler(); + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = mainIdentity, + Issuer = "https://example.com", + Audience = "https://api.example.com", + Expires = DateTime.UtcNow.AddHours(1), + SigningCredentials = Default.AsymmetricSigningCredentials, + ActorClaimType = "act", + ActClaimSupportEnabled = true, + }; + var token = tokenHandler.CreateToken(tokenDescriptor); + JsonWebToken decodedToken = tokenHandler.ReadJsonWebToken(token); - // Verify actor claim exists - Assert.True(decodedToken.Payload.HasClaim("act"), "JWT token should contain 'act' claim"); + // Verify actor claim exists + Assert.True(decodedToken.Payload.HasClaim("act"), "JWT token should contain 'act' claim"); - // Verify the actor object structure - var actorObject = decodedToken.Payload.GetValue("act"); - Console.WriteLine("actor token created: " + actorObject.ToString()); + // Verify the actor object structure + var actorObject = decodedToken.Payload.GetValue("act"); + Console.WriteLine("actor token created: " + actorObject.ToString()); - Assert.Equal(JsonValueKind.Object, actorObject.ValueKind); + Assert.Equal(JsonValueKind.Object, actorObject.ValueKind); - // Verify main actor claims directly from JSON object - Assert.Equal("actor-subject-id", actorObject.GetProperty("sub").GetString()); - Assert.Equal("Actor Name", actorObject.GetProperty("name").GetString()); + // Verify main actor claims directly from JSON object + Assert.Equal("actor-subject-id", actorObject.GetProperty("sub").GetString()); + Assert.Equal("Actor Name", actorObject.GetProperty("name").GetString()); - // Verify nested actor exists and is a JSON object - Assert.True(actorObject.TryGetProperty(tokenDescriptor.ActorClaimType, out var nestedActorElement)); - Assert.Equal(JsonValueKind.Object, nestedActorElement.ValueKind); - Console.WriteLine("nested token created: " + nestedActorElement.ToString()); + // Verify nested actor exists and is a JSON object + Assert.True(actorObject.TryGetProperty(tokenDescriptor.ActorClaimType, out var nestedActorElement)); + Assert.Equal(JsonValueKind.Object, nestedActorElement.ValueKind); + Console.WriteLine("nested token created: " + nestedActorElement.ToString()); - // Verify nested actor claims directly from JSON object - Assert.Equal("nested-actor-id", nestedActorElement.GetProperty("sub").GetString()); - Assert.Equal("Nested Actor", nestedActorElement.GetProperty("name").GetString()); - TestUtilities.AssertFailIfErrors(context); - } - finally - { - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); - } + // Verify nested actor claims directly from JSON object + Assert.Equal("nested-actor-id", nestedActorElement.GetProperty("sub").GetString()); + Assert.Equal("Nested Actor", nestedActorElement.GetProperty("name").GetString()); + TestUtilities.AssertFailIfErrors(context); } - [ResetAppContextSwitches] + [Fact] public void MaxActorChainLength_RejectsNegativeValues() { - bool switchValue = false; - AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); - // Arrange SecurityTokenDescriptor tokenDescriptor = new SecurityTokenDescriptor { Subject = null, Issuer = "https://example.com", Audience = "https://api.example.com", - SigningCredentials = Default.AsymmetricSigningCredentials + SigningCredentials = Default.AsymmetricSigningCredentials, + ActClaimSupportEnabled = true, }; - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); + tokenDescriptor.ActorClaimType = "act"; // Set the actor claim name to "act" for testing int originalValue = tokenDescriptor.MaxActorChainLength; - try - { - tokenDescriptor.ActorClaimType = "act"; // Set the actor claim name to "act" for testing - // Act & Assert - Valid value 0 should not throw - tokenDescriptor.MaxActorChainLength = 0; - Assert.Equal(0, tokenDescriptor.MaxActorChainLength); - - // Act & Assert - Negative value - var ex = Assert.Throws(() => - tokenDescriptor.MaxActorChainLength = -5); - Assert.Contains("IDX11027", ex.Message); - - // Act & Assert - Valid value 1 should not throw - tokenDescriptor.MaxActorChainLength = 1; - Assert.Equal(1, tokenDescriptor.MaxActorChainLength); - - ex = Assert.Throws(() => - tokenDescriptor.MaxActorChainLength = 10); - Assert.Contains("IDX11027", ex.Message); - } - finally - { - // Restore to original value - tokenDescriptor.MaxActorChainLength = originalValue; - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); - } + tokenDescriptor.ActorClaimType = "act"; // Set the actor claim name to "act" for testing + // Act & Assert - Valid value 0 should not throw + tokenDescriptor.MaxActorChainLength = 0; + Assert.Equal(0, tokenDescriptor.MaxActorChainLength); + + // Act & Assert - Negative value + var ex = Assert.Throws(() => + tokenDescriptor.MaxActorChainLength = -5); + Assert.Contains("IDX11027", ex.Message); + + // Act & Assert - Valid value 1 should not throw + tokenDescriptor.MaxActorChainLength = 1; + Assert.Equal(1, tokenDescriptor.MaxActorChainLength); + + ex = Assert.Throws(() => + tokenDescriptor.MaxActorChainLength = 10); + Assert.Contains("IDX11027", ex.Message); } - [ResetAppContextSwitches] [Fact] public void NestedSubjectActorTokens_ExceedingMaxDepth_ThrowsException() { var context = new CompareContext($"{this}.NestedActorTokens_ExceedingMaxDepth_ThrowsException"); - bool switchValue = false; - AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); + try { // Arrange @@ -431,7 +383,8 @@ public void NestedSubjectActorTokens_ExceedingMaxDepth_ThrowsException() Audience = "https://api.example.com", SigningCredentials = Default.AsymmetricSigningCredentials, ActorClaimType = "act", - MaxActorChainLength = 2 + MaxActorChainLength = 2, + ActClaimSupportEnabled = true, }; // Act - This should throw a SecurityTokenException @@ -453,20 +406,13 @@ public void NestedSubjectActorTokens_ExceedingMaxDepth_ThrowsException() // Unexpected exception type context.Diffs.Add($"Unexpected exception type: {ex.GetType()}, Message: {ex.Message}"); } - finally - { - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); - } - } [Fact] public void NestedClaimsDictionaryActorTokens_ExceedingMaxDepth_ThrowsException() { var context = new CompareContext($"{this}.NestedClaimsDictionaryActorTokens_ExceedingMaxDepth_ThrowsException"); - bool switchValue = false; - AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); + try { // Arrange @@ -521,87 +467,74 @@ public void NestedClaimsDictionaryActorTokens_ExceedingMaxDepth_ThrowsException( // Unexpected exception type context.Diffs.Add($"Unexpected exception type: {ex.GetType()}, Message: {ex.Message}"); } - finally - { - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); - } } - [ResetAppContextSwitches] + [Fact] public void ActorTokens_MixedSourceRespectMaxActorChainLength() { - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); - try + // Arrange + var handler = new JsonWebTokenHandler(); + string actorname = "act"; + // Create level 2 actor (will be in claims dictionary) + var level2Actor = new CaseSensitiveClaimsIdentity("Level2Auth"); + level2Actor.AddClaim(new Claim("sub", "level2-actor")); + level2Actor.AddClaim(new Claim("name", "Level 2 Actor")); + + // Create nested actors that should be truncated + var level3Actor = new CaseSensitiveClaimsIdentity("Level3Auth"); + level3Actor.AddClaim(new Claim("sub", "level3-actor")); + level3Actor.AddClaim(new Claim("name", "Level 3 Actor")); + + // Create level 1 actor with nested actor + var level1Actor = new CaseSensitiveClaimsIdentity("Level1Auth"); + level1Actor.AddClaim(new Claim("sub", "level1-actor")); + level1Actor.AddClaim(new Claim("name", "Level 1 Actor")); + level1Actor.Actor = level3Actor; // This should be ignored due to MaxActorChainLength + + // Create the main identity + var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); + mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); + mainIdentity.AddClaim(new Claim("name", "Main User")); + mainIdentity.Actor = level1Actor; + + // Create a token with additional actor in Claims dictionary + var tokenDescriptor = new SecurityTokenDescriptor { - // Arrange - var handler = new JsonWebTokenHandler(); - string actorname = "act"; - // Create level 2 actor (will be in claims dictionary) - var level2Actor = new CaseSensitiveClaimsIdentity("Level2Auth"); - level2Actor.AddClaim(new Claim("sub", "level2-actor")); - level2Actor.AddClaim(new Claim("name", "Level 2 Actor")); - - // Create nested actors that should be truncated - var level3Actor = new CaseSensitiveClaimsIdentity("Level3Auth"); - level3Actor.AddClaim(new Claim("sub", "level3-actor")); - level3Actor.AddClaim(new Claim("name", "Level 3 Actor")); - - // Create level 1 actor with nested actor - var level1Actor = new CaseSensitiveClaimsIdentity("Level1Auth"); - level1Actor.AddClaim(new Claim("sub", "level1-actor")); - level1Actor.AddClaim(new Claim("name", "Level 1 Actor")); - level1Actor.Actor = level3Actor; // This should be ignored due to MaxActorChainLength - - // Create the main identity - var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); - mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); - mainIdentity.AddClaim(new Claim("name", "Main User")); - mainIdentity.Actor = level1Actor; - - // Create a token with additional actor in Claims dictionary - var tokenDescriptor = new SecurityTokenDescriptor + Subject = mainIdentity, + Issuer = "https://example.com", + Audience = "https://api.example.com", + SigningCredentials = Default.AsymmetricSigningCredentials, + // Add level 2 actor in claims dictionary to replace level 1's actor + Claims = new Dictionary { - Subject = mainIdentity, - Issuer = "https://example.com", - Audience = "https://api.example.com", - SigningCredentials = Default.AsymmetricSigningCredentials, - // Add level 2 actor in claims dictionary to replace level 1's actor - Claims = new Dictionary - { - { actorname, level2Actor } - }, - ActorClaimType = actorname, - MaxActorChainLength = 1 - }; + { actorname, level2Actor } + }, + ActorClaimType = actorname, + MaxActorChainLength = 1, + ActClaimSupportEnabled = true, + }; - var token = handler.CreateToken(tokenDescriptor); - var jwtToken = handler.ReadJsonWebToken(token); + var token = handler.CreateToken(tokenDescriptor); + var jwtToken = handler.ReadJsonWebToken(token); - // Assert - Check actor object structure - Assert.True(jwtToken.Payload.HasClaim(actorname), "JWT token should contain 'act' claim"); - var actorObject = jwtToken.Payload.GetValue(tokenDescriptor.ActorClaimType); + // Assert - Check actor object structure + Assert.True(jwtToken.Payload.HasClaim(actorname), "JWT token should contain 'act' claim"); + var actorObject = jwtToken.Payload.GetValue(tokenDescriptor.ActorClaimType); - Assert.Equal(JsonValueKind.Object, actorObject.ValueKind); + Assert.Equal(JsonValueKind.Object, actorObject.ValueKind); - // Verify we get the actor from Claims dictionary (should be level2Actor) - Assert.Equal("level2-actor", actorObject.GetProperty("sub").GetString()); - Assert.Equal("Level 2 Actor", actorObject.GetProperty("name").GetString()); + // Verify we get the actor from Claims dictionary (should be level2Actor) + Assert.Equal("level2-actor", actorObject.GetProperty("sub").GetString()); + Assert.Equal("Level 2 Actor", actorObject.GetProperty("name").GetString()); - // There should be no nested actor because we're already at max depth - Assert.False(actorObject.TryGetProperty("act", out _), "There should be no nested actor claim due to MaxActorChainLength"); - } - finally - { - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); - } + // There should be no nested actor because we're already at max depth + Assert.False(actorObject.TryGetProperty("act", out _), "There should be no nested actor claim due to MaxActorChainLength"); } - [ResetAppContextSwitches] + [Fact] public void NestedClaimTokens_ExceedingMaxDepth_ThrowsException() { var context = new CompareContext($"{this}.NestedActorTokens_ExceedingMaxDepth_ThrowsException"); - bool switchValue = false; - AppContext.TryGetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, out switchValue); var actorname = "act"; try { @@ -639,9 +572,9 @@ public void NestedClaimTokens_ExceedingMaxDepth_ThrowsException() { "act", level1Actor } }, ActorClaimType = actorname, - MaxActorChainLength = 1 + MaxActorChainLength = 1, + ActClaimSupportEnabled = true, }; - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true); // Act - This should throw a SecurityTokenException var token = handler.CreateToken(tokenDescriptor); @@ -661,10 +594,6 @@ public void NestedClaimTokens_ExceedingMaxDepth_ThrowsException() // Unexpected exception type context.Diffs.Add($"Unexpected exception type: {ex.GetType()}, Message: {ex.Message}"); } - finally - { - AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, false); - } } } } diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/TokenValidationParametersTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/TokenValidationParametersTests.cs index b0ff1c1758..beb573b913 100644 --- a/test/Microsoft.IdentityModel.Tokens.Tests/TokenValidationParametersTests.cs +++ b/test/Microsoft.IdentityModel.Tokens.Tests/TokenValidationParametersTests.cs @@ -15,7 +15,7 @@ namespace Microsoft.IdentityModel.Tokens.Tests { public class TokenValidationParametersTests { - int ExpectedPropertyCount = 66; + int ExpectedPropertyCount = 67; // GetSets() compares the total property count which includes internal properties, against a list of public properties, minus delegates. // This allows us to keep track of any properties we are including in the total that are not public nor delegates. @@ -198,6 +198,7 @@ public void GetSets() { PropertyNamesAndSetGetValue = new List>> { + new KeyValuePair>("ActClaimSupportEnabled", new List{false}), new KeyValuePair>("ActorClaimType", new List{"actort"}), new KeyValuePair>("ActorChainDepth", new List{0,1}), new KeyValuePair>("ActorValidationParameters", new List{(TokenValidationParameters)null, new TokenValidationParameters(), new TokenValidationParameters()}), From e3e47ee6914fe0a8e60b80933e4891951dbb361c Mon Sep 17 00:00:00 2001 From: saurabhsathe-ms Date: Wed, 4 Jun 2025 13:05:08 -0700 Subject: [PATCH 47/52] NIT updates --- .../JsonWebTokenHandler.CreateToken.cs | 1 + .../JsonWebTokenHandler.cs | 1 - src/Microsoft.IdentityModel.Tokens/AppContextSwitches.cs | 1 - .../ActClaimTests/ActClaimSerializationTests.cs | 3 --- 4 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs index ea06fcaf94..2b37197f1a 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs @@ -668,6 +668,7 @@ internal static void WriteJwsPayload( // Duplicates are resolved according to the following priority: // SecurityTokenDescriptor.{Audience/Audiences, Issuer, Expires, IssuedAt, NotBefore}, SecurityTokenDescriptor.Claims, SecurityTokenDescriptor.Subject.Claims // SecurityTokenDescriptor.Claims are KeyValuePairs, whereas SecurityTokenDescriptor.Subject.Claims are System.Security.Claims.Claim and are processed differently. + if (tokenDescriptor.Claims != null && tokenDescriptor.Claims.Count > 0) { foreach (KeyValuePair kvp in tokenDescriptor.Claims) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs index 8f576c9cf8..b24dae0b62 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs @@ -216,7 +216,6 @@ private ClaimsIdentity CreateClaimsIdentityWithMapping(JsonWebToken jwtToken, To _ = validationParameters ?? throw LogHelper.LogArgumentNullException(nameof(validationParameters)); ClaimsIdentity identity = validationParameters.CreateClaimsIdentity(jwtToken, issuer); - Console.WriteLine($"*********************************Here in this --- {validationParameters.ActorClaimType}?"); foreach (Claim jwtClaim in jwtToken.Claims) { bool wasMapped = _inboundClaimTypeMap.TryGetValue(jwtClaim.Type, out string claimType); diff --git a/src/Microsoft.IdentityModel.Tokens/AppContextSwitches.cs b/src/Microsoft.IdentityModel.Tokens/AppContextSwitches.cs index bfc98f1c4e..7f32a2fb85 100644 --- a/src/Microsoft.IdentityModel.Tokens/AppContextSwitches.cs +++ b/src/Microsoft.IdentityModel.Tokens/AppContextSwitches.cs @@ -123,7 +123,6 @@ internal static void ResetAllSwitches() _useCapitalizedXMLTypeAttr = null; AppContext.SetSwitch(UseCapitalizedXMLTypeAttrSwitch, false); - } } } diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActClaimTests/ActClaimSerializationTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActClaimTests/ActClaimSerializationTests.cs index 167da46710..29d5811082 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActClaimTests/ActClaimSerializationTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActClaimTests/ActClaimSerializationTests.cs @@ -291,8 +291,6 @@ public void NestedActorTokenAsSubjectShouldBeProperlySerialized() // Verify the actor object structure var actorObject = decodedToken.Payload.GetValue("act"); - Console.WriteLine("actor token created: " + actorObject.ToString()); - Assert.Equal(JsonValueKind.Object, actorObject.ValueKind); // Verify main actor claims directly from JSON object @@ -302,7 +300,6 @@ public void NestedActorTokenAsSubjectShouldBeProperlySerialized() // Verify nested actor exists and is a JSON object Assert.True(actorObject.TryGetProperty(tokenDescriptor.ActorClaimType, out var nestedActorElement)); Assert.Equal(JsonValueKind.Object, nestedActorElement.ValueKind); - Console.WriteLine("nested token created: " + nestedActorElement.ToString()); // Verify nested actor claims directly from JSON object Assert.Equal("nested-actor-id", nestedActorElement.GetProperty("sub").GetString()); From 1dfda65b4dbabe27bc88f1c33bcce0478773f421 Mon Sep 17 00:00:00 2001 From: saurabhsathe-ms Date: Sat, 7 Jun 2025 13:38:12 -0700 Subject: [PATCH 48/52] Removed the use of flag during serialization and everything is working --- .../JsonWebTokenHandler.CreateToken.cs | 8 ++++---- .../PublicAPI.Unshipped.txt | 3 ++- .../SecurityTokenDescriptor.cs | 4 ++-- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs index 2b37197f1a..1b01680557 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs @@ -668,13 +668,14 @@ internal static void WriteJwsPayload( // Duplicates are resolved according to the following priority: // SecurityTokenDescriptor.{Audience/Audiences, Issuer, Expires, IssuedAt, NotBefore}, SecurityTokenDescriptor.Claims, SecurityTokenDescriptor.Subject.Claims // SecurityTokenDescriptor.Claims are KeyValuePairs, whereas SecurityTokenDescriptor.Subject.Claims are System.Security.Claims.Claim and are processed differently. - + bool isActorFound = false; if (tokenDescriptor.Claims != null && tokenDescriptor.Claims.Count > 0) { foreach (KeyValuePair kvp in tokenDescriptor.Claims) { - if (tokenDescriptor.ActClaimSupportEnabled && kvp.Key.Equals(tokenDescriptor.ActorClaimType, StringComparison.Ordinal)) + if (kvp.Key.Equals(tokenDescriptor.ActorClaimType, StringComparison.Ordinal)) { + isActorFound = true; continue; } if (!descriptorClaimsAudienceChecked && kvp.Key.Equals(JwtRegisteredClaimNames.Aud, StringComparison.Ordinal)) @@ -758,7 +759,7 @@ internal static void WriteJwsPayload( JsonPrimitives.WriteObject(ref writer, kvp.Key, kvp.Value); } } - if (tokenDescriptor.ActClaimSupportEnabled) + if (isActorFound || tokenDescriptor.Subject?.Actor != null) WriteActorToken(writer, tokenDescriptor, setDefaultTimesOnTokenCreation, tokenLifetimeInMinutes); AddSubjectClaims(ref writer, tokenDescriptor, audienceSet, issuerSet, ref expSet, ref iatSet, ref nbfSet); @@ -1129,7 +1130,6 @@ private static SecurityTokenDescriptor CreateActorTokenDescriptor(SecurityTokenD if (actorTokenDescriptor != null) { ValidateActorChainDepth(tokenDescriptor); - actorTokenDescriptor.ActClaimSupportEnabled = tokenDescriptor.ActClaimSupportEnabled; actorTokenDescriptor.MaxActorChainLength = tokenDescriptor.MaxActorChainLength; actorTokenDescriptor.ActorClaimType = tokenDescriptor.ActorClaimType; actorTokenDescriptor.ActorChainDepth = tokenDescriptor.ActorChainDepth + 1; diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI.Unshipped.txt index ae14a4bd93..5ba44a4bcc 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI.Unshipped.txt @@ -289,4 +289,5 @@ Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorChainDepth.set -> vo Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimType.get -> string Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActorClaimType.set -> void Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.get -> int -Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.set -> void \ No newline at end of file +Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.MaxActorChainLength.set -> void +~virtual Microsoft.IdentityModel.Tokens.ActClaimRetrieverDelegate.Invoke(System.Text.Json.JsonElement actClaim, Microsoft.IdentityModel.Tokens.TokenValidationParameters tokenValidationParameters = null) -> System.Security.Claims.ClaimsIdentity diff --git a/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs b/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs index 401074839b..bdb3d6024a 100644 --- a/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs +++ b/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs @@ -171,13 +171,13 @@ public string ActorClaimType get => _actorClaimType; set { - if (string.IsNullOrEmpty(value)) + if (string.IsNullOrEmpty(value) || string.Equals(value.Trim(), ClaimTypes.Actor, StringComparison.OrdinalIgnoreCase)) throw LogHelper.LogExceptionMessage( new ArgumentOutOfRangeException( LogHelper.FormatInvariant( LogMessages.IDX11027, LogHelper.MarkAsNonPII("ActorClaimType")) - + ". ActorClaimType cannot be empty.")); + + $". ActorClaimType cannot be empty or equal to {ClaimTypes.Actor}.")); _actorClaimType = value; } } From 6caaae5e11db814da0334a80fd4c44febccaad1a Mon Sep 17 00:00:00 2001 From: saurabhsathe-ms Date: Sat, 7 Jun 2025 14:32:16 -0700 Subject: [PATCH 49/52] Removed the flag from SecurityTokenDescriptor altogether --- .../PublicAPI.Unshipped.txt | 2 -- .../SecurityTokenDescriptor.cs | 31 ++----------------- .../ActClaimDeserializationTests.cs | 8 ----- .../ActClaimSerializationTests.cs | 9 ------ 4 files changed, 2 insertions(+), 48 deletions(-) diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI.Unshipped.txt index 5ba44a4bcc..48f630977e 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI.Unshipped.txt @@ -271,8 +271,6 @@ virtual Microsoft.IdentityModel.Tokens.Experimental.ValidationParameters.CreateC ~static Microsoft.IdentityModel.Tokens.Experimental.MessageDetail.NullParameter(string parameterName) -> Microsoft.IdentityModel.Tokens.Experimental.MessageDetail Microsoft.IdentityModel.Tokens.JsonWebKeySet.JsonData.get -> string Microsoft.IdentityModel.Tokens.JsonWebKeySet.JsonData.set -> void -Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActClaimSupportEnabled.get -> bool -Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.ActClaimSupportEnabled.set -> void Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActClaimSupportEnabled.get -> bool Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActClaimSupportEnabled.set -> void Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActorChainDepth.get -> int diff --git a/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs b/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs index bdb3d6024a..e99b51850c 100644 --- a/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs +++ b/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs @@ -19,7 +19,7 @@ public class SecurityTokenDescriptor private string _actorClaimType = "act"; private int _actorClainDepth; private int _maxActorChainLength = 4; - private bool _actClaimSupportEnabled; + /// /// Gets or sets the value of the {"": audience} claim. Will be combined with and any "Aud" claims in /// or when creating a token. @@ -152,8 +152,7 @@ public int MaxActorChainLength /// /// Gets or sets the claim type that identifies the actor claim in tokens. - /// The default value is "actort" when is off - /// and "act" when the switch is on. + /// and "act" when the switch is on. /// This property determines which claim in a token contains the actor information during token /// validation and creation. /// For JWT tokens, this is the claim name in the payload that holds the actor object. @@ -202,31 +201,5 @@ public int ActorChainDepth _actorClainDepth = value; } } - - - /// - /// Controls how JWT actor claims are handled in the Microsoft.IdentityModel libraries. - /// - /// When enabled (set to true): - /// - Actor claims use the "act" claim name by default - /// - Actor claims are serialized as JSON objects - /// - Nested actor claims (actor-within-actor) are supported - /// - Claims from actor tokens appear in the ClaimsIdentity.Actor property - /// - MaxActorChainLength and ActorChainDepth properties control nesting depth validation - /// - Custom ActClaimRetrieverDelegate can be used for custom actor claim handling - /// - /// When disabled (default setting): - /// - Actor claims use the legacy "actort" claim name - /// - Actor claims are expected to be string-encoded JWT tokens - /// - Only simple actor relationships are supported (no deep nesting) - /// - public bool ActClaimSupportEnabled - { - get => _actClaimSupportEnabled; - set - { - _actClaimSupportEnabled = value; - } - } } } diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActClaimTests/ActClaimDeserializationTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActClaimTests/ActClaimDeserializationTests.cs index c9b5046908..ff02fbb8c1 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActClaimTests/ActClaimDeserializationTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActClaimTests/ActClaimDeserializationTests.cs @@ -506,7 +506,6 @@ public async Task ValidateTokenAsync_WithActorInToken_ProvidesActorClaimsIdentit }, ActorClaimType = "act", MaxActorChainLength = 4, - ActClaimSupportEnabled = true, }; string token = handler.CreateToken(tokenDescriptor); handler.MapInboundClaims = true; @@ -600,7 +599,6 @@ ClaimsIdentity CustomDelegate(JsonElement element, TokenValidationParameters tok Expires = DateTime.UtcNow.AddHours(1), SigningCredentials = Default.AsymmetricSigningCredentials, Claims = new Dictionary { { "act", actor } }, - ActClaimSupportEnabled = true, }; var token = handler.CreateToken(tokenDescriptor); @@ -654,7 +652,6 @@ public async Task ValidateTokenAsync_NestedActors_DefaultDelegate_CreatesProperC Expires = DateTime.UtcNow.AddHours(1), SigningCredentials = Default.AsymmetricSigningCredentials, Claims = new Dictionary { { "act", actor } }, - ActClaimSupportEnabled = true, }; var token = handler.CreateToken(tokenDescriptor); @@ -710,7 +707,6 @@ public async Task ValidateTokenAsync_NestingBeyondMaxActorChain_ThrowsException( Expires = DateTime.UtcNow.AddHours(1), SigningCredentials = Default.AsymmetricSigningCredentials, Claims = new Dictionary { { "act", level1Actor } }, - ActClaimSupportEnabled = true, }; var token = handler.CreateToken(tokenDescriptor); @@ -763,7 +759,6 @@ ClaimsIdentity CustomDelegate(JsonElement element, TokenValidationParameters tok Expires = DateTime.UtcNow.AddHours(1), SigningCredentials = Default.AsymmetricSigningCredentials, Claims = new Dictionary { { "act", actor } }, - ActClaimSupportEnabled = true, }; var token = handler.CreateToken(tokenDescriptor); @@ -777,7 +772,6 @@ ClaimsIdentity CustomDelegate(JsonElement element, TokenValidationParameters tok ActorClaimType = "act", MaxActorChainLength = 2, ActClaimRetrieverDelegate = CustomDelegate, - ActClaimSupportEnabled = true, }; var result = await handler.ValidateTokenAsync(token, validationParameters); @@ -819,7 +813,6 @@ ClaimsIdentity CustomDelegate(JsonElement element, TokenValidationParameters tok Audience = "https://api.example.com", Expires = DateTime.UtcNow.AddHours(1), SigningCredentials = Default.AsymmetricSigningCredentials, - ActClaimSupportEnabled = true, }; var token = handler.CreateToken(tokenDescriptor); @@ -866,7 +859,6 @@ ClaimsIdentity CustomDelegate(JsonElement element, TokenValidationParameters tok Expires = DateTime.UtcNow.AddHours(1), SigningCredentials = Default.AsymmetricSigningCredentials, Claims = new Dictionary { { "act", claimsActor } }, - ActClaimSupportEnabled = true, }; var token2 = handler.CreateToken(tokenDescriptor2); diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActClaimTests/ActClaimSerializationTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActClaimTests/ActClaimSerializationTests.cs index 29d5811082..156be4fe4e 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActClaimTests/ActClaimSerializationTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActClaimTests/ActClaimSerializationTests.cs @@ -43,7 +43,6 @@ public void ActorTokenInClaimsDictionaryShouldBeProperlySerialized() { { actorname, actorIdentity } }, - ActClaimSupportEnabled = true, }; var token = tokenHandler.CreateToken(tokenDescriptor); JsonWebToken decodedToken = tokenHandler.ReadJsonWebToken(token); @@ -93,7 +92,6 @@ public void ActorTokenAsSubjectShouldBeProperlySerialized() Audience = "https://api.example.com", Expires = DateTime.UtcNow.AddHours(1), SigningCredentials = Default.AsymmetricSigningCredentials, - ActClaimSupportEnabled = true, }; var token = tokenHandler.CreateToken(tokenDescriptor); JsonWebToken decodedToken = tokenHandler.ReadJsonWebToken(token); @@ -158,7 +156,6 @@ public void ActorTokenInBothClaimsAndSubjectShouldPreferClaimsValue() { { actorname, claimsActorIdentity } }, - ActClaimSupportEnabled = true, }; var token = tokenHandler.CreateToken(tokenDescriptor); JsonWebToken decodedToken = tokenHandler.ReadJsonWebToken(token); @@ -218,7 +215,6 @@ public void NestedActorTokenInClaimsDictionaryShouldBeProperlySerialized() { { "act", actorIdentity } }, - ActClaimSupportEnabled = true, }; var token = tokenHandler.CreateToken(tokenDescriptor); JsonWebToken decodedToken = tokenHandler.ReadJsonWebToken(token); @@ -281,7 +277,6 @@ public void NestedActorTokenAsSubjectShouldBeProperlySerialized() Expires = DateTime.UtcNow.AddHours(1), SigningCredentials = Default.AsymmetricSigningCredentials, ActorClaimType = "act", - ActClaimSupportEnabled = true, }; var token = tokenHandler.CreateToken(tokenDescriptor); JsonWebToken decodedToken = tokenHandler.ReadJsonWebToken(token); @@ -317,7 +312,6 @@ public void MaxActorChainLength_RejectsNegativeValues() Issuer = "https://example.com", Audience = "https://api.example.com", SigningCredentials = Default.AsymmetricSigningCredentials, - ActClaimSupportEnabled = true, }; tokenDescriptor.ActorClaimType = "act"; // Set the actor claim name to "act" for testing @@ -381,7 +375,6 @@ public void NestedSubjectActorTokens_ExceedingMaxDepth_ThrowsException() SigningCredentials = Default.AsymmetricSigningCredentials, ActorClaimType = "act", MaxActorChainLength = 2, - ActClaimSupportEnabled = true, }; // Act - This should throw a SecurityTokenException @@ -508,7 +501,6 @@ public void ActorTokens_MixedSourceRespectMaxActorChainLength() }, ActorClaimType = actorname, MaxActorChainLength = 1, - ActClaimSupportEnabled = true, }; var token = handler.CreateToken(tokenDescriptor); @@ -570,7 +562,6 @@ public void NestedClaimTokens_ExceedingMaxDepth_ThrowsException() }, ActorClaimType = actorname, MaxActorChainLength = 1, - ActClaimSupportEnabled = true, }; // Act - This should throw a SecurityTokenException From 7cbe282bd3d8dd420a847debe355eae9515b24a4 Mon Sep 17 00:00:00 2001 From: saurabhsathe-ms Date: Sat, 7 Jun 2025 16:52:24 -0700 Subject: [PATCH 50/52] Removed the flag from token validation parameters too. Also added one testcase to ensure that proper logic is getting triggered for right parameter --- .../JsonWebTokenHandler.cs | 22 +-- .../PublicAPI.Unshipped.txt | 2 - .../SecurityTokenDescriptor.cs | 4 +- .../TokenValidationParameters.cs | 35 +---- .../ActClaimDeserializationTests.cs | 128 ++++++++++++++++-- .../TokenValidationParametersTests.cs | 5 +- 6 files changed, 133 insertions(+), 63 deletions(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs index b24dae0b62..12e66e4f6e 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs @@ -223,14 +223,14 @@ private ClaimsIdentity CreateClaimsIdentityWithMapping(JsonWebToken jwtToken, To if (!wasMapped) claimType = jwtClaim.Type; - if (claimType == validationParameters.ActorClaimType) + if (claimType.Equals(validationParameters.ActorClaimType) || claimType.Equals("actort")) { if (identity.Actor != null) throw LogHelper.LogExceptionMessage(new InvalidOperationException(LogHelper.FormatInvariant( LogMessages.IDX14112, - LogHelper.MarkAsNonPII(JwtRegisteredClaimNames.Actort), + LogHelper.MarkAsNonPII(claimType), jwtClaim.Value))); - identity.Actor = CreateClaimsIdentityActor(jwtToken, jwtClaim.Value, validationParameters); + identity.Actor = CreateClaimsIdentityActor(jwtToken, jwtClaim.Value, validationParameters, claimType.Equals(validationParameters.ActorClaimType)); } if (wasMapped) @@ -283,11 +283,11 @@ private ClaimsIdentity CreateClaimsIdentityPrivate(JsonWebToken jwtToken, TokenV foreach (Claim jwtClaim in jwtToken.Claims) { string claimType = jwtClaim.Type; - if (claimType == validationParameters.ActorClaimType) + if (claimType == validationParameters.ActorClaimType || claimType.Equals("actort")) { if (identity.Actor != null) - throw LogHelper.LogExceptionMessage(new InvalidOperationException(LogHelper.FormatInvariant(LogMessages.IDX14112, LogHelper.MarkAsNonPII(JwtRegisteredClaimNames.Actort), jwtClaim.Value))); - identity.Actor = CreateClaimsIdentityActor(jwtToken, jwtClaim.Value, validationParameters); + throw LogHelper.LogExceptionMessage(new InvalidOperationException(LogHelper.FormatInvariant(LogMessages.IDX14112, LogHelper.MarkAsNonPII(claimType), jwtClaim.Value))); + identity.Actor = CreateClaimsIdentityActor(jwtToken, jwtClaim.Value, validationParameters, claimType.Equals(validationParameters.ActorClaimType)); } if (jwtClaim.Properties.Count == 0) @@ -619,12 +619,14 @@ private static TokenValidationResult ReadToken(string token, TokenValidationPara /// /// The actor claim string. /// The token validation parameters. + /// This tells us if we want to deserialize it as a JWT or Json Object. If this is set to true then we deserialize as JsonObject else as JWT /// A ClaimsIdentity representing the actor. /// Thrown if or is null. private ClaimsIdentity CreateClaimsIdentityActor( - JsonWebToken jwtToken, - string actorString, - TokenValidationParameters tokenValidationParameters) + JsonWebToken jwtToken, + string actorString, + TokenValidationParameters tokenValidationParameters, + bool isStandardAct = false) { if (string.IsNullOrEmpty(actorString)) throw LogHelper.LogArgumentNullException(nameof(actorString)); @@ -632,7 +634,7 @@ private ClaimsIdentity CreateClaimsIdentityActor( if (tokenValidationParameters == null) throw LogHelper.LogArgumentNullException(nameof(tokenValidationParameters)); - if (tokenValidationParameters.ActClaimSupportEnabled) + if (isStandardAct) { if (jwtToken.TryGetPayloadValue(tokenValidationParameters.ActorClaimType, out JsonElement actClaim)) { diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI.Unshipped.txt index 48f630977e..aee6e4aa69 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI.Unshipped.txt @@ -271,8 +271,6 @@ virtual Microsoft.IdentityModel.Tokens.Experimental.ValidationParameters.CreateC ~static Microsoft.IdentityModel.Tokens.Experimental.MessageDetail.NullParameter(string parameterName) -> Microsoft.IdentityModel.Tokens.Experimental.MessageDetail Microsoft.IdentityModel.Tokens.JsonWebKeySet.JsonData.get -> string Microsoft.IdentityModel.Tokens.JsonWebKeySet.JsonData.set -> void -Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActClaimSupportEnabled.get -> bool -Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActClaimSupportEnabled.set -> void Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActorChainDepth.get -> int Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActorChainDepth.set -> void Microsoft.IdentityModel.Tokens.TokenValidationParameters.ActorClaimType.get -> string diff --git a/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs b/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs index e99b51850c..5ce495903b 100644 --- a/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs +++ b/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs @@ -170,13 +170,13 @@ public string ActorClaimType get => _actorClaimType; set { - if (string.IsNullOrEmpty(value) || string.Equals(value.Trim(), ClaimTypes.Actor, StringComparison.OrdinalIgnoreCase)) + if (string.IsNullOrEmpty(value) || string.Equals(value.Trim(), "actort", StringComparison.OrdinalIgnoreCase)) throw LogHelper.LogExceptionMessage( new ArgumentOutOfRangeException( LogHelper.FormatInvariant( LogMessages.IDX11027, LogHelper.MarkAsNonPII("ActorClaimType")) - + $". ActorClaimType cannot be empty or equal to {ClaimTypes.Actor}.")); + + ". ActorClaimType cannot be empty or equal to actort")); _actorClaimType = value; } } diff --git a/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs b/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs index 8e04d238d5..af400084f1 100644 --- a/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs +++ b/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs @@ -20,7 +20,6 @@ public partial class TokenValidationParameters private string _nameClaimType = ClaimsIdentity.DefaultNameClaimType; private string _roleClaimType = ClaimsIdentity.DefaultRoleClaimType; private Dictionary _instancePropertyBag; - private bool _actClaimSupportEnabled; /// /// This is the default value of when creating a . @@ -134,7 +133,6 @@ protected TokenValidationParameters(TokenValidationParameters other) MaxActorChainLength = other.MaxActorChainLength; ActorChainDepth = other.ActorChainDepth; ActorClaimType = other.ActorClaimType; - ActClaimSupportEnabled = other.ActClaimSupportEnabled; } /// @@ -789,8 +787,6 @@ public string RoleClaimType /// /// Gets or sets the claim type that identifies the actor claim in tokens. - /// The default value is "actort" when is off - /// and "act" when the switch is on. /// This property determines which claim in a token contains the actor information during token /// validation and creation. /// For JWT tokens, this is the claim name in the payload that holds the actor object. @@ -805,16 +801,16 @@ public string RoleClaimType /// public string ActorClaimType { - get => _actClaimSupportEnabled ? actorClaimType : "actort"; + get => actorClaimType; set { - if (string.IsNullOrEmpty(value)) + if (string.IsNullOrEmpty(value) || string.Equals(value.Trim(), "actort", StringComparison.OrdinalIgnoreCase)) throw LogHelper.LogExceptionMessage( new ArgumentOutOfRangeException( LogHelper.FormatInvariant( LogMessages.IDX11027, LogHelper.MarkAsNonPII("ActorClaimType")) - + ". ActorClaimType cannot be set to empty.")); + + $". ActorClaimType cannot be empty or equal to actort.")); actorClaimType = value; } } @@ -890,30 +886,5 @@ public int MaxActorChainLength maxActorChainLength = value; } } - - /// - /// Controls how JWT actor claims are handled in the Microsoft.IdentityModel libraries. - /// - /// When enabled (set to true): - /// - Actor claims use the "act" claim name by default - /// - Actor claims are serialized as JSON objects - /// - Nested actor claims (actor-within-actor) are supported - /// - Claims from actor tokens appear in the ClaimsIdentity.Actor property - /// - MaxActorChainLength and ActorChainDepth properties control nesting depth validation - /// - Custom ActClaimRetrieverDelegate can be used for custom actor claim handling - /// - /// When disabled (default setting): - /// - Actor claims use the legacy "actort" claim name - /// - Actor claims are expected to be string-encoded JWT tokens - /// - Only simple actor relationships are supported (no deep nesting) - /// - public bool ActClaimSupportEnabled - { - get => _actClaimSupportEnabled; - set - { - _actClaimSupportEnabled = value; - } - } } } diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActClaimTests/ActClaimDeserializationTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActClaimTests/ActClaimDeserializationTests.cs index ff02fbb8c1..79a0621691 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActClaimTests/ActClaimDeserializationTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActClaimTests/ActClaimDeserializationTests.cs @@ -31,7 +31,6 @@ public void BasicJsonElementShouldCreateClaimsIdentityCorrectly() var validationParameters = new TokenValidationParameters() { ActorClaimType = "act", - ActClaimSupportEnabled = true, }; // Create ClaimsIdentity from JsonElement @@ -77,7 +76,6 @@ public void NestedActorInJsonElementShouldCreateNestedClaimsIdentity() var tokenValidationParameters = new TokenValidationParameters { ActorClaimType = "act", - ActClaimSupportEnabled = true, }; // Create ClaimsIdentity from JsonElement @@ -128,7 +126,6 @@ public void MultiLevelNestedActorJsonShouldHandleProperDepth() { ActorClaimType = "act", MaxActorChainLength = 3, - ActClaimSupportEnabled = true }; // Create ClaimsIdentity from JsonElement @@ -188,7 +185,6 @@ public void NestedActorExceedingMaxDepth_ThrowsException() ActorClaimType = "act", MaxActorChainLength = 2, ActorChainDepth = 1, - ActClaimSupportEnabled = true }; // Act - This should throw a SecurityTokenException @@ -233,7 +229,6 @@ public void JsonElementWithArrayValuesShouldProcessCorrectly() var tokenValidationParameters = new TokenValidationParameters { ActorClaimType = "act", - ActClaimSupportEnabled = true }; // Create ClaimsIdentity from JsonElement @@ -282,7 +277,6 @@ public void JsonElementWithComplexTypesShouldHandleCorrectly() var tokenValidationParameters = new TokenValidationParameters { ActorClaimType = "act", - ActClaimSupportEnabled = true, }; // Create ClaimsIdentity from JsonElement @@ -329,7 +323,6 @@ public void NonObjectJsonElement_ThrowsException() var tokenValidationParameters = new TokenValidationParameters { ActorClaimType = "act", - ActClaimSupportEnabled = true, }; // Act - This should throw an ArgumentException @@ -404,7 +397,6 @@ public void CustomActorClaimNameShouldBeRespected() var tokenValidationParameters = new TokenValidationParameters { ActorClaimType = "actort", - ActClaimSupportEnabled = true, }; // Create ClaimsIdentity from JsonElement @@ -452,7 +444,6 @@ public void ActorChainDepthShouldBeIncremented() ActorClaimType = "act", MaxActorChainLength = 4, ActorChainDepth = 2, - ActClaimSupportEnabled = true, }; // Create ClaimsIdentity from JsonElement @@ -513,7 +504,6 @@ public async Task ValidateTokenAsync_WithActorInToken_ProvidesActorClaimsIdentit // Validate token var validationParameters = new TokenValidationParameters { - ActClaimSupportEnabled = true, ValidateIssuer = false, ValidateAudience = false, ValidateLifetime = false, @@ -612,7 +602,6 @@ ClaimsIdentity CustomDelegate(JsonElement element, TokenValidationParameters tok ActorClaimType = "act", MaxActorChainLength = 3, ActClaimRetrieverDelegate = CustomDelegate, - ActClaimSupportEnabled = true, }; var result = await handler.ValidateTokenAsync(token, validationParameters); @@ -664,7 +653,6 @@ public async Task ValidateTokenAsync_NestedActors_DefaultDelegate_CreatesProperC ValidateIssuerSigningKey = true, ActorClaimType = "act", MaxActorChainLength = 2, - ActClaimSupportEnabled = true, }; var result = await handler.ValidateTokenAsync(token, validationParameters); @@ -719,7 +707,6 @@ public async Task ValidateTokenAsync_NestingBeyondMaxActorChain_ThrowsException( ValidateIssuerSigningKey = true, ActorClaimType = "act", MaxActorChainLength = 2, - ActClaimSupportEnabled = true, }; handler.MapInboundClaims = true; var result = await handler.ValidateTokenAsync(token, validationParameters); @@ -824,7 +811,6 @@ ClaimsIdentity CustomDelegate(JsonElement element, TokenValidationParameters tok IssuerSigningKey = Default.AsymmetricSigningKey, ValidateIssuerSigningKey = true, ActorClaimType = "act", - ActClaimSupportEnabled = true, }; // Default delegate @@ -869,5 +855,119 @@ ClaimsIdentity CustomDelegate(JsonElement element, TokenValidationParameters tok TestUtilities.AssertFailIfErrors(context); } + + [Fact] + public async Task ValidateTokenAsync_WithActortClaim_HandlesJwtStringNotJson() + { + var context = new CompareContext($"{this}.ValidateTokenAsync_WithActortClaim_HandlesJwtStringNotJson"); + try + { + // ARRANGE + // First create a JWT token to use as the actor token string + var innerHandler = new JsonWebTokenHandler(); + var actorJwtIdentity = new CaseSensitiveClaimsIdentity("ActorAuth"); + actorJwtIdentity.AddClaim(new Claim("sub", "actor-subject-id")); + actorJwtIdentity.AddClaim(new Claim("name", "Actor Name")); + + var actorJwtDescriptor = new SecurityTokenDescriptor + { + Subject = actorJwtIdentity, + Issuer = "https://actor.example.com", + Audience = "https://api.example.com", + SigningCredentials = Default.AsymmetricSigningCredentials + }; + + // Create the actor token as a JWT string + string actorJwtString = innerHandler.CreateToken(actorJwtDescriptor); + + // Now create the main token with the actort claim containing the JWT string + var mainIdentity = new CaseSensitiveClaimsIdentity("Bearer"); + mainIdentity.AddClaim(new Claim("sub", "main-subject-id")); + mainIdentity.AddClaim(new Claim("name", "Main User")); + + var handler = new JsonWebTokenHandler(); + var mainTokenDescriptor = new SecurityTokenDescriptor + { + Subject = mainIdentity, + Issuer = "https://example.com", + Audience = "https://api.example.com", + Expires = DateTime.UtcNow.AddHours(1), + SigningCredentials = Default.AsymmetricSigningCredentials, + Claims = new Dictionary + { + // Use actort claim with JWT string + { "actort", actorJwtString } + } + }; + + // Create the main token + string mainToken = handler.CreateToken(mainTokenDescriptor); + + // ACT + // Validate the token with actort claim type + var validationParameters = new TokenValidationParameters + { + ValidateIssuer = false, + ValidateAudience = false, + ValidateLifetime = false, + IssuerSigningKey = Default.AsymmetricSigningKey, + ValidateIssuerSigningKey = true + }; + + var result = await handler.ValidateTokenAsync(mainToken, validationParameters); + + // ASSERT + // Verify validation succeeded + Assert.True(result.IsValid); + Assert.NotNull(result.ClaimsIdentity); + + // Verify actor is processed as a JWT + Assert.NotNull(result.ClaimsIdentity.Actor); + + // The actor should have claims from the JWT token + var actorSubClaim = result.ClaimsIdentity.Actor.Claims.FirstOrDefault(c => c.Type == "sub"); + var actorNameClaim = result.ClaimsIdentity.Actor.Claims.FirstOrDefault(c => c.Type == "name"); + + Assert.NotNull(actorSubClaim); + Assert.NotNull(actorNameClaim); + Assert.Equal("actor-subject-id", actorSubClaim.Value); + Assert.Equal("Actor Name", actorNameClaim.Value); + + // For comparison, create another token with 'act' claim as JSON + var jsonActor = new CaseSensitiveClaimsIdentity("ActorAuth"); + jsonActor.AddClaim(new Claim("sub", "json-actor-id")); + jsonActor.AddClaim(new Claim("name", "JSON Actor")); + + var jsonTokenDescriptor = new SecurityTokenDescriptor + { + Subject = mainIdentity, + Issuer = "https://example.com", + Audience = "https://api.example.com", + Expires = DateTime.UtcNow.AddHours(1), + SigningCredentials = Default.AsymmetricSigningCredentials, + Claims = new Dictionary + { + { "actor_claim_name", jsonActor } + }, + ActorClaimType = "actor_claim_name", + }; + + string jsonToken = handler.CreateToken(jsonTokenDescriptor); + validationParameters.ActorClaimType = "actor_claim_name"; + var jsonResult = await handler.ValidateTokenAsync(jsonToken, validationParameters); + + // Verify different processing method + Assert.NotNull(jsonResult.ClaimsIdentity.Actor); + Assert.Equal("json-actor-id", jsonResult.ClaimsIdentity.Actor.Claims.First(c => c.Type == "sub").Value); + + TestUtilities.AssertFailIfErrors(context); + } + catch (Exception ex) + { + context.Diffs.Add($"Exception: {ex}"); + TestUtilities.AssertFailIfErrors(context); + + } + } } } diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/TokenValidationParametersTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/TokenValidationParametersTests.cs index beb573b913..f7d0584d54 100644 --- a/test/Microsoft.IdentityModel.Tokens.Tests/TokenValidationParametersTests.cs +++ b/test/Microsoft.IdentityModel.Tokens.Tests/TokenValidationParametersTests.cs @@ -15,7 +15,7 @@ namespace Microsoft.IdentityModel.Tokens.Tests { public class TokenValidationParametersTests { - int ExpectedPropertyCount = 67; + int ExpectedPropertyCount = 66; // GetSets() compares the total property count which includes internal properties, against a list of public properties, minus delegates. // This allows us to keep track of any properties we are including in the total that are not public nor delegates. @@ -198,8 +198,7 @@ public void GetSets() { PropertyNamesAndSetGetValue = new List>> { - new KeyValuePair>("ActClaimSupportEnabled", new List{false}), - new KeyValuePair>("ActorClaimType", new List{"actort"}), + new KeyValuePair>("ActorClaimType", new List{"act"}), new KeyValuePair>("ActorChainDepth", new List{0,1}), new KeyValuePair>("ActorValidationParameters", new List{(TokenValidationParameters)null, new TokenValidationParameters(), new TokenValidationParameters()}), new KeyValuePair>("AuthenticationType", new List{(string)null, Guid.NewGuid().ToString(), Guid.NewGuid().ToString()}), From e28773c74f14ee3010834fa287757bf0a16ec0fd Mon Sep 17 00:00:00 2001 From: saurabhsathe-ms Date: Thu, 19 Jun 2025 10:54:42 -0700 Subject: [PATCH 51/52] Resolved feedback! --- .../JsonWebTokenHandler.CreateToken.cs | 16 ++++++++++++++-- .../LogMessages.cs | 1 + 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs index 1b01680557..ba64f0fd4c 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs @@ -1090,6 +1090,7 @@ internal static void WriteActorToken( var actorTokenDescriptor = CreateActorTokenDescriptor(tokenDescriptor); if (actorTokenDescriptor == null || actorTokenDescriptor.Subject == null) return; + writer.WritePropertyName(tokenDescriptor.ActorClaimType); WriteJwsPayload(ref writer, actorTokenDescriptor, setDefaultTimesOnTokenCreation, tokenLifetimeInMinutes); } @@ -1110,14 +1111,25 @@ private static SecurityTokenDescriptor CreateActorTokenDescriptor(SecurityTokenD { SecurityTokenDescriptor actorTokenDescriptor = null; - // Check for actor in claims first if (tokenDescriptor.Claims?.ContainsKey(tokenDescriptor.ActorClaimType) == true) { - ClaimsIdentity actor = tokenDescriptor.Claims[tokenDescriptor.ActorClaimType] as ClaimsIdentity; + object actorValue = tokenDescriptor.Claims[tokenDescriptor.ActorClaimType]; + ClaimsIdentity actor = actorValue as ClaimsIdentity; + + if (actor == null) + { + throw LogHelper.LogExceptionMessage(new SecurityTokenException( + LogHelper.FormatInvariant( + LogMessages.IDX14315, + LogHelper.MarkAsNonPII(tokenDescriptor.ActorClaimType), + LogHelper.MarkAsNonPII(actorValue?.GetType().ToString() ?? "null")))); + } + actorTokenDescriptor = new SecurityTokenDescriptor { Subject = actor, }; + } // Then check for actor in subject else if (tokenDescriptor.Subject?.Actor != null) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/LogMessages.cs b/src/Microsoft.IdentityModel.JsonWebTokens/LogMessages.cs index 62696d684a..a39363427e 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/LogMessages.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/LogMessages.cs @@ -53,5 +53,6 @@ internal static class LogMessages internal const string IDX14312 = "IDX14312: Unable to decode the cipher text as a Base64Url encoded string."; internal const string IDX14313 = "IDX14313: Unable to serialize/deserialize act claim. Maximum actor token depth reached. Current nesting depth is {0} while max depth set is {1}"; internal const string IDX14314 = "IDX14314: Unable to deserialize act claim. Exception faced while using custom delegate to deserialize act claim. Nested exception is :{0}"; + internal const string IDX14315 = "IDX14315: Encountered an exception while processing the actor claim. Actor claim {0} is not a claims identity. It is of type {1}."; } } From badf08a0087f04ba450fc80da6c35fe5433f6ee9 Mon Sep 17 00:00:00 2001 From: saurabhsathe-ms Date: Thu, 19 Jun 2025 14:57:54 -0700 Subject: [PATCH 52/52] Change HasKey with TryGetValue for perf improvement --- .../JsonWebTokenHandler.CreateToken.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs index ba64f0fd4c..d588b32be1 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs @@ -1111,12 +1111,9 @@ private static SecurityTokenDescriptor CreateActorTokenDescriptor(SecurityTokenD { SecurityTokenDescriptor actorTokenDescriptor = null; - if (tokenDescriptor.Claims?.ContainsKey(tokenDescriptor.ActorClaimType) == true) + if (tokenDescriptor.Claims?.TryGetValue(tokenDescriptor.ActorClaimType, out object actorValue) == true) { - object actorValue = tokenDescriptor.Claims[tokenDescriptor.ActorClaimType]; - ClaimsIdentity actor = actorValue as ClaimsIdentity; - - if (actor == null) + if (actorValue is not ClaimsIdentity actor) { throw LogHelper.LogExceptionMessage(new SecurityTokenException( LogHelper.FormatInvariant(