Skip to content

Conversation

@saurabhsathe-ms
Copy link
Contributor

@saurabhsathe-ms saurabhsathe-ms commented May 6, 2025

Serialize and deserialize actor token in claims identity

  • [*] You've read the Contributor Guide and Code of Conduct.
  • [*] You've included unit or integration tests for your change, where applicable.
  • [*] You've included inline docs for your change, where applicable.
  • [*] If any gains or losses in performance are possible, you've included benchmarks for your changes. More info
  • [*] There's an open issue for the PR that you are making. If you'd like to propose a new feature or change, please open an issue to discuss the change or find an existing issue.

Description

In this PR:

  1. I have added a logic to serialize the actor token which can be present in SecurityTokenDescriptor.Claims dictionary or in SecurityTokenDescriptor.Subject or in both
  2. A static variable MaxActorChainLength has been introduced to control the recursion depth while serialization in case of nested actor tokens
  3. When actor token is present in both, actor token from SecurityTokenDescriptor.Claims dictionary will be chosen

File changes:

Here’s a summary of all the changes made in this PR across the listed files:


1. src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs

  • Support for Nested Actor Claims ("act"):
    • Introduced logic to serialize/deserialize the actor claim (act) in JWT payloads, controlled by a new AppContext switch.
    • Added skipping logic for the actor claim when writing claims.
    • Added a new method WriteActorToken that serializes nested actor tokens, with recursion depth checks via MaxActorChainLength.
    • Added logic for validating and creating nested actor token descriptors.
    • Enforced maximum actor nesting using ValidateActorChainDepth.

2. src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs

  • Actor Claim Validation:
    • If actor claim handling is enabled (via AppContext switch) and the actor claim (act) exists, validation is delegated to a new ActorTokenValidationDelegate.
    • Throws an explicit exception if validation is required but the delegate is not provided.

3. src/Microsoft.IdentityModel.JsonWebTokens/LogMessages.cs

  • New Log Messages:
    • Added IDX14115 for missing ActorTokenValidationDelegate.
    • Added IDX14313 for exceeding maximum actor token nesting.

4. src/Microsoft.IdentityModel.JsonWebTokens/Microsoft.IdentityModel.JsonWebTokens.csproj

  • No functional code change; only a BOM (byte order mark) added at the top of the file.

5. src/Microsoft.IdentityModel.Tokens/AppContextSwitches.cs

  • New AppContext Switch:
    • Introduced SerializeDeserializeActorClaimSwitch and its property to enable new actor claim (act) behavior.
    • Reset logic updated to include the new switch.

6. src/Microsoft.IdentityModel.Tokens/Delegates.cs

  • New Delegate:
    • Added ActorTokenValidationDelegate, which allows custom validation of the actor claim (act).

7. src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt

  • API Surface Updates:
    • Documented new constants and properties related to actor claim support and logging.

8. src/Microsoft.IdentityModel.Tokens/LogMessages.cs

  • New Log Message:
    • Added IDX11027 for invalid handler configuration parameter values.

9. src/Microsoft.IdentityModel.Tokens/PublicAPI.Unshipped.txt

  • Public API Additions:
    • Documented new properties and delegates related to actor claim handling for TokenValidationParameters and SecurityTokenDescriptor.

10. src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs

  • Actor Claim (act) Support:
    • Added properties for MaxActorChainLength, ActorClaimName, and ActorChainDepth, with validation and documentation.
    • These allow configuring how deep actor chains can be, what the actor claim is called, and tracking the current depth.

11. src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs

  • Actor Claim (act) Support:
    • Added properties for MaxActorChainLength, ActorClaimName, ActorChainDepth, ActorTokenValidationDelegate, and ActorTokenValidationParameters.
    • These allow customization of actor claim validation, nesting, and claim name.
    • Updated the Clone() method to copy the new delegate property.

12. test/Microsoft.IdentityModel.JsonWebTokens.Tests/ActorClaimsTests.cs

  • New Test File:
    • Presumably adds tests for the new actor claim features (although the diff shows "No diff generated for this file"—likely a new file).

13. test/Microsoft.IdentityModel.TestUtils/ValidationDelegates.cs

  • Test Delegate:
    • Added a static implementation of ActorTokenValidationDelegate for use in tests.

14. test/Microsoft.IdentityModel.Tokens.Tests/TokenValidationParametersTests.cs

  • Unit Test Updates:
    • Increased expected property count due to new properties.
    • Added checks for new actor-related properties to GetSets() tests.
    • Updated CreateTokenValidationParameters() to set the new delegate for testing.

In summary:
This PR introduces configurable support for nested actor claims ("act") in JWTs, with depth limitation, customizable claim name, and a pluggable validation delegate, plus associated logging, AppContext switches, and extensive test and API surface updates for these new features.
Fixes #1840

@saurabhsathe-ms saurabhsathe-ms marked this pull request as ready for review May 6, 2025 16:41
@saurabhsathe-ms saurabhsathe-ms requested a review from a team as a code owner May 6, 2025 16:41
@github-actions
Copy link

github-actions bot commented May 6, 2025

Summary

Summary
Generated on: 5/6/2025 - 4:45:08 PM
Coverage date: 5/6/2025 - 4:35:01 PM - 5/6/2025 - 4:44:42 PM
Parser: MultiReport (60x Cobertura)
Assemblies: 1
Classes: 7
Files: 2
Line coverage: 80.3% (620 of 772)
Covered lines: 620
Uncovered lines: 152
Coverable lines: 772
Total lines: 483
Branch coverage: 67.8% (228 of 336)
Covered branches: 228
Total branches: 336
Method coverage: Feature is only available for sponsors

Coverage

Microsoft.IdentityModel.JsonWebTokens - 80.3%
Name Line Branch
Microsoft.IdentityModel.JsonWebTokens 80.3% 67.8%
Microsoft.IdentityModel.JsonWebTokens.JwtTokenUtilities 100%
System.Text.RegularExpressions.Generated 80.3% 67.8%
System.Text.RegularExpressions.Generated 80.3% 67.8%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F12A1AEDDDFE32BA
DF4DBFF323AF1BCB48B9F9721B7CD3E05F5E034CF225E3DF8__CreateJweRegex_1
79.2% 68%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F12A1AEDDDFE32BA
DF4DBFF323AF1BCB48B9F9721B7CD3E05F5E034CF225E3DF8__CreateJwsRegex_0
81.4% 67.6%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F334844C618E00D3
CEC5D3FE0D00CF0141BBEE98635313BB2CB8D3921464CE05A__CreateJweRegex_1
79.2% 68%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F334844C618E00D3
CEC5D3FE0D00CF0141BBEE98635313BB2CB8D3921464CE05A__CreateJwsRegex_0
81.4% 67.6%

@github-actions
Copy link

github-actions bot commented May 6, 2025

Summary

Summary
Generated on: 5/6/2025 - 4:53:03 PM
Coverage date: 5/6/2025 - 4:42:26 PM - 5/6/2025 - 4:52:37 PM
Parser: MultiReport (60x Cobertura)
Assemblies: 1
Classes: 7
Files: 2
Line coverage: 80.3% (620 of 772)
Covered lines: 620
Uncovered lines: 152
Coverable lines: 772
Total lines: 483
Branch coverage: 67.8% (228 of 336)
Covered branches: 228
Total branches: 336
Method coverage: Feature is only available for sponsors

Coverage

Microsoft.IdentityModel.JsonWebTokens - 80.3%
Name Line Branch
Microsoft.IdentityModel.JsonWebTokens 80.3% 67.8%
Microsoft.IdentityModel.JsonWebTokens.JwtTokenUtilities 100%
System.Text.RegularExpressions.Generated 80.3% 67.8%
System.Text.RegularExpressions.Generated 80.3% 67.8%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F12A1AEDDDFE32BA
DF4DBFF323AF1BCB48B9F9721B7CD3E05F5E034CF225E3DF8__CreateJweRegex_1
79.2% 68%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F12A1AEDDDFE32BA
DF4DBFF323AF1BCB48B9F9721B7CD3E05F5E034CF225E3DF8__CreateJwsRegex_0
81.4% 67.6%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F334844C618E00D3
CEC5D3FE0D00CF0141BBEE98635313BB2CB8D3921464CE05A__CreateJweRegex_1
79.2% 68%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F334844C618E00D3
CEC5D3FE0D00CF0141BBEE98635313BB2CB8D3921464CE05A__CreateJwsRegex_0
81.4% 67.6%

@github-actions
Copy link

github-actions bot commented May 6, 2025

Summary

Summary
Generated on: 5/6/2025 - 4:56:18 PM
Coverage date: 5/6/2025 - 4:45:49 PM - 5/6/2025 - 4:55:52 PM
Parser: MultiReport (60x Cobertura)
Assemblies: 1
Classes: 7
Files: 2
Line coverage: 80.3% (620 of 772)
Covered lines: 620
Uncovered lines: 152
Coverable lines: 772
Total lines: 483
Branch coverage: 67.8% (228 of 336)
Covered branches: 228
Total branches: 336
Method coverage: Feature is only available for sponsors

Coverage

Microsoft.IdentityModel.JsonWebTokens - 80.3%
Name Line Branch
Microsoft.IdentityModel.JsonWebTokens 80.3% 67.8%
Microsoft.IdentityModel.JsonWebTokens.JwtTokenUtilities 100%
System.Text.RegularExpressions.Generated 80.3% 67.8%
System.Text.RegularExpressions.Generated 80.3% 67.8%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F12A1AEDDDFE32BA
DF4DBFF323AF1BCB48B9F9721B7CD3E05F5E034CF225E3DF8__CreateJweRegex_1
79.2% 68%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F12A1AEDDDFE32BA
DF4DBFF323AF1BCB48B9F9721B7CD3E05F5E034CF225E3DF8__CreateJwsRegex_0
81.4% 67.6%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F334844C618E00D3
CEC5D3FE0D00CF0141BBEE98635313BB2CB8D3921464CE05A__CreateJweRegex_1
79.2% 68%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F334844C618E00D3
CEC5D3FE0D00CF0141BBEE98635313BB2CB8D3921464CE05A__CreateJwsRegex_0
81.4% 67.6%

@saurabhsathe-ms saurabhsathe-ms changed the title Ssathe/serialize claims identity Ssathe/Serialize actor token in claims identity May 6, 2025
@github-actions
Copy link

github-actions bot commented May 6, 2025

Summary

Summary
Generated on: 5/6/2025 - 6:42:30 PM
Coverage date: 5/6/2025 - 6:32:21 PM - 5/6/2025 - 6:42:05 PM
Parser: MultiReport (60x Cobertura)
Assemblies: 1
Classes: 7
Files: 2
Line coverage: 80.3% (620 of 772)
Covered lines: 620
Uncovered lines: 152
Coverable lines: 772
Total lines: 483
Branch coverage: 67.8% (228 of 336)
Covered branches: 228
Total branches: 336
Method coverage: Feature is only available for sponsors

Coverage

Microsoft.IdentityModel.JsonWebTokens - 80.3%
Name Line Branch
Microsoft.IdentityModel.JsonWebTokens 80.3% 67.8%
Microsoft.IdentityModel.JsonWebTokens.JwtTokenUtilities 100%
System.Text.RegularExpressions.Generated 80.3% 67.8%
System.Text.RegularExpressions.Generated 80.3% 67.8%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F12A1AEDDDFE32BA
DF4DBFF323AF1BCB48B9F9721B7CD3E05F5E034CF225E3DF8__CreateJweRegex_1
79.2% 68%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F12A1AEDDDFE32BA
DF4DBFF323AF1BCB48B9F9721B7CD3E05F5E034CF225E3DF8__CreateJwsRegex_0
81.4% 67.6%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F334844C618E00D3
CEC5D3FE0D00CF0141BBEE98635313BB2CB8D3921464CE05A__CreateJweRegex_1
79.2% 68%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F334844C618E00D3
CEC5D3FE0D00CF0141BBEE98635313BB2CB8D3921464CE05A__CreateJwsRegex_0
81.4% 67.6%

@saurabhsathe-ms saurabhsathe-ms requested a review from Copilot May 6, 2025 20:01
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR implements logic to serialize nested actor tokens within the claims identity and introduces a static property to control the maximum allowed nesting depth.

  • Added a static property MaxActorChainLength to control recursion depth with appropriate validations.
  • Updated token creation logic to handle nested actor tokens from either the Claims dictionary or the Subject.
  • Extended API documentation and logging with a new log message for exceeding the maximum actor chain depth.

Reviewed Changes

Copilot reviewed 4 out of 5 changed files in this pull request and generated 1 comment.

File Description
src/Microsoft.IdentityModel.JsonWebTokens/PublicAPI.Unshipped.txt Added API entries for the new MaxActorChainLength property.
src/Microsoft.IdentityModel.JsonWebTokens/LogMessages.cs Added log message IDX14313 for exceeding maximum actor chain depth.
src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs Added logic to serialize nested actor tokens, including new parameters and recursion checks.
Files not reviewed (1)
  • src/Microsoft.IdentityModel.JsonWebTokens/Microsoft.IdentityModel.JsonWebTokens.csproj: Language not supported

@saurabhsathe-ms saurabhsathe-ms requested a review from Copilot May 6, 2025 20:13
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds functionality to serialize actor tokens within the claims identity while limiting recursion through a maximum actor chain depth. Key changes include:

  • Introducing the MaxActorChainLength static property to control nested actor token depth.
  • Updating CreateToken, WriteJwsPayload, and AddSubjectClaims methods to handle actor token serialization.
  • Adding a new log message (IDX14313) to indicate when the actor token chain exceeds the allowed maximum depth.

Reviewed Changes

Copilot reviewed 4 out of 5 changed files in this pull request and generated 2 comments.

File Description
src/Microsoft.IdentityModel.JsonWebTokens/PublicAPI.Unshipped.txt Added getters/setters for MaxActorChainLength in the public API
src/Microsoft.IdentityModel.JsonWebTokens/LogMessages.cs Added new log message for serialization depth errors
src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs Updated token creation logic to include actor token serialization
Files not reviewed (1)
  • src/Microsoft.IdentityModel.JsonWebTokens/Microsoft.IdentityModel.JsonWebTokens.csproj: Language not supported

@github-actions
Copy link

github-actions bot commented May 6, 2025

Summary

Summary
Generated on: 5/6/2025 - 8:25:28 PM
Coverage date: 5/6/2025 - 8:15:01 PM - 5/6/2025 - 8:25:03 PM
Parser: MultiReport (60x Cobertura)
Assemblies: 1
Classes: 7
Files: 2
Line coverage: 80.3% (620 of 772)
Covered lines: 620
Uncovered lines: 152
Coverable lines: 772
Total lines: 483
Branch coverage: 67.8% (228 of 336)
Covered branches: 228
Total branches: 336
Method coverage: Feature is only available for sponsors

Coverage

Microsoft.IdentityModel.JsonWebTokens - 80.3%
Name Line Branch
Microsoft.IdentityModel.JsonWebTokens 80.3% 67.8%
Microsoft.IdentityModel.JsonWebTokens.JwtTokenUtilities 100%
System.Text.RegularExpressions.Generated 80.3% 67.8%
System.Text.RegularExpressions.Generated 80.3% 67.8%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F12A1AEDDDFE32BA
DF4DBFF323AF1BCB48B9F9721B7CD3E05F5E034CF225E3DF8__CreateJweRegex_1
79.2% 68%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F12A1AEDDDFE32BA
DF4DBFF323AF1BCB48B9F9721B7CD3E05F5E034CF225E3DF8__CreateJwsRegex_0
81.4% 67.6%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F334844C618E00D3
CEC5D3FE0D00CF0141BBEE98635313BB2CB8D3921464CE05A__CreateJweRegex_1
79.2% 68%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F334844C618E00D3
CEC5D3FE0D00CF0141BBEE98635313BB2CB8D3921464CE05A__CreateJwsRegex_0
81.4% 67.6%

@github-actions
Copy link

github-actions bot commented May 7, 2025

Summary

Summary
Generated on: 5/7/2025 - 5:37:47 AM
Coverage date: 5/7/2025 - 5:27:07 AM - 5/7/2025 - 5:37:22 AM
Parser: MultiReport (60x Cobertura)
Assemblies: 1
Classes: 7
Files: 2
Line coverage: 80.3% (620 of 772)
Covered lines: 620
Uncovered lines: 152
Coverable lines: 772
Total lines: 483
Branch coverage: 67.8% (228 of 336)
Covered branches: 228
Total branches: 336
Method coverage: Feature is only available for sponsors

Coverage

Microsoft.IdentityModel.JsonWebTokens - 80.3%
Name Line Branch
Microsoft.IdentityModel.JsonWebTokens 80.3% 67.8%
Microsoft.IdentityModel.JsonWebTokens.JwtTokenUtilities 100%
System.Text.RegularExpressions.Generated 80.3% 67.8%
System.Text.RegularExpressions.Generated 80.3% 67.8%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F12A1AEDDDFE32BA
DF4DBFF323AF1BCB48B9F9721B7CD3E05F5E034CF225E3DF8__CreateJweRegex_1
79.2% 68%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F12A1AEDDDFE32BA
DF4DBFF323AF1BCB48B9F9721B7CD3E05F5E034CF225E3DF8__CreateJwsRegex_0
81.4% 67.6%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F334844C618E00D3
CEC5D3FE0D00CF0141BBEE98635313BB2CB8D3921464CE05A__CreateJweRegex_1
79.2% 68%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F334844C618E00D3
CEC5D3FE0D00CF0141BBEE98635313BB2CB8D3921464CE05A__CreateJwsRegex_0
81.4% 67.6%

@github-actions
Copy link

github-actions bot commented May 7, 2025

Summary

Summary
Generated on: 5/7/2025 - 5:15:49 PM
Coverage date: 5/7/2025 - 5:05:19 PM - 5/7/2025 - 5:15:21 PM
Parser: MultiReport (60x Cobertura)
Assemblies: 1
Classes: 7
Files: 2
Line coverage: 80.3% (620 of 772)
Covered lines: 620
Uncovered lines: 152
Coverable lines: 772
Total lines: 483
Branch coverage: 67.8% (228 of 336)
Covered branches: 228
Total branches: 336
Method coverage: Feature is only available for sponsors

Coverage

Microsoft.IdentityModel.JsonWebTokens - 80.3%
Name Line Branch
Microsoft.IdentityModel.JsonWebTokens 80.3% 67.8%
Microsoft.IdentityModel.JsonWebTokens.JwtTokenUtilities 100%
System.Text.RegularExpressions.Generated 80.3% 67.8%
System.Text.RegularExpressions.Generated 80.3% 67.8%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F12A1AEDDDFE32BA
DF4DBFF323AF1BCB48B9F9721B7CD3E05F5E034CF225E3DF8__CreateJweRegex_1
79.2% 68%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F12A1AEDDDFE32BA
DF4DBFF323AF1BCB48B9F9721B7CD3E05F5E034CF225E3DF8__CreateJwsRegex_0
81.4% 67.6%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F334844C618E00D3
CEC5D3FE0D00CF0141BBEE98635313BB2CB8D3921464CE05A__CreateJweRegex_1
79.2% 68%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F334844C618E00D3
CEC5D3FE0D00CF0141BBEE98635313BB2CB8D3921464CE05A__CreateJwsRegex_0
81.4% 67.6%

@saurabhsathe-ms saurabhsathe-ms requested a review from sruke May 7, 2025 18:52
… testcase to ensure that proper logic is getting triggered for right parameter
@saurabhsathe-ms
Copy link
Contributor Author

saurabhsathe-ms commented Jun 8, 2025

Hello @kevinchalet ,
I have modified the code as per the expectations to remove the flag. You can find the code here : https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/tree/ssathe/serializeClaimsIdentityWithoutFlag
Here are some details:
Serialization:

  1. As per this code version, customer can use custom claim names while both serialization as well as deserialization.
  2. Customers are not allowed to use "actort" or any permutations of the word.
  3. The default value of custom name is "act".
  4. If claim names equals actort then default (current behavior) is used for actor claim and if the claim name matches custom claim name supplied in STD, then we serialize it as a JSON object

Deserialization:

  1. Customers can use custom claim names while both serialization as well as deserialization.
  2. Customers are not allowed to use "actort" or any permutations of the word.
  3. If claim names equals actort then the claim is deserialized assuming it to be a JWT using existing logic. If it matches the custom claim name supplied in TVP then its deserialized as a JSONObject using default or custom delegate.
  4. "act" will be the default value of custom claim name

@kevinchalet and @sruke , I wanted to have your opinion if the logic fits right. This approach looks good to me as we are avoiding those extra flags while giving the customers an option to use custom claim names too. Please let me know if anything else is expected or if we can continue with this approach. I will merge that branch into this one if everything looks good and we can proceed. I will rename the flag or delegate as we proceed.

Thank you again and looking forward to your responses!

@sruke
Copy link
Contributor

sruke commented Jun 9, 2025

Hello @kevinchalet , I have modified the code as per the expectations to remove the flag. You can find the code here : https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/tree/ssathe/serializeClaimsIdentityWithoutFlag Here are some details: Serialization:

  1. As per this code version, customer can use custom claim names while both serialization as well as deserialization.
  2. Customers are not allowed to use "actort" or any permutations of the word.
  3. The default value of custom name is "act".
  4. If claim names equals actort then default (current behavior) is used for actor claim and if the claim name matches custom claim name supplied in STD, then we serialize it as a JSON object

Deserialization:

  1. Customers can use custom claim names while both serialization as well as deserialization.
  2. Customers are not allowed to use "actort" or any permutations of the word.
  3. If claim names equals actort then the claim is deserialized assuming it to be a JWT using existing logic. If it matches the custom claim name supplied in TVP then its deserialized as a JSONObject using default or custom delegate.
  4. "act" will be the default value of custom claim name

@kevinchalet and @sruke , I wanted to have your opinion if the logic fits right. This approach looks good to me as we are avoiding those extra flags while giving the customers an option to use custom claim names too. Please let me know if anything else is expected or if we can continue with this approach. I will merge that branch into this one if everything looks good and we can proceed. I will rename the flag or delegate as we proceed.

Thank you again and looking forward to your responses!

Moving away from app context switches and checking if ActorClaimType is set on TVPs or SecurityTokenDescriptor LGTM.

@kevinchalet
Copy link
Contributor

@saurabhsathe-ms thanks! Looks good: AFAICT, the only missing piece is the ability to map ClaimsIdentity.Actor to the standard act claim, represented as a JSON object instead of a JWT token 👍🏻

Note: I finished implementation OAuth 2.0 Token Exchange support in OpenIddict (the first preview will ship today). That would be awesome if this PR was included in the next IM minor version so we can offer the best delegation story possible to users 😃

@saurabhsathe-ms
Copy link
Contributor Author

saurabhsathe-ms commented Jun 16, 2025

ClaimsIdentity.Actor

Hello @kevinchalet , thank you so much for your reply. ClaimsIdentity.Actor will be mapped to the claim whose name matches whats provided in TVP.ActorClaimType or "actort". TVP.ActorClaimType cannot take value as "actort". Its default value is "act". During serialization you can specify the actor claim type as "act" and it will be serialized as JsonObject. Right now, if you provide the TVP.ActorClaimType as "act" it will be deserialized assuming that actor claim named "act" was serialized as JSONObject and if the claim name is "actort" its deserialized assuming that it was serialized as JWT. So, can you please elaborate on what we are missing more?

@github-actions
Copy link

Summary

Summary
Generated on: 6/16/2025 - 5:37:35 PM
Coverage date: 6/16/2025 - 5:25:53 PM - 6/16/2025 - 5:37:00 PM
Parser: MultiReport (60x Cobertura)
Assemblies: 1
Classes: 7
Files: 2
Line coverage: 80.3% (620 of 772)
Covered lines: 620
Uncovered lines: 152
Coverable lines: 772
Total lines: 483
Branch coverage: 67.8% (228 of 336)
Covered branches: 228
Total branches: 336
Method coverage: Feature is only available for sponsors

Coverage

Microsoft.IdentityModel.JsonWebTokens - 80.3%
Name Line Branch
Microsoft.IdentityModel.JsonWebTokens 80.3% 67.8%
Microsoft.IdentityModel.JsonWebTokens.JwtTokenUtilities 100%
System.Text.RegularExpressions.Generated 80.3% 67.8%
System.Text.RegularExpressions.Generated 80.3% 67.8%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F12A1AEDDDFE32BA
DF4DBFF323AF1BCB48B9F9721B7CD3E05F5E034CF225E3DF8__CreateJweRegex_1
79.2% 68%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F12A1AEDDDFE32BA
DF4DBFF323AF1BCB48B9F9721B7CD3E05F5E034CF225E3DF8__CreateJwsRegex_0
81.4% 67.6%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F334844C618E00D3
CEC5D3FE0D00CF0141BBEE98635313BB2CB8D3921464CE05A__CreateJweRegex_1
79.2% 68%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F334844C618E00D3
CEC5D3FE0D00CF0141BBEE98635313BB2CB8D3921464CE05A__CreateJwsRegex_0
81.4% 67.6%

@kevinchalet
Copy link
Contributor

Its default value is "act". During serialization you can specify the actor claim type as "act" and it will be serialized as JsonObject.

Interesting. Looking at the code, it seems the only way the ClaimsIdentity.Actor property can be represented during serialization is as a JWT:

if (isActorFound || tokenDescriptor.Subject?.Actor != null)
    WriteActorToken(writer, tokenDescriptor, setDefaultTimesOnTokenCreation, tokenLifetimeInMinutes);

Did I miss something?

@saurabhsathe-ms
Copy link
Contributor Author

saurabhsathe-ms commented Jun 16, 2025

Its default value is "act". During serialization you can specify the actor claim type as "act" and it will be serialized as JsonObject.

Interesting. Looking at the code, it seems the only way the ClaimsIdentity.Actor property can be represented during serialization is as a JWT:

if (isActorFound || tokenDescriptor.Subject?.Actor != null)
    WriteActorToken(writer, tokenDescriptor, setDefaultTimesOnTokenCreation, tokenLifetimeInMinutes);

Did I miss something?

I took a look at JWTSecurityHandler which is the legacy handler that was before JsobWebtTokenHandler. What it used to do was take actor claim serialize it into a JWT and then serialize the serialized actor JWT while encoding the token as whole. Hence the generated JsonWebToken.Actor is a string that returned the encoded JWT that we needed to deserialize again. Also, it never had a logic for actor that was provided as Subject.Actor. What I am doing now is if you provide act as a claim in claims dictionary or a Subject.Actor it doesnt get serialized as a JWT rightaway but rather nested claims are processed and then serialized as a JWT as a part of token encoding process.
In case of deserialization, if it sees "act" it assumes that this was encoded as JSON and deserializes as JSON object. Let me know if you have more questions @kevinchalet

@kevinchalet
Copy link
Contributor

Also, it never had a logic for actor that was provided as Subject.Actor.

It did - and still does, actually - support serializing ClaimsIdentity.Actor: 😃

JwtPayload payload = new JwtPayload(issuer, audience, subject == null ? null : subject.Claims, notBefore, expires);
JwtHeader header = new JwtHeader(signingCredentials);
if (subject != null && subject.Actor != null)
{
payload.AddClaim(new Claim(JwtRegisteredClaimNames.Actort, this.CreateActorValue(subject.Actor)));
}

But of course, it only supports serializing it as a JWT, not as a JSON node.

Leaving JwtSecurityTokenHandler as-is is likely fine, but I'm 100% convinced JsonWebTokenHandler should support serializing ClaimsIdentity.Actor to a JSON node.

@saurabhsathe-ms
Copy link
Contributor Author

saurabhsathe-ms commented Jun 16, 2025

Thank you so much for your response @kevinchalet . Sorry for overseeing the Subject.Actor details. I am little confused as why its not a JSON. For example in this testcase we can correctly correct get the JSON back:

    [Fact]
    public void ActorTokenInClaimsDictionaryShouldBeProperlySerialized()
    {
        var context = new CompareContext($"{this}.ActorTokenInClaimsDictionaryShouldBeProperlySerialized");
        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<string, object>
                {
                    { 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.ActorClaimType), "JWT token should contain 'actort' claim");
            // Verify the actor object directly
            var actorObject = decodedToken.Payload.GetValue<JsonElement>(tokenDescriptor.ActorClaimType);
            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}");
        }

@kevinchalet
Copy link
Contributor

kevinchalet commented Jun 17, 2025

I am little confused as why its not a JSON. For example in this testcase we can correctly correct get the JSON back:

Yeah, that part works, but I was explicitly referring to the serialization of ClaimsIdentity.Actor.
I haven't tested, but I don't think this test is green:

[Fact]
public void ActorAttachedToSubjectIdentityShouldBeProperlySerialized()
{
    var context = new CompareContext($"{this}.ActorAttachedToSubjectIdentityShouldBeProperlySerialized");
    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"));

        // Attach the actor to the main identity
        mainIdentity.Actor = actorIdentity;

        // Create a token using the standard "act" claim instead of "actort"
        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",
        };
        var token = tokenHandler.CreateToken(tokenDescriptor);
        JsonWebToken decodedToken = tokenHandler.ReadJsonWebToken(token);

        // Verify actor claim exists in the token
        Assert.True(decodedToken.Payload.HasClaim(tokenDescriptor.ActorClaimType), "JWT token should contain 'act' claim");
        // Verify the actor object directly
        var actorObject = decodedToken.Payload.GetValue<JsonElement>(tokenDescriptor.ActorClaimType);
        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}");
    }
}

@github-actions
Copy link

Summary

Summary
Generated on: 6/19/2025 - 6:09:04 PM
Coverage date: 6/19/2025 - 5:58:15 PM - 6/19/2025 - 6:08:30 PM
Parser: MultiReport (60x Cobertura)
Assemblies: 1
Classes: 7
Files: 2
Line coverage: 80.3% (620 of 772)
Covered lines: 620
Uncovered lines: 152
Coverable lines: 772
Total lines: 483
Branch coverage: 67.8% (228 of 336)
Covered branches: 228
Total branches: 336
Method coverage: Feature is only available for sponsors

Coverage

Microsoft.IdentityModel.JsonWebTokens - 80.3%
Name Line Branch
Microsoft.IdentityModel.JsonWebTokens 80.3% 67.8%
Microsoft.IdentityModel.JsonWebTokens.JwtTokenUtilities 100%
System.Text.RegularExpressions.Generated 80.3% 67.8%
System.Text.RegularExpressions.Generated 80.3% 67.8%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F12A1AEDDDFE32BA
DF4DBFF323AF1BCB48B9F9721B7CD3E05F5E034CF225E3DF8__CreateJweRegex_1
79.2% 68%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F12A1AEDDDFE32BA
DF4DBFF323AF1BCB48B9F9721B7CD3E05F5E034CF225E3DF8__CreateJwsRegex_0
81.4% 67.6%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F334844C618E00D3
CEC5D3FE0D00CF0141BBEE98635313BB2CB8D3921464CE05A__CreateJweRegex_1
79.2% 68%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F334844C618E00D3
CEC5D3FE0D00CF0141BBEE98635313BB2CB8D3921464CE05A__CreateJwsRegex_0
81.4% 67.6%

@saurabhsathe-ms saurabhsathe-ms marked this pull request as ready for review June 19, 2025 21:21
@github-actions
Copy link

Summary

Summary
Generated on: 6/19/2025 - 9:36:33 PM
Coverage date: 6/19/2025 - 9:25:35 PM - 6/19/2025 - 9:35:48 PM
Parser: MultiReport (60x Cobertura)
Assemblies: 1
Classes: 7
Files: 2
Line coverage: 80.3% (620 of 772)
Covered lines: 620
Uncovered lines: 152
Coverable lines: 772
Total lines: 483
Branch coverage: 67.8% (228 of 336)
Covered branches: 228
Total branches: 336
Method coverage: Feature is only available for sponsors

Coverage

Microsoft.IdentityModel.JsonWebTokens - 80.3%
Name Line Branch
Microsoft.IdentityModel.JsonWebTokens 80.3% 67.8%
Microsoft.IdentityModel.JsonWebTokens.JwtTokenUtilities 100%
System.Text.RegularExpressions.Generated 80.3% 67.8%
System.Text.RegularExpressions.Generated 80.3% 67.8%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F12A1AEDDDFE32BA
DF4DBFF323AF1BCB48B9F9721B7CD3E05F5E034CF225E3DF8__CreateJweRegex_1
79.2% 68%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F12A1AEDDDFE32BA
DF4DBFF323AF1BCB48B9F9721B7CD3E05F5E034CF225E3DF8__CreateJwsRegex_0
81.4% 67.6%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F334844C618E00D3
CEC5D3FE0D00CF0141BBEE98635313BB2CB8D3921464CE05A__CreateJweRegex_1
79.2% 68%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F334844C618E00D3
CEC5D3FE0D00CF0141BBEE98635313BB2CB8D3921464CE05A__CreateJwsRegex_0
81.4% 67.6%

@github-actions
Copy link

Summary

Summary
Generated on: 6/19/2025 - 10:12:09 PM
Coverage date: 6/19/2025 - 10:00:49 PM - 6/19/2025 - 10:11:29 PM
Parser: MultiReport (60x Cobertura)
Assemblies: 1
Classes: 7
Files: 2
Line coverage: 80.3% (620 of 772)
Covered lines: 620
Uncovered lines: 152
Coverable lines: 772
Total lines: 483
Branch coverage: 67.8% (228 of 336)
Covered branches: 228
Total branches: 336
Method coverage: Feature is only available for sponsors

Coverage

Microsoft.IdentityModel.JsonWebTokens - 80.3%
Name Line Branch
Microsoft.IdentityModel.JsonWebTokens 80.3% 67.8%
Microsoft.IdentityModel.JsonWebTokens.JwtTokenUtilities 100%
System.Text.RegularExpressions.Generated 80.3% 67.8%
System.Text.RegularExpressions.Generated 80.3% 67.8%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F12A1AEDDDFE32BA
DF4DBFF323AF1BCB48B9F9721B7CD3E05F5E034CF225E3DF8__CreateJweRegex_1
79.2% 68%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F12A1AEDDDFE32BA
DF4DBFF323AF1BCB48B9F9721B7CD3E05F5E034CF225E3DF8__CreateJwsRegex_0
81.4% 67.6%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F334844C618E00D3
CEC5D3FE0D00CF0141BBEE98635313BB2CB8D3921464CE05A__CreateJweRegex_1
79.2% 68%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F334844C618E00D3
CEC5D3FE0D00CF0141BBEE98635313BB2CB8D3921464CE05A__CreateJwsRegex_0
81.4% 67.6%

public class ActClaimSerializationTests
{
[Fact]
public void ActorTokenInClaimsDictionaryShouldBeProperlySerialized()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest adding underscores to the test names like in other tests. Something like:

  • ActorTokenInClaimsDictionary_CorrectlySerialized
  • NestedActorToken_InClaimsDictionary_IsCorrectlySerialized

See guidelines here: https://learn.microsoft.com/en-us/dotnet/core/testing/unit-testing-best-practices#follow-test-naming-standards

/// <para>This limit applies to both token creation and validation processes.</para>
/// </remarks>
/// <exception cref="ArgumentOutOfRangeException">Thrown if the value is less than 0 or greater than 4.</exception>
public int MaxActorChainLength
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the previous discussions, was there a request to make MaxActorChainLength and ActorChainDepth be setteable by the user?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the offline conversation - I'd remove the MaxActorChainLength and ActorChainDepth public properties.

/// and use "act" as the claim type.</para>
/// <para>To use the legacy string-based actor token format, leave the switch off and use "actort".</para>
/// </remarks>
public string ActorClaimType
Copy link
Contributor

@pmaytak pmaytak Jul 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to specs, can a valid JWT have both, "act" and "actort" claims? What about multiple actor claims?

Copy link
Contributor

@pmaytak pmaytak Jul 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a need for this property? When reading a token, why not just parse the first "act" claim that we come upon?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An option here is to allow setting this to only "act" or "actort" as valid actor claim type name values and default to "actort" since it's what is used now.

/// Thrown if the value is null or empty.
/// </exception>
/// <remarks>
/// <para>To use the newer JSON object-based actor format, set <c>AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true)</c>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this switch exist? I don't see it in files.

/// <remarks>
/// <para>To use the newer JSON object-based actor format, set <c>AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true)</c>
/// and use "act" as the claim type.</para>
/// <para>To use the legacy string-based actor token format, leave the switch off and use "actort".</para>
Copy link
Contributor

@pmaytak pmaytak Jul 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why default to legacy and not the newest format?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"actort" is what is currently used, so we should default to it.

/// <para>This limit applies to both token creation and validation processes.</para>
/// </remarks>
/// <exception cref="ArgumentOutOfRangeException">Thrown if the value is less than 0 or greater than 4.</exception>
public int MaxActorChainLength
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the offline conversation - I'd remove the MaxActorChainLength and ActorChainDepth public properties.

[Fact]
public void BasicJsonElementShouldCreateClaimsIdentityCorrectly()
{
var context = new CompareContext($"{this}.BasicJsonElementShouldCreateClaimsIdentityCorrectly");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests are simple. I would remove the CompareContext and try/catch - any unexpected exceptions will just break the test anyway.

context.Diffs.Add("Expected exception was not thrown.");
TestUtilities.AssertFailIfErrors(context);
}
catch (SecurityTokenException ex)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assert.Throws should be used instead of try/catch.

/// <remarks>
/// <para>To use the newer JSON object-based actor format, set <c>AppContext.SetSwitch(AppContextSwitches.EnableActClaimSupportSwitch, true)</c>
/// and use "act" as the claim type.</para>
/// <para>To use the legacy string-based actor token format, leave the switch off and use "actort".</para>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"actort" is what is currently used, so we should default to it.

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<String>, and System.Text.Json.JsonElement.";
public const string IDX11027 = "IDX11027: Invalid JsonWebToken handler configuration parameter value provided for {0}";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this you can use the formatter for that extra message so the callers don't have to do string concatenation.

Suggested change
public const string IDX11027 = "IDX11027: Invalid JsonWebToken handler configuration parameter value provided for {0}";
public const string IDX11027 = "IDX11027: Invalid JsonWebToken handler configuration parameter value provided for {0}. {1}";

claimType = jwtClaim.Type;

if (claimType == ClaimTypes.Actor)
if (claimType.Equals(validationParameters.ActorClaimType) || claimType.Equals("actort"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use JwtRegisteredClaimNames.Actort instead of literal.

/// <param name="tokenValidationParameters">These parameters have details like nested actor chain length and max permissible actor length</param>
/// <param name="issuer">The issuer for the claims.</param>
/// <returns>A ClaimsIdentity containing claims from the JsonElement.</returns>
public static ClaimsIdentity CreateActorClaimsIdentityFromJsonElement(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be private.

}

if (jsonElement.ValueKind != JsonValueKind.Object)
throw LogHelper.LogExceptionMessage(new ArgumentException("Actor token must be a JSON object"));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use IDX error message?

// Use CaseSensitiveClaimsIdentity for consistent behavior with the rest of the library
var identity = new CaseSensitiveClaimsIdentity();

issuer = issuer ?? ClaimsIdentity.DefaultIssuer;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
issuer = issuer ?? ClaimsIdentity.DefaultIssuer;
issuer ??= ClaimsIdentity.DefaultIssuer;

LogHelper.FormatInvariant(
LogMessages.IDX14315,
LogHelper.MarkAsNonPII(tokenDescriptor.ActorClaimType),
LogHelper.MarkAsNonPII(actorValue?.GetType().ToString() ?? "null"))));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
LogHelper.MarkAsNonPII(actorValue?.GetType().ToString() ?? "null"))));
LogHelper.MarkAsNonPII(actorValue?.GetType().FullName ?? "null"))));

@pmaytak
Copy link
Contributor

pmaytak commented Jul 9, 2025

A consideration is to split this PR into two, if it's easier to implement: (1) enable serialization/deserialization for "actort" claim type only and (2) support "act" claim type.

JsonPrimitives.WriteObject(ref writer, kvp.Key, kvp.Value);
}
}
if (isActorFound || tokenDescriptor.Subject?.Actor != null)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be: !isActorFound

{
foreach (KeyValuePair<string, object> kvp in tokenDescriptor.Claims)
{
if (kvp.Key.Equals(tokenDescriptor.ActorClaimType, StringComparison.Ordinal))
Copy link
Contributor

@brentschmaltz brentschmaltz Jul 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following the logic for duplicate claims, we should write out the 'actor' claim, not sure why we continue.

         // Duplicates are resolved according to the following priority:
         // SecurityTokenDescriptor.{Audience/Audiences, Issuer, Expires, IssuedAt, NotBefore}, SecurityTokenDescriptor.Claims, SecurityTokenDescriptor.Subject.Claims
         // SecurityTokenDescriptor.Claims are KeyValuePairs<string,object>, whereas SecurityTokenDescriptor.Subject.Claims are System.Security.Claims.Claim and are processed differently.

bool setDefaultTimesOnTokenCreation,
int tokenLifetimeInMinutes)
{
if (tokenDescriptor == null)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tokenDescriptor should never be null here, you can check for null, but don't throw, just return.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, will do this!

return;

writer.WritePropertyName(tokenDescriptor.ActorClaimType);
WriteJwsPayload(ref writer, actorTokenDescriptor, setDefaultTimesOnTokenCreation, tokenLifetimeInMinutes);
Copy link
Contributor

@brentschmaltz brentschmaltz Jul 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can simply this method by just passing the ClaimsIdentity, and the writing the claims into JSON.
Assume that only the claims in the ClaimsIdentity need to be added to the JSON.

The parameters to this method could be.

internal static void WriteActorToken(
            ref Utf8JsonWriter writer,
            string claimName,
            ClaimsIdentity claimsIdentity)

}
}
internal static void WriteActorToken(
Utf8JsonWriter writer,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is important to pass the writer using the 'ref' keyword.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will do this thank you for your review @brentschmaltz

@github-actions
Copy link

Summary

Summary
Generated on: 9/30/2025 - 10:34:18 PM
Coverage date: 9/30/2025 - 10:20:09 PM - 9/30/2025 - 10:33:27 PM
Parser: MultiReport (72x Cobertura)
Assemblies: 1
Classes: 10
Files: 3
Line coverage: 80.3% (931 of 1159)
Covered lines: 931
Uncovered lines: 228
Coverable lines: 1159
Total lines: 908
Branch coverage: 67.8% (342 of 504)
Covered branches: 342
Total branches: 504
Method coverage: Feature is only available for sponsors

Coverage

Microsoft.IdentityModel.JsonWebTokens - 80.3%
Name Line Branch
Microsoft.IdentityModel.JsonWebTokens 80.3% 67.8%
Microsoft.IdentityModel.JsonWebTokens.JwtTokenUtilities 100%
System.Text.RegularExpressions.Generated 80.3% 67.8%
System.Text.RegularExpressions.Generated 80.3% 67.8%
System.Text.RegularExpressions.Generated 80.3% 67.8%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F12A1AEDDDFE32BA
DF4DBFF323AF1BCB48B9F9721B7CD3E05F5E034CF225E3DF8__CreateJweRegex_1
79.2% 68%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F12A1AEDDDFE32BA
DF4DBFF323AF1BCB48B9F9721B7CD3E05F5E034CF225E3DF8__CreateJwsRegex_0
81.4% 67.6%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F334844C618E00D3
CEC5D3FE0D00CF0141BBEE98635313BB2CB8D3921464CE05A__CreateJweRegex_1
79.2% 68%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F334844C618E00D3
CEC5D3FE0D00CF0141BBEE98635313BB2CB8D3921464CE05A__CreateJwsRegex_0
81.4% 67.6%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>FE89C4BBA95BC726
BBFA730D496EDD9355321BE5DAEE79F3CE10E7FA17DC7FA64__CreateJweRegex_1
79.2% 68%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>FE89C4BBA95BC726
BBFA730D496EDD9355321BE5DAEE79F3CE10E7FA17DC7FA64__CreateJwsRegex_0
81.4% 67.6%

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] ClaimsIdentity Actor not serialized into JWTs

7 participants