Skip to content

Add agent identity extension methods for ClaimsPrincipal and ClaimsIdentity - #3515

Merged
Jean-Marc Prieur (jmprieur) merged 11 commits into
masterfrom
copilot/fix-2b1c96f6-8d41-4b94-900e-30503306567e
Oct 8, 2025
Merged

Add agent identity extension methods for ClaimsPrincipal and ClaimsIdentity#3515
Jean-Marc Prieur (jmprieur) merged 11 commits into
masterfrom
copilot/fix-2b1c96f6-8d41-4b94-900e-30503306567e

Conversation

CopilotAI commented Oct 2, 2025

Copy link
Copy Markdown
Contributor
  • Create AgentIdentityExtensions.cs in src/Microsoft.Identity.Web.AgentIdentities with extension methods (moved from TokenCache)
    • GetParentAgentBlueprint for ClaimsPrincipal
    • GetParentAgentBlueprint for ClaimsIdentity
    • IsAgentUserIdentity for ClaimsPrincipal
    • IsAgentUserIdentity for ClaimsIdentity
  • Create AgentIdentityExtensionsTests.cs in tests/Microsoft.Identity.Web.Test with comprehensive tests
    • Tests for GetParentAgentBlueprint (both Principal and Identity)
    • Tests for IsAgentUserIdentity with various scenarios
  • Update PublicAPI.Unshipped.txt files to document new public APIs in correct project
  • Add project reference to Microsoft.Identity.Web.AgentIdentities in test project
  • Build and test to ensure no regressions
  • Verify all tests pass (12 new tests + 627 existing tests = 639 total)
  • Extract magic number 13 into AgentIdUser constant
  • Rename ContainsFunctionCode to ContainsSubjectFacet and update comments
  • Add token validation using ClaimsIdentity extension methods in E2E tests
    • Extract JWT tokens from authorization headers in AgentUserIdentityTests
    • Parse tokens with JwtSecurityTokenHandler and create CaseSensitiveClaimsIdentity
    • Validate tokens using IsAgentUserIdentity() and GetParentAgentBlueprint() extension methods in all four test methods
  • Assert parent blueprint equals agent application in all E2E tests
    • Added assertions in AgentUserIdentityTests (4 test methods)
    • Added assertions in AutonomousAgentTests
Original prompt

Title: Add agent identity extension methods: GetParentAgentBlueprint and IsAgentUserIdentity

Summary
Add extension methods on ClaimsPrincipal and ClaimsIdentity to help developers detect agent identities and retrieve the parent agent blueprint from token claims.

Requirements

  1. Add the following extension methods in a new file under src/Microsoft.Identity.Web.TokenCache:

    • ClaimsPrincipal.GetParentAgentBlueprint(): retrieves the value of the xms_par_app_azp claim if it exists, returns null otherwise.
    • ClaimsIdentity.GetParentAgentBlueprint(): same as above for ClaimsIdentity.
    • ClaimsPrincipal.IsAgentUserIdentity(): returns true if the xms_sub_fct claim exists, is strictly a space-separated string of integers, every token is a valid integer, and the collection contains the integer 13; false otherwise.
    • ClaimsIdentity.IsAgentUserIdentity(): same as above for ClaimsIdentity.
  2. Add unit tests under tests/Microsoft.Identity.Web.Test to validate the above behaviors.

Notes

  • Follow project style used by existing ClaimsPrincipalExtensions.cs (namespace Microsoft.Identity.Web, XML doc comments, null checks).
  • Do not depend on internal helpers like Throws.IfNull or ClaimConstants; use public BCL APIs to keep the new code self-contained.
  • No changes expected to .csproj files.

Proposed changes

// Copyright (c) Microsoft Corporation.// Licensed under the MIT License.usingSystem;usingSystem.Globalization;usingSystem.Security.Claims;namespaceMicrosoft.Identity.Web{/// <summary>/// Extensions to read agent identity-related claims./// </summary>publicstaticclassAgentIdentityExtensions{/// <summary>/// Claim type for the parent agent blueprint./// </summary>privateconststringXmsParAppAzp="xms_par_app_azp";/// <summary>/// Claim type for subject function codes (space-separated integers)./// </summary>privateconststringXmsSubFct="xms_sub_fct";/// <summary>/// Retrieves the parent agent blueprint (xms_par_app_azp) value from a ClaimsPrincipal, if present./// </summary>/// <param name="claimsPrincipal">The claims principal.</param>/// <returns>The value of the xms_par_app_azp claim if it exists; otherwise, null.</returns>publicstaticstring?GetParentAgentBlueprint(thisClaimsPrincipalclaimsPrincipal){if(claimsPrincipalisnull){thrownewArgumentNullException(nameof(claimsPrincipal));}returnclaimsPrincipal.FindFirst(XmsParAppAzp)?.Value;}/// <summary>/// Retrieves the parent agent blueprint (xms_par_app_azp) value from a ClaimsIdentity, if present./// </summary>/// <param name="identity">The claims identity.</param>/// <returns>The value of the xms_par_app_azp claim if it exists; otherwise, null.</returns>publicstaticstring?GetParentAgentBlueprint(thisClaimsIdentityidentity){if(identityisnull){thrownewArgumentNullException(nameof(identity));}returnidentity.FindFirst(XmsParAppAzp)?.Value;}/// <summary>/// Determines whether the ClaimsPrincipal represents an agent user identity./// True if the xms_sub_fct claim exists, is a space-separated string of integers,/// and that collection contains the integer 13./// </summary>/// <param name="claimsPrincipal">The claims principal.</param>/// <returns>True if xms_sub_fct contains 13 and all tokens are integers; otherwise false.</returns>publicstaticboolIsAgentUserIdentity(thisClaimsPrincipalclaimsPrincipal){if(claimsPrincipalisnull){thrownewArgumentNullException(nameof(claimsPrincipal));}varvalue=claimsPrincipal.FindFirst(XmsSubFct)?.Value;returnContainsFunctionCode(value,13);}/// <summary>/// Determines whether the ClaimsIdentity represents an agent user identity./// True if the xms_sub_fct claim exists, is a space-separated string of integers,/// and that collection contains the integer 13./// </summary>/// <param name="identity">The claims identity.</param>/// <returns>True if xms_sub_fct contains 13 and all tokens are integers; otherwise false.</returns>publicstaticboolIsAgentUserIdentity(thisClaimsIdentityidentity){if(identityisnull){thrownewArgumentNullException(nameof(identity));}varvalue=identity.FindFirst(XmsSubFct)?.Value;returnContainsFunctionCode(value,13);}/// <summary>
...</details>*This pull request was created as a result of the following prompt from Copilot chat.*> Title: Add agent identity extension methods: GetParentAgentBlueprint and IsAgentUserIdentity
>> Summary
> Add extension methods on ClaimsPrincipal and ClaimsIdentity to help developers detect agent identities and retrieve the parent agent blueprint from token claims.>> Requirements
>1) Add the following extension methods in a new file under src/Microsoft.Identity.Web.TokenCache:>-ClaimsPrincipal.GetParentAgentBlueprint(): retrieves the value of the xms_par_app_azp claim if it exists, returns null otherwise.>-ClaimsIdentity.GetParentAgentBlueprint(): same as above for ClaimsIdentity.>-ClaimsPrincipal.IsAgentUserIdentity():returns true if the xms_sub_fct claim exists,is strictly a space-separated string of integers,every token is a valid integer,and the collection contains the integer 13; false otherwise.>-ClaimsIdentity.IsAgentUserIdentity():same as above for ClaimsIdentity.>>2)Add unit tests under tests/Microsoft.Identity.Web.Test to validate the above behaviors.>>Notes>- Follow project style used by existing ClaimsPrincipalExtensions.cs(namespaceMicrosoft.Identity.Web,XMLdoc comments,null checks).>- Do not depend on internal helpers like Throws.IfNull or ClaimConstants; use public BCL APIs to keep the new code self-contained.>- No changes expected to .csproj files.>> Proposed changes
>> ```csharp name=src/Microsoft.Identity.Web.TokenCache/AgentIdentityExtensions.cs
>// Copyright (c) Microsoft Corporation.>// Licensed under the MIT License.>>using System;>using System.Globalization;>using System.Security.Claims;>>namespace Microsoft.Identity.Web
>{>/// <summary>>/// Extensions to read agent identity-related claims.>/// </summary>>publicstaticclassAgentIdentityExtensions>{>/// <summary>>/// Claim type for the parent agent blueprint.>/// </summary>>privateconststringXmsParAppAzp="xms_par_app_azp";>>/// <summary>>/// Claim type for subject function codes (space-separated integers).>/// </summary>>privateconststringXmsSubFct="xms_sub_fct";>>/// <summary>>/// Retrieves the parent agent blueprint (xms_par_app_azp) value from a ClaimsPrincipal, if present.>/// </summary>>/// <param name="claimsPrincipal">The claims principal.</param>>/// <returns>The value of the xms_par_app_azp claim if it exists; otherwise, null.</returns>>publicstaticstring?GetParentAgentBlueprint(thisClaimsPrincipalclaimsPrincipal)>{>if(claimsPrincipalisnull)>{>thrownewArgumentNullException(nameof(claimsPrincipal));>}>>return claimsPrincipal.FindFirst(XmsParAppAzp)?.Value;>}>>/// <summary>>/// Retrieves the parent agent blueprint (xms_par_app_azp) value from a ClaimsIdentity, if present.>/// </summary>>/// <param name="identity">The claims identity.</param>>/// <returns>The value of the xms_par_app_azp claim if it exists; otherwise, null.</returns>>publicstatic string? GetParentAgentBlueprint(thisClaimsIdentityidentity)>{>if(identityisnull)>{>thrownewArgumentNullException(nameof(identity));>}>>returnidentity.FindFirst(XmsParAppAzp)?.Value;>}>>/// <summary>>/// Determines whether the ClaimsPrincipal represents an agent user identity.>/// True if the xms_sub_fct claim exists, is a space-separated string of integers,>/// and that collection contains the integer 13.>/// </summary>>/// <param name="claimsPrincipal">The claims principal.</param>>/// <returns>True if xms_sub_fct contains 13 and all tokens are integers; otherwise false.</returns>> public staticbool IsAgentUserIdentity(thisClaimsPrincipalclaimsPrincipal)>{>if(claimsPrincipalisnull)>{>thrownewArgumentNullException(nameof(claimsPrincipal));>}>>var value =claimsPrincipal.FindFirst(XmsSubFct)?.Value;>returnContainsFunctionCode(value,13);>}>>/// <summary>>/// Determines whether the ClaimsIdentity represents an agent user identity.>/// True if the xms_sub_fct claim exists, is a space-separated string of integers,>/// and that collection contains the integer 13.>/// </summary>>/// <param name="identity">The claims identity.</param>>/// <returns>True if xms_sub_fct contains 13 and all tokens are integers; otherwise false.</returns>> public staticboolIsAgentUserIdentity(thisClaimsIdentityidentity)>{>if(identity isnull)>{>thrownew ArgumentNullException(nameof(identity));>}>> var value = identity.FindFirst(XmsSubFct)?.Value;>return ContainsFunctionCode(value,13);>}>>/// <summary>>/// Parses a claim string representing a space-separated collection of integers and checks for a target code.>/// Returns true only if all tokens are valid integers and one equals the target code.>/// </summary>> private staticbool ContainsFunctionCode(string? raw,int targetCode)>{>if(string.IsNullOrWhiteSpace(raw))>{>return false;>}>> var tokens = raw.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);// split on whitespace>bool found = false;>>foreach(var token in tokens)>{>if(!int.TryParse(token, NumberStyles.Integer, CultureInfo.InvariantCulture,outint n))>{>// If any token is non-integer, the claim is not a valid collection of integers.>return false;>}>>if(n == targetCode)>{> found = true;>}>}>>return found;>}>}>}> ```
>> ```csharp name=tests/Microsoft.Identity.Web.Test/AgentIdentityExtensionsTests.cs
>// Copyright (c) Microsoft Corporation.>// Licensed under the MIT License.>>using System.Security.Claims;>using Microsoft.Identity.Web.Test.Common;>using Xunit;>>namespace Microsoft.Identity.Web.Test
>{> public class AgentIdentityExtensionsTests
>{> private const string ParentAgentBlueprintClaim = "xms_par_app_azp";> private const string ParentAgentBlueprintValue = "agent-blueprint-123";>> private const string SubjectFunctionClaim = "xms_sub_fct";>>[Fact]> public void GetParentAgentBlueprint_FromClaimsPrincipal_WithClaim_ReturnsValue()>{> var principal =new ClaimsPrincipal(>new CaseSensitiveClaimsIdentity(new[]>{>new Claim(ParentAgentBlueprintClaim, ParentAgentBlueprintValue),>}));>> Assert.Equal(ParentAgentBlueprintValue, principal.GetParentAgentBlueprint());>}>>[Fact]> public void GetParentAgentBlueprint_FromClaimsPrincipal_NoClaim_ReturnsNull()>{> var principal =new ClaimsPrincipal();> Assert.Null(principal.GetParentAgentBlueprint());>}>>[Fact]> public void GetParentAgentBlueprint_FromClaimsIdentity_WithClaim_ReturnsValue()>{> var identity =new CaseSensitiveClaimsIdentity(new[]>{>new Claim(ParentAgentBlueprintClaim, ParentAgentBlueprintValue),>});>> Assert.Equal(ParentAgentBlueprintValue, identity.GetParentAgentBlueprint());>}>>[Fact]> public void GetParentAgentBlueprint_FromClaimsIdentity_NoClaim_ReturnsNull()>{> var identity =new CaseSensitiveClaimsIdentity();> Assert.Null(identity.GetParentAgentBlueprint());>}>>[Fact]> public void IsAgentUserIdentity_Principal_SpaceSeparated_Contains13_ReturnsTrue()>{> var principal =new ClaimsPrincipal(>new CaseSensitiveClaimsIdentity(new[]>{>new Claim(SubjectFunctionClaim, "121315"),>}));>> Assert.True(principal.IsAgentUserIdentity());>}>>[Fact]> public void IsAgentUserIdentity_Principal_SpaceSeparated_No13_ReturnsFalse()>{> var principal =new ClaimsPrincipal(>new CaseSensitiveClaimsIdentity(new[]>{>new Claim(SubjectFunctionClaim, "1234"),>}));>> Assert.False(principal.IsAgentUserIdentity());>}>>[Fact]> public void IsAgentUserIdentity_Principal_ExtraWhitespace_Contains13_ReturnsTrue()>{> var principal =new ClaimsPrincipal(>new CaseSensitiveClaimsIdentity(new[]>{>new Claim(SubjectFunctionClaim, " 91321 "),>}));>> Assert.True(principal.IsAgentUserIdentity());>}>>[Fact]> public void IsAgentUserIdentity_Principal_InvalidToken_ReturnsFalse()>{> var principal =new ClaimsPrincipal(>new CaseSensitiveClaimsIdentity(new[]>{>new Claim(SubjectFunctionClaim, "12 a 13"),>}));>> Assert.False(principal.IsAgentUserIdentity());>}>>[Fact]> public void IsAgentUserIdentity_Principal_NoClaim_ReturnsFalse()>{> var principal =new ClaimsPrincipal();> Assert.False(principal.IsAgentUserIdentity());>}>>[Fact]> public void IsAgentUserIdentity_Identity_SpaceSeparated_Contains13_ReturnsTrue()>{> var identity =new CaseSensitiveClaimsIdentity(new[]>{>new Claim(SubjectFunctionClaim, "781321"),>});>> Assert.True(identity.IsAgentUserIdentity());>}>>[Fact]> public void IsAgentUserIdentity_Identity_EmptyString_ReturnsFalse()>{> var identity =new CaseSensitiveClaimsIdentity(new[]>{>new Claim(SubjectFunctionClaim, " "),>});>> Assert.False(identity.IsAgentUserIdentity());>}>>[Fact]> public void IsAgentUserIdentity_Identity_NoClaim_ReturnsFalse()>{> var identity =new CaseSensitiveClaimsIdentity();> Assert.False(identity.IsAgentUserIdentity());>}>}>}> ```
>> Build/test guidance
>- Build the solution.>- Run the test suite; the new tests are in AgentIdentityExtensionsTests.>> Acceptance criteria
>- All new tests pass.>- No regressions in existing tests.>- New methods are available for ClaimsPrincipal and ClaimsIdentity as specified.><!-- START COPILOT CODING AGENT TIPS -->---
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more [Copilot coding agent tips](https://gh.io/copilot-coding-agent-tips) in the docs.

Co-authored-by: jmprieur <13203188+jmprieur@users.noreply.github.com>
CopilotAI changed the title [WIP] Add agent identity extension methods and testsAdd agent identity extension methods for ClaimsPrincipal and ClaimsIdentityOct 2, 2025
…s project
Co-authored-by: jmprieur <13203188+jmprieur@users.noreply.github.com>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM

@jmprieur
Jean-Marc Prieur (jmprieur) marked this pull request as ready for review October 2, 2025 03:09
Comment threadsrc/Microsoft.Identity.Web.AgentIdentities/AgentIdentityExtensions.cs Outdated
Comment threadsrc/Microsoft.Identity.Web.AgentIdentities/AgentIdentityExtensions.cs Outdated
Co-authored-by: keegan-caruso <54109988+keegan-caruso@users.noreply.github.com>
@jmprieur

Copy link
Copy Markdown
Collaborator

Copilot : let's add other asserts using the ClaimsIdentity extension methods in the Agent identity end to end tests under tests/E2E%20Tests/AgentApplications

When the test gets the authorization header, extract the token, read it with JsonWebTokenHandler, and create an untrusted claims identity from the claims in the token.

Co-authored-by: jmprieur <13203188+jmprieur@users.noreply.github.com>

CopilotAI commented Oct 2, 2025

Copy link
Copy Markdown
ContributorAuthor

Copilot : let's add other asserts using the ClaimsIdentity extension methods in the Agent identity end to end tests under tests/E2E%20Tests/AgentApplications

When the test gets the authorization...

Added assertions in all four AgentUserIdentityTests methods (722ac3b). Each test now extracts the token from the authorization header, parses it with JwtSecurityTokenHandler, creates a CaseSensitiveClaimsIdentity from the claims, and uses IsAgentUserIdentity() and GetParentAgentBlueprint() extension methods to validate the token.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Copilot : this is great, I left comments.,
Could we also assert the 2 extension methods on ClaimsIndentity in Autonomous agent tests as well?

Comment threadtests/E2E Tests/AgentApplications/AgentUserIdentityTestscs.cs Outdated
Comment threadtests/E2E Tests/AgentApplications/AgentUserIdentityTestscs.cs Outdated
Comment threadtests/E2E Tests/AgentApplications/AgentUserIdentityTestscs.cs Outdated
Comment threadtests/E2E Tests/AgentApplications/AgentUserIdentityTestscs.cs Outdated
Sign up for freeto 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.

4 participants

@jmprieur@keegan-caruso@saurabhsathe-ms
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Add agent identity extension methods for ClaimsPrincipal and ClaimsIdentity by Copilot · Pull Request #3515 · AzureAD/microsoft-identity-web · GitHub
Skip to content

Add agent identity extension methods for ClaimsPrincipal and ClaimsIdentity - #3515

Merged
Jean-Marc Prieur (jmprieur) merged 11 commits into
masterfrom
copilot/fix-2b1c96f6-8d41-4b94-900e-30503306567e
Oct 8, 2025
Merged

Add agent identity extension methods for ClaimsPrincipal and ClaimsIdentity#3515
Jean-Marc Prieur (jmprieur) merged 11 commits into
masterfrom
copilot/fix-2b1c96f6-8d41-4b94-900e-30503306567e

Conversation

CopilotAI commented Oct 2, 2025

Copy link
Copy Markdown
Contributor
  • Create AgentIdentityExtensions.cs in src/Microsoft.Identity.Web.AgentIdentities with extension methods (moved from TokenCache)
    • GetParentAgentBlueprint for ClaimsPrincipal
    • GetParentAgentBlueprint for ClaimsIdentity
    • IsAgentUserIdentity for ClaimsPrincipal
    • IsAgentUserIdentity for ClaimsIdentity
  • Create AgentIdentityExtensionsTests.cs in tests/Microsoft.Identity.Web.Test with comprehensive tests
    • Tests for GetParentAgentBlueprint (both Principal and Identity)
    • Tests for IsAgentUserIdentity with various scenarios
  • Update PublicAPI.Unshipped.txt files to document new public APIs in correct project
  • Add project reference to Microsoft.Identity.Web.AgentIdentities in test project
  • Build and test to ensure no regressions
  • Verify all tests pass (12 new tests + 627 existing tests = 639 total)
  • Extract magic number 13 into AgentIdUser constant
  • Rename ContainsFunctionCode to ContainsSubjectFacet and update comments
  • Add token validation using ClaimsIdentity extension methods in E2E tests
    • Extract JWT tokens from authorization headers in AgentUserIdentityTests
    • Parse tokens with JwtSecurityTokenHandler and create CaseSensitiveClaimsIdentity
    • Validate tokens using IsAgentUserIdentity() and GetParentAgentBlueprint() extension methods in all four test methods
  • Assert parent blueprint equals agent application in all E2E tests
    • Added assertions in AgentUserIdentityTests (4 test methods)
    • Added assertions in AutonomousAgentTests
Original prompt

Title: Add agent identity extension methods: GetParentAgentBlueprint and IsAgentUserIdentity

Summary
Add extension methods on ClaimsPrincipal and ClaimsIdentity to help developers detect agent identities and retrieve the parent agent blueprint from token claims.

Requirements

  1. Add the following extension methods in a new file under src/Microsoft.Identity.Web.TokenCache:

    • ClaimsPrincipal.GetParentAgentBlueprint(): retrieves the value of the xms_par_app_azp claim if it exists, returns null otherwise.
    • ClaimsIdentity.GetParentAgentBlueprint(): same as above for ClaimsIdentity.
    • ClaimsPrincipal.IsAgentUserIdentity(): returns true if the xms_sub_fct claim exists, is strictly a space-separated string of integers, every token is a valid integer, and the collection contains the integer 13; false otherwise.
    • ClaimsIdentity.IsAgentUserIdentity(): same as above for ClaimsIdentity.
  2. Add unit tests under tests/Microsoft.Identity.Web.Test to validate the above behaviors.

Notes

  • Follow project style used by existing ClaimsPrincipalExtensions.cs (namespace Microsoft.Identity.Web, XML doc comments, null checks).
  • Do not depend on internal helpers like Throws.IfNull or ClaimConstants; use public BCL APIs to keep the new code self-contained.
  • No changes expected to .csproj files.

Proposed changes

// Copyright (c) Microsoft Corporation.// Licensed under the MIT License.usingSystem;usingSystem.Globalization;usingSystem.Security.Claims;namespaceMicrosoft.Identity.Web{/// <summary>/// Extensions to read agent identity-related claims./// </summary>publicstaticclassAgentIdentityExtensions{/// <summary>/// Claim type for the parent agent blueprint./// </summary>privateconststringXmsParAppAzp="xms_par_app_azp";/// <summary>/// Claim type for subject function codes (space-separated integers)./// </summary>privateconststringXmsSubFct="xms_sub_fct";/// <summary>/// Retrieves the parent agent blueprint (xms_par_app_azp) value from a ClaimsPrincipal, if present./// </summary>/// <param name="claimsPrincipal">The claims principal.</param>/// <returns>The value of the xms_par_app_azp claim if it exists; otherwise, null.</returns>publicstaticstring?GetParentAgentBlueprint(thisClaimsPrincipalclaimsPrincipal){if(claimsPrincipalisnull){thrownewArgumentNullException(nameof(claimsPrincipal));}returnclaimsPrincipal.FindFirst(XmsParAppAzp)?.Value;}/// <summary>/// Retrieves the parent agent blueprint (xms_par_app_azp) value from a ClaimsIdentity, if present./// </summary>/// <param name="identity">The claims identity.</param>/// <returns>The value of the xms_par_app_azp claim if it exists; otherwise, null.</returns>publicstaticstring?GetParentAgentBlueprint(thisClaimsIdentityidentity){if(identityisnull){thrownewArgumentNullException(nameof(identity));}returnidentity.FindFirst(XmsParAppAzp)?.Value;}/// <summary>/// Determines whether the ClaimsPrincipal represents an agent user identity./// True if the xms_sub_fct claim exists, is a space-separated string of integers,/// and that collection contains the integer 13./// </summary>/// <param name="claimsPrincipal">The claims principal.</param>/// <returns>True if xms_sub_fct contains 13 and all tokens are integers; otherwise false.</returns>publicstaticboolIsAgentUserIdentity(thisClaimsPrincipalclaimsPrincipal){if(claimsPrincipalisnull){thrownewArgumentNullException(nameof(claimsPrincipal));}varvalue=claimsPrincipal.FindFirst(XmsSubFct)?.Value;returnContainsFunctionCode(value,13);}/// <summary>/// Determines whether the ClaimsIdentity represents an agent user identity./// True if the xms_sub_fct claim exists, is a space-separated string of integers,/// and that collection contains the integer 13./// </summary>/// <param name="identity">The claims identity.</param>/// <returns>True if xms_sub_fct contains 13 and all tokens are integers; otherwise false.</returns>publicstaticboolIsAgentUserIdentity(thisClaimsIdentityidentity){if(identityisnull){thrownewArgumentNullException(nameof(identity));}varvalue=identity.FindFirst(XmsSubFct)?.Value;returnContainsFunctionCode(value,13);}/// <summary>
...</details>*This pull request was created as a result of the following prompt from Copilot chat.*> Title: Add agent identity extension methods: GetParentAgentBlueprint and IsAgentUserIdentity
>> Summary
> Add extension methods on ClaimsPrincipal and ClaimsIdentity to help developers detect agent identities and retrieve the parent agent blueprint from token claims.>> Requirements
>1) Add the following extension methods in a new file under src/Microsoft.Identity.Web.TokenCache:>-ClaimsPrincipal.GetParentAgentBlueprint(): retrieves the value of the xms_par_app_azp claim if it exists, returns null otherwise.>-ClaimsIdentity.GetParentAgentBlueprint(): same as above for ClaimsIdentity.>-ClaimsPrincipal.IsAgentUserIdentity():returns true if the xms_sub_fct claim exists,is strictly a space-separated string of integers,every token is a valid integer,and the collection contains the integer 13; false otherwise.>-ClaimsIdentity.IsAgentUserIdentity():same as above for ClaimsIdentity.>>2)Add unit tests under tests/Microsoft.Identity.Web.Test to validate the above behaviors.>>Notes>- Follow project style used by existing ClaimsPrincipalExtensions.cs(namespaceMicrosoft.Identity.Web,XMLdoc comments,null checks).>- Do not depend on internal helpers like Throws.IfNull or ClaimConstants; use public BCL APIs to keep the new code self-contained.>- No changes expected to .csproj files.>> Proposed changes
>> ```csharp name=src/Microsoft.Identity.Web.TokenCache/AgentIdentityExtensions.cs
>// Copyright (c) Microsoft Corporation.>// Licensed under the MIT License.>>using System;>using System.Globalization;>using System.Security.Claims;>>namespace Microsoft.Identity.Web
>{>/// <summary>>/// Extensions to read agent identity-related claims.>/// </summary>>publicstaticclassAgentIdentityExtensions>{>/// <summary>>/// Claim type for the parent agent blueprint.>/// </summary>>privateconststringXmsParAppAzp="xms_par_app_azp";>>/// <summary>>/// Claim type for subject function codes (space-separated integers).>/// </summary>>privateconststringXmsSubFct="xms_sub_fct";>>/// <summary>>/// Retrieves the parent agent blueprint (xms_par_app_azp) value from a ClaimsPrincipal, if present.>/// </summary>>/// <param name="claimsPrincipal">The claims principal.</param>>/// <returns>The value of the xms_par_app_azp claim if it exists; otherwise, null.</returns>>publicstaticstring?GetParentAgentBlueprint(thisClaimsPrincipalclaimsPrincipal)>{>if(claimsPrincipalisnull)>{>thrownewArgumentNullException(nameof(claimsPrincipal));>}>>return claimsPrincipal.FindFirst(XmsParAppAzp)?.Value;>}>>/// <summary>>/// Retrieves the parent agent blueprint (xms_par_app_azp) value from a ClaimsIdentity, if present.>/// </summary>>/// <param name="identity">The claims identity.</param>>/// <returns>The value of the xms_par_app_azp claim if it exists; otherwise, null.</returns>>publicstatic string? GetParentAgentBlueprint(thisClaimsIdentityidentity)>{>if(identityisnull)>{>thrownewArgumentNullException(nameof(identity));>}>>returnidentity.FindFirst(XmsParAppAzp)?.Value;>}>>/// <summary>>/// Determines whether the ClaimsPrincipal represents an agent user identity.>/// True if the xms_sub_fct claim exists, is a space-separated string of integers,>/// and that collection contains the integer 13.>/// </summary>>/// <param name="claimsPrincipal">The claims principal.</param>>/// <returns>True if xms_sub_fct contains 13 and all tokens are integers; otherwise false.</returns>> public staticbool IsAgentUserIdentity(thisClaimsPrincipalclaimsPrincipal)>{>if(claimsPrincipalisnull)>{>thrownewArgumentNullException(nameof(claimsPrincipal));>}>>var value =claimsPrincipal.FindFirst(XmsSubFct)?.Value;>returnContainsFunctionCode(value,13);>}>>/// <summary>>/// Determines whether the ClaimsIdentity represents an agent user identity.>/// True if the xms_sub_fct claim exists, is a space-separated string of integers,>/// and that collection contains the integer 13.>/// </summary>>/// <param name="identity">The claims identity.</param>>/// <returns>True if xms_sub_fct contains 13 and all tokens are integers; otherwise false.</returns>> public staticboolIsAgentUserIdentity(thisClaimsIdentityidentity)>{>if(identity isnull)>{>thrownew ArgumentNullException(nameof(identity));>}>> var value = identity.FindFirst(XmsSubFct)?.Value;>return ContainsFunctionCode(value,13);>}>>/// <summary>>/// Parses a claim string representing a space-separated collection of integers and checks for a target code.>/// Returns true only if all tokens are valid integers and one equals the target code.>/// </summary>> private staticbool ContainsFunctionCode(string? raw,int targetCode)>{>if(string.IsNullOrWhiteSpace(raw))>{>return false;>}>> var tokens = raw.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);// split on whitespace>bool found = false;>>foreach(var token in tokens)>{>if(!int.TryParse(token, NumberStyles.Integer, CultureInfo.InvariantCulture,outint n))>{>// If any token is non-integer, the claim is not a valid collection of integers.>return false;>}>>if(n == targetCode)>{> found = true;>}>}>>return found;>}>}>}> ```
>> ```csharp name=tests/Microsoft.Identity.Web.Test/AgentIdentityExtensionsTests.cs
>// Copyright (c) Microsoft Corporation.>// Licensed under the MIT License.>>using System.Security.Claims;>using Microsoft.Identity.Web.Test.Common;>using Xunit;>>namespace Microsoft.Identity.Web.Test
>{> public class AgentIdentityExtensionsTests
>{> private const string ParentAgentBlueprintClaim = "xms_par_app_azp";> private const string ParentAgentBlueprintValue = "agent-blueprint-123";>> private const string SubjectFunctionClaim = "xms_sub_fct";>>[Fact]> public void GetParentAgentBlueprint_FromClaimsPrincipal_WithClaim_ReturnsValue()>{> var principal =new ClaimsPrincipal(>new CaseSensitiveClaimsIdentity(new[]>{>new Claim(ParentAgentBlueprintClaim, ParentAgentBlueprintValue),>}));>> Assert.Equal(ParentAgentBlueprintValue, principal.GetParentAgentBlueprint());>}>>[Fact]> public void GetParentAgentBlueprint_FromClaimsPrincipal_NoClaim_ReturnsNull()>{> var principal =new ClaimsPrincipal();> Assert.Null(principal.GetParentAgentBlueprint());>}>>[Fact]> public void GetParentAgentBlueprint_FromClaimsIdentity_WithClaim_ReturnsValue()>{> var identity =new CaseSensitiveClaimsIdentity(new[]>{>new Claim(ParentAgentBlueprintClaim, ParentAgentBlueprintValue),>});>> Assert.Equal(ParentAgentBlueprintValue, identity.GetParentAgentBlueprint());>}>>[Fact]> public void GetParentAgentBlueprint_FromClaimsIdentity_NoClaim_ReturnsNull()>{> var identity =new CaseSensitiveClaimsIdentity();> Assert.Null(identity.GetParentAgentBlueprint());>}>>[Fact]> public void IsAgentUserIdentity_Principal_SpaceSeparated_Contains13_ReturnsTrue()>{> var principal =new ClaimsPrincipal(>new CaseSensitiveClaimsIdentity(new[]>{>new Claim(SubjectFunctionClaim, "121315"),>}));>> Assert.True(principal.IsAgentUserIdentity());>}>>[Fact]> public void IsAgentUserIdentity_Principal_SpaceSeparated_No13_ReturnsFalse()>{> var principal =new ClaimsPrincipal(>new CaseSensitiveClaimsIdentity(new[]>{>new Claim(SubjectFunctionClaim, "1234"),>}));>> Assert.False(principal.IsAgentUserIdentity());>}>>[Fact]> public void IsAgentUserIdentity_Principal_ExtraWhitespace_Contains13_ReturnsTrue()>{> var principal =new ClaimsPrincipal(>new CaseSensitiveClaimsIdentity(new[]>{>new Claim(SubjectFunctionClaim, " 91321 "),>}));>> Assert.True(principal.IsAgentUserIdentity());>}>>[Fact]> public void IsAgentUserIdentity_Principal_InvalidToken_ReturnsFalse()>{> var principal =new ClaimsPrincipal(>new CaseSensitiveClaimsIdentity(new[]>{>new Claim(SubjectFunctionClaim, "12 a 13"),>}));>> Assert.False(principal.IsAgentUserIdentity());>}>>[Fact]> public void IsAgentUserIdentity_Principal_NoClaim_ReturnsFalse()>{> var principal =new ClaimsPrincipal();> Assert.False(principal.IsAgentUserIdentity());>}>>[Fact]> public void IsAgentUserIdentity_Identity_SpaceSeparated_Contains13_ReturnsTrue()>{> var identity =new CaseSensitiveClaimsIdentity(new[]>{>new Claim(SubjectFunctionClaim, "781321"),>});>> Assert.True(identity.IsAgentUserIdentity());>}>>[Fact]> public void IsAgentUserIdentity_Identity_EmptyString_ReturnsFalse()>{> var identity =new CaseSensitiveClaimsIdentity(new[]>{>new Claim(SubjectFunctionClaim, " "),>});>> Assert.False(identity.IsAgentUserIdentity());>}>>[Fact]> public void IsAgentUserIdentity_Identity_NoClaim_ReturnsFalse()>{> var identity =new CaseSensitiveClaimsIdentity();> Assert.False(identity.IsAgentUserIdentity());>}>}>}> ```
>> Build/test guidance
>- Build the solution.>- Run the test suite; the new tests are in AgentIdentityExtensionsTests.>> Acceptance criteria
>- All new tests pass.>- No regressions in existing tests.>- New methods are available for ClaimsPrincipal and ClaimsIdentity as specified.><!-- START COPILOT CODING AGENT TIPS -->---
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more [Copilot coding agent tips](https://gh.io/copilot-coding-agent-tips) in the docs.

Co-authored-by: jmprieur <13203188+jmprieur@users.noreply.github.com>
CopilotAI changed the title [WIP] Add agent identity extension methods and testsAdd agent identity extension methods for ClaimsPrincipal and ClaimsIdentityOct 2, 2025
…s project
Co-authored-by: jmprieur <13203188+jmprieur@users.noreply.github.com>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM

@jmprieur
Jean-Marc Prieur (jmprieur) marked this pull request as ready for review October 2, 2025 03:09
Comment threadsrc/Microsoft.Identity.Web.AgentIdentities/AgentIdentityExtensions.cs Outdated
Comment threadsrc/Microsoft.Identity.Web.AgentIdentities/AgentIdentityExtensions.cs Outdated
Co-authored-by: keegan-caruso <54109988+keegan-caruso@users.noreply.github.com>
@jmprieur

Copy link
Copy Markdown
Collaborator

Copilot : let's add other asserts using the ClaimsIdentity extension methods in the Agent identity end to end tests under tests/E2E%20Tests/AgentApplications

When the test gets the authorization header, extract the token, read it with JsonWebTokenHandler, and create an untrusted claims identity from the claims in the token.

Co-authored-by: jmprieur <13203188+jmprieur@users.noreply.github.com>

CopilotAI commented Oct 2, 2025

Copy link
Copy Markdown
ContributorAuthor

Copilot : let's add other asserts using the ClaimsIdentity extension methods in the Agent identity end to end tests under tests/E2E%20Tests/AgentApplications

When the test gets the authorization...

Added assertions in all four AgentUserIdentityTests methods (722ac3b). Each test now extracts the token from the authorization header, parses it with JwtSecurityTokenHandler, creates a CaseSensitiveClaimsIdentity from the claims, and uses IsAgentUserIdentity() and GetParentAgentBlueprint() extension methods to validate the token.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Copilot : this is great, I left comments.,
Could we also assert the 2 extension methods on ClaimsIndentity in Autonomous agent tests as well?

Comment threadtests/E2E Tests/AgentApplications/AgentUserIdentityTestscs.cs Outdated
Comment threadtests/E2E Tests/AgentApplications/AgentUserIdentityTestscs.cs Outdated
Comment threadtests/E2E Tests/AgentApplications/AgentUserIdentityTestscs.cs Outdated
Comment threadtests/E2E Tests/AgentApplications/AgentUserIdentityTestscs.cs Outdated
Sign up for freeto 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.

4 participants

@jmprieur@keegan-caruso@saurabhsathe-ms
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Add agent identity extension methods for ClaimsPrincipal and ClaimsIdentity by Copilot · Pull Request #3515 · AzureAD/microsoft-identity-web · GitHub
Skip to content

Add agent identity extension methods for ClaimsPrincipal and ClaimsIdentity - #3515

Merged
Jean-Marc Prieur (jmprieur) merged 11 commits into
masterfrom
copilot/fix-2b1c96f6-8d41-4b94-900e-30503306567e
Oct 8, 2025
Merged

Add agent identity extension methods for ClaimsPrincipal and ClaimsIdentity#3515
Jean-Marc Prieur (jmprieur) merged 11 commits into
masterfrom
copilot/fix-2b1c96f6-8d41-4b94-900e-30503306567e

Conversation

CopilotAI commented Oct 2, 2025

Copy link
Copy Markdown
Contributor
  • Create AgentIdentityExtensions.cs in src/Microsoft.Identity.Web.AgentIdentities with extension methods (moved from TokenCache)
    • GetParentAgentBlueprint for ClaimsPrincipal
    • GetParentAgentBlueprint for ClaimsIdentity
    • IsAgentUserIdentity for ClaimsPrincipal
    • IsAgentUserIdentity for ClaimsIdentity
  • Create AgentIdentityExtensionsTests.cs in tests/Microsoft.Identity.Web.Test with comprehensive tests
    • Tests for GetParentAgentBlueprint (both Principal and Identity)
    • Tests for IsAgentUserIdentity with various scenarios
  • Update PublicAPI.Unshipped.txt files to document new public APIs in correct project
  • Add project reference to Microsoft.Identity.Web.AgentIdentities in test project
  • Build and test to ensure no regressions
  • Verify all tests pass (12 new tests + 627 existing tests = 639 total)
  • Extract magic number 13 into AgentIdUser constant
  • Rename ContainsFunctionCode to ContainsSubjectFacet and update comments
  • Add token validation using ClaimsIdentity extension methods in E2E tests
    • Extract JWT tokens from authorization headers in AgentUserIdentityTests
    • Parse tokens with JwtSecurityTokenHandler and create CaseSensitiveClaimsIdentity
    • Validate tokens using IsAgentUserIdentity() and GetParentAgentBlueprint() extension methods in all four test methods
  • Assert parent blueprint equals agent application in all E2E tests
    • Added assertions in AgentUserIdentityTests (4 test methods)
    • Added assertions in AutonomousAgentTests
Original prompt

Title: Add agent identity extension methods: GetParentAgentBlueprint and IsAgentUserIdentity

Summary
Add extension methods on ClaimsPrincipal and ClaimsIdentity to help developers detect agent identities and retrieve the parent agent blueprint from token claims.

Requirements

  1. Add the following extension methods in a new file under src/Microsoft.Identity.Web.TokenCache:

    • ClaimsPrincipal.GetParentAgentBlueprint(): retrieves the value of the xms_par_app_azp claim if it exists, returns null otherwise.
    • ClaimsIdentity.GetParentAgentBlueprint(): same as above for ClaimsIdentity.
    • ClaimsPrincipal.IsAgentUserIdentity(): returns true if the xms_sub_fct claim exists, is strictly a space-separated string of integers, every token is a valid integer, and the collection contains the integer 13; false otherwise.
    • ClaimsIdentity.IsAgentUserIdentity(): same as above for ClaimsIdentity.
  2. Add unit tests under tests/Microsoft.Identity.Web.Test to validate the above behaviors.

Notes

  • Follow project style used by existing ClaimsPrincipalExtensions.cs (namespace Microsoft.Identity.Web, XML doc comments, null checks).
  • Do not depend on internal helpers like Throws.IfNull or ClaimConstants; use public BCL APIs to keep the new code self-contained.
  • No changes expected to .csproj files.

Proposed changes

// Copyright (c) Microsoft Corporation.// Licensed under the MIT License.usingSystem;usingSystem.Globalization;usingSystem.Security.Claims;namespaceMicrosoft.Identity.Web{/// <summary>/// Extensions to read agent identity-related claims./// </summary>publicstaticclassAgentIdentityExtensions{/// <summary>/// Claim type for the parent agent blueprint./// </summary>privateconststringXmsParAppAzp="xms_par_app_azp";/// <summary>/// Claim type for subject function codes (space-separated integers)./// </summary>privateconststringXmsSubFct="xms_sub_fct";/// <summary>/// Retrieves the parent agent blueprint (xms_par_app_azp) value from a ClaimsPrincipal, if present./// </summary>/// <param name="claimsPrincipal">The claims principal.</param>/// <returns>The value of the xms_par_app_azp claim if it exists; otherwise, null.</returns>publicstaticstring?GetParentAgentBlueprint(thisClaimsPrincipalclaimsPrincipal){if(claimsPrincipalisnull){thrownewArgumentNullException(nameof(claimsPrincipal));}returnclaimsPrincipal.FindFirst(XmsParAppAzp)?.Value;}/// <summary>/// Retrieves the parent agent blueprint (xms_par_app_azp) value from a ClaimsIdentity, if present./// </summary>/// <param name="identity">The claims identity.</param>/// <returns>The value of the xms_par_app_azp claim if it exists; otherwise, null.</returns>publicstaticstring?GetParentAgentBlueprint(thisClaimsIdentityidentity){if(identityisnull){thrownewArgumentNullException(nameof(identity));}returnidentity.FindFirst(XmsParAppAzp)?.Value;}/// <summary>/// Determines whether the ClaimsPrincipal represents an agent user identity./// True if the xms_sub_fct claim exists, is a space-separated string of integers,/// and that collection contains the integer 13./// </summary>/// <param name="claimsPrincipal">The claims principal.</param>/// <returns>True if xms_sub_fct contains 13 and all tokens are integers; otherwise false.</returns>publicstaticboolIsAgentUserIdentity(thisClaimsPrincipalclaimsPrincipal){if(claimsPrincipalisnull){thrownewArgumentNullException(nameof(claimsPrincipal));}varvalue=claimsPrincipal.FindFirst(XmsSubFct)?.Value;returnContainsFunctionCode(value,13);}/// <summary>/// Determines whether the ClaimsIdentity represents an agent user identity./// True if the xms_sub_fct claim exists, is a space-separated string of integers,/// and that collection contains the integer 13./// </summary>/// <param name="identity">The claims identity.</param>/// <returns>True if xms_sub_fct contains 13 and all tokens are integers; otherwise false.</returns>publicstaticboolIsAgentUserIdentity(thisClaimsIdentityidentity){if(identityisnull){thrownewArgumentNullException(nameof(identity));}varvalue=identity.FindFirst(XmsSubFct)?.Value;returnContainsFunctionCode(value,13);}/// <summary>
...</details>*This pull request was created as a result of the following prompt from Copilot chat.*> Title: Add agent identity extension methods: GetParentAgentBlueprint and IsAgentUserIdentity
>> Summary
> Add extension methods on ClaimsPrincipal and ClaimsIdentity to help developers detect agent identities and retrieve the parent agent blueprint from token claims.>> Requirements
>1) Add the following extension methods in a new file under src/Microsoft.Identity.Web.TokenCache:>-ClaimsPrincipal.GetParentAgentBlueprint(): retrieves the value of the xms_par_app_azp claim if it exists, returns null otherwise.>-ClaimsIdentity.GetParentAgentBlueprint(): same as above for ClaimsIdentity.>-ClaimsPrincipal.IsAgentUserIdentity():returns true if the xms_sub_fct claim exists,is strictly a space-separated string of integers,every token is a valid integer,and the collection contains the integer 13; false otherwise.>-ClaimsIdentity.IsAgentUserIdentity():same as above for ClaimsIdentity.>>2)Add unit tests under tests/Microsoft.Identity.Web.Test to validate the above behaviors.>>Notes>- Follow project style used by existing ClaimsPrincipalExtensions.cs(namespaceMicrosoft.Identity.Web,XMLdoc comments,null checks).>- Do not depend on internal helpers like Throws.IfNull or ClaimConstants; use public BCL APIs to keep the new code self-contained.>- No changes expected to .csproj files.>> Proposed changes
>> ```csharp name=src/Microsoft.Identity.Web.TokenCache/AgentIdentityExtensions.cs
>// Copyright (c) Microsoft Corporation.>// Licensed under the MIT License.>>using System;>using System.Globalization;>using System.Security.Claims;>>namespace Microsoft.Identity.Web
>{>/// <summary>>/// Extensions to read agent identity-related claims.>/// </summary>>publicstaticclassAgentIdentityExtensions>{>/// <summary>>/// Claim type for the parent agent blueprint.>/// </summary>>privateconststringXmsParAppAzp="xms_par_app_azp";>>/// <summary>>/// Claim type for subject function codes (space-separated integers).>/// </summary>>privateconststringXmsSubFct="xms_sub_fct";>>/// <summary>>/// Retrieves the parent agent blueprint (xms_par_app_azp) value from a ClaimsPrincipal, if present.>/// </summary>>/// <param name="claimsPrincipal">The claims principal.</param>>/// <returns>The value of the xms_par_app_azp claim if it exists; otherwise, null.</returns>>publicstaticstring?GetParentAgentBlueprint(thisClaimsPrincipalclaimsPrincipal)>{>if(claimsPrincipalisnull)>{>thrownewArgumentNullException(nameof(claimsPrincipal));>}>>return claimsPrincipal.FindFirst(XmsParAppAzp)?.Value;>}>>/// <summary>>/// Retrieves the parent agent blueprint (xms_par_app_azp) value from a ClaimsIdentity, if present.>/// </summary>>/// <param name="identity">The claims identity.</param>>/// <returns>The value of the xms_par_app_azp claim if it exists; otherwise, null.</returns>>publicstatic string? GetParentAgentBlueprint(thisClaimsIdentityidentity)>{>if(identityisnull)>{>thrownewArgumentNullException(nameof(identity));>}>>returnidentity.FindFirst(XmsParAppAzp)?.Value;>}>>/// <summary>>/// Determines whether the ClaimsPrincipal represents an agent user identity.>/// True if the xms_sub_fct claim exists, is a space-separated string of integers,>/// and that collection contains the integer 13.>/// </summary>>/// <param name="claimsPrincipal">The claims principal.</param>>/// <returns>True if xms_sub_fct contains 13 and all tokens are integers; otherwise false.</returns>> public staticbool IsAgentUserIdentity(thisClaimsPrincipalclaimsPrincipal)>{>if(claimsPrincipalisnull)>{>thrownewArgumentNullException(nameof(claimsPrincipal));>}>>var value =claimsPrincipal.FindFirst(XmsSubFct)?.Value;>returnContainsFunctionCode(value,13);>}>>/// <summary>>/// Determines whether the ClaimsIdentity represents an agent user identity.>/// True if the xms_sub_fct claim exists, is a space-separated string of integers,>/// and that collection contains the integer 13.>/// </summary>>/// <param name="identity">The claims identity.</param>>/// <returns>True if xms_sub_fct contains 13 and all tokens are integers; otherwise false.</returns>> public staticboolIsAgentUserIdentity(thisClaimsIdentityidentity)>{>if(identity isnull)>{>thrownew ArgumentNullException(nameof(identity));>}>> var value = identity.FindFirst(XmsSubFct)?.Value;>return ContainsFunctionCode(value,13);>}>>/// <summary>>/// Parses a claim string representing a space-separated collection of integers and checks for a target code.>/// Returns true only if all tokens are valid integers and one equals the target code.>/// </summary>> private staticbool ContainsFunctionCode(string? raw,int targetCode)>{>if(string.IsNullOrWhiteSpace(raw))>{>return false;>}>> var tokens = raw.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);// split on whitespace>bool found = false;>>foreach(var token in tokens)>{>if(!int.TryParse(token, NumberStyles.Integer, CultureInfo.InvariantCulture,outint n))>{>// If any token is non-integer, the claim is not a valid collection of integers.>return false;>}>>if(n == targetCode)>{> found = true;>}>}>>return found;>}>}>}> ```
>> ```csharp name=tests/Microsoft.Identity.Web.Test/AgentIdentityExtensionsTests.cs
>// Copyright (c) Microsoft Corporation.>// Licensed under the MIT License.>>using System.Security.Claims;>using Microsoft.Identity.Web.Test.Common;>using Xunit;>>namespace Microsoft.Identity.Web.Test
>{> public class AgentIdentityExtensionsTests
>{> private const string ParentAgentBlueprintClaim = "xms_par_app_azp";> private const string ParentAgentBlueprintValue = "agent-blueprint-123";>> private const string SubjectFunctionClaim = "xms_sub_fct";>>[Fact]> public void GetParentAgentBlueprint_FromClaimsPrincipal_WithClaim_ReturnsValue()>{> var principal =new ClaimsPrincipal(>new CaseSensitiveClaimsIdentity(new[]>{>new Claim(ParentAgentBlueprintClaim, ParentAgentBlueprintValue),>}));>> Assert.Equal(ParentAgentBlueprintValue, principal.GetParentAgentBlueprint());>}>>[Fact]> public void GetParentAgentBlueprint_FromClaimsPrincipal_NoClaim_ReturnsNull()>{> var principal =new ClaimsPrincipal();> Assert.Null(principal.GetParentAgentBlueprint());>}>>[Fact]> public void GetParentAgentBlueprint_FromClaimsIdentity_WithClaim_ReturnsValue()>{> var identity =new CaseSensitiveClaimsIdentity(new[]>{>new Claim(ParentAgentBlueprintClaim, ParentAgentBlueprintValue),>});>> Assert.Equal(ParentAgentBlueprintValue, identity.GetParentAgentBlueprint());>}>>[Fact]> public void GetParentAgentBlueprint_FromClaimsIdentity_NoClaim_ReturnsNull()>{> var identity =new CaseSensitiveClaimsIdentity();> Assert.Null(identity.GetParentAgentBlueprint());>}>>[Fact]> public void IsAgentUserIdentity_Principal_SpaceSeparated_Contains13_ReturnsTrue()>{> var principal =new ClaimsPrincipal(>new CaseSensitiveClaimsIdentity(new[]>{>new Claim(SubjectFunctionClaim, "121315"),>}));>> Assert.True(principal.IsAgentUserIdentity());>}>>[Fact]> public void IsAgentUserIdentity_Principal_SpaceSeparated_No13_ReturnsFalse()>{> var principal =new ClaimsPrincipal(>new CaseSensitiveClaimsIdentity(new[]>{>new Claim(SubjectFunctionClaim, "1234"),>}));>> Assert.False(principal.IsAgentUserIdentity());>}>>[Fact]> public void IsAgentUserIdentity_Principal_ExtraWhitespace_Contains13_ReturnsTrue()>{> var principal =new ClaimsPrincipal(>new CaseSensitiveClaimsIdentity(new[]>{>new Claim(SubjectFunctionClaim, " 91321 "),>}));>> Assert.True(principal.IsAgentUserIdentity());>}>>[Fact]> public void IsAgentUserIdentity_Principal_InvalidToken_ReturnsFalse()>{> var principal =new ClaimsPrincipal(>new CaseSensitiveClaimsIdentity(new[]>{>new Claim(SubjectFunctionClaim, "12 a 13"),>}));>> Assert.False(principal.IsAgentUserIdentity());>}>>[Fact]> public void IsAgentUserIdentity_Principal_NoClaim_ReturnsFalse()>{> var principal =new ClaimsPrincipal();> Assert.False(principal.IsAgentUserIdentity());>}>>[Fact]> public void IsAgentUserIdentity_Identity_SpaceSeparated_Contains13_ReturnsTrue()>{> var identity =new CaseSensitiveClaimsIdentity(new[]>{>new Claim(SubjectFunctionClaim, "781321"),>});>> Assert.True(identity.IsAgentUserIdentity());>}>>[Fact]> public void IsAgentUserIdentity_Identity_EmptyString_ReturnsFalse()>{> var identity =new CaseSensitiveClaimsIdentity(new[]>{>new Claim(SubjectFunctionClaim, " "),>});>> Assert.False(identity.IsAgentUserIdentity());>}>>[Fact]> public void IsAgentUserIdentity_Identity_NoClaim_ReturnsFalse()>{> var identity =new CaseSensitiveClaimsIdentity();> Assert.False(identity.IsAgentUserIdentity());>}>}>}> ```
>> Build/test guidance
>- Build the solution.>- Run the test suite; the new tests are in AgentIdentityExtensionsTests.>> Acceptance criteria
>- All new tests pass.>- No regressions in existing tests.>- New methods are available for ClaimsPrincipal and ClaimsIdentity as specified.><!-- START COPILOT CODING AGENT TIPS -->---
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more [Copilot coding agent tips](https://gh.io/copilot-coding-agent-tips) in the docs.

Co-authored-by: jmprieur <13203188+jmprieur@users.noreply.github.com>
CopilotAI changed the title [WIP] Add agent identity extension methods and testsAdd agent identity extension methods for ClaimsPrincipal and ClaimsIdentityOct 2, 2025
…s project
Co-authored-by: jmprieur <13203188+jmprieur@users.noreply.github.com>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM

@jmprieur
Jean-Marc Prieur (jmprieur) marked this pull request as ready for review October 2, 2025 03:09
Comment threadsrc/Microsoft.Identity.Web.AgentIdentities/AgentIdentityExtensions.cs Outdated
Comment threadsrc/Microsoft.Identity.Web.AgentIdentities/AgentIdentityExtensions.cs Outdated
Co-authored-by: keegan-caruso <54109988+keegan-caruso@users.noreply.github.com>
@jmprieur

Copy link
Copy Markdown
Collaborator

Copilot : let's add other asserts using the ClaimsIdentity extension methods in the Agent identity end to end tests under tests/E2E%20Tests/AgentApplications

When the test gets the authorization header, extract the token, read it with JsonWebTokenHandler, and create an untrusted claims identity from the claims in the token.

Co-authored-by: jmprieur <13203188+jmprieur@users.noreply.github.com>

CopilotAI commented Oct 2, 2025

Copy link
Copy Markdown
ContributorAuthor

Copilot : let's add other asserts using the ClaimsIdentity extension methods in the Agent identity end to end tests under tests/E2E%20Tests/AgentApplications

When the test gets the authorization...

Added assertions in all four AgentUserIdentityTests methods (722ac3b). Each test now extracts the token from the authorization header, parses it with JwtSecurityTokenHandler, creates a CaseSensitiveClaimsIdentity from the claims, and uses IsAgentUserIdentity() and GetParentAgentBlueprint() extension methods to validate the token.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Copilot : this is great, I left comments.,
Could we also assert the 2 extension methods on ClaimsIndentity in Autonomous agent tests as well?

Comment threadtests/E2E Tests/AgentApplications/AgentUserIdentityTestscs.cs Outdated
Comment threadtests/E2E Tests/AgentApplications/AgentUserIdentityTestscs.cs Outdated
Comment threadtests/E2E Tests/AgentApplications/AgentUserIdentityTestscs.cs Outdated
Comment threadtests/E2E Tests/AgentApplications/AgentUserIdentityTestscs.cs Outdated
Sign up for freeto 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.

4 participants

@jmprieur@keegan-caruso@saurabhsathe-ms
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Add agent identity extension methods for ClaimsPrincipal and ClaimsIdentity by Copilot · Pull Request #3515 · AzureAD/microsoft-identity-web · GitHub
Skip to content

Add agent identity extension methods for ClaimsPrincipal and ClaimsIdentity - #3515

Merged
Jean-Marc Prieur (jmprieur) merged 11 commits into
masterfrom
copilot/fix-2b1c96f6-8d41-4b94-900e-30503306567e
Oct 8, 2025
Merged

Add agent identity extension methods for ClaimsPrincipal and ClaimsIdentity#3515
Jean-Marc Prieur (jmprieur) merged 11 commits into
masterfrom
copilot/fix-2b1c96f6-8d41-4b94-900e-30503306567e

Conversation

CopilotAI commented Oct 2, 2025

Copy link
Copy Markdown
Contributor
  • Create AgentIdentityExtensions.cs in src/Microsoft.Identity.Web.AgentIdentities with extension methods (moved from TokenCache)
    • GetParentAgentBlueprint for ClaimsPrincipal
    • GetParentAgentBlueprint for ClaimsIdentity
    • IsAgentUserIdentity for ClaimsPrincipal
    • IsAgentUserIdentity for ClaimsIdentity
  • Create AgentIdentityExtensionsTests.cs in tests/Microsoft.Identity.Web.Test with comprehensive tests
    • Tests for GetParentAgentBlueprint (both Principal and Identity)
    • Tests for IsAgentUserIdentity with various scenarios
  • Update PublicAPI.Unshipped.txt files to document new public APIs in correct project
  • Add project reference to Microsoft.Identity.Web.AgentIdentities in test project
  • Build and test to ensure no regressions
  • Verify all tests pass (12 new tests + 627 existing tests = 639 total)
  • Extract magic number 13 into AgentIdUser constant
  • Rename ContainsFunctionCode to ContainsSubjectFacet and update comments
  • Add token validation using ClaimsIdentity extension methods in E2E tests
    • Extract JWT tokens from authorization headers in AgentUserIdentityTests
    • Parse tokens with JwtSecurityTokenHandler and create CaseSensitiveClaimsIdentity
    • Validate tokens using IsAgentUserIdentity() and GetParentAgentBlueprint() extension methods in all four test methods
  • Assert parent blueprint equals agent application in all E2E tests
    • Added assertions in AgentUserIdentityTests (4 test methods)
    • Added assertions in AutonomousAgentTests
Original prompt

Title: Add agent identity extension methods: GetParentAgentBlueprint and IsAgentUserIdentity

Summary
Add extension methods on ClaimsPrincipal and ClaimsIdentity to help developers detect agent identities and retrieve the parent agent blueprint from token claims.

Requirements

  1. Add the following extension methods in a new file under src/Microsoft.Identity.Web.TokenCache:

    • ClaimsPrincipal.GetParentAgentBlueprint(): retrieves the value of the xms_par_app_azp claim if it exists, returns null otherwise.
    • ClaimsIdentity.GetParentAgentBlueprint(): same as above for ClaimsIdentity.
    • ClaimsPrincipal.IsAgentUserIdentity(): returns true if the xms_sub_fct claim exists, is strictly a space-separated string of integers, every token is a valid integer, and the collection contains the integer 13; false otherwise.
    • ClaimsIdentity.IsAgentUserIdentity(): same as above for ClaimsIdentity.
  2. Add unit tests under tests/Microsoft.Identity.Web.Test to validate the above behaviors.

Notes

  • Follow project style used by existing ClaimsPrincipalExtensions.cs (namespace Microsoft.Identity.Web, XML doc comments, null checks).
  • Do not depend on internal helpers like Throws.IfNull or ClaimConstants; use public BCL APIs to keep the new code self-contained.
  • No changes expected to .csproj files.

Proposed changes

// Copyright (c) Microsoft Corporation.// Licensed under the MIT License.usingSystem;usingSystem.Globalization;usingSystem.Security.Claims;namespaceMicrosoft.Identity.Web{/// <summary>/// Extensions to read agent identity-related claims./// </summary>publicstaticclassAgentIdentityExtensions{/// <summary>/// Claim type for the parent agent blueprint./// </summary>privateconststringXmsParAppAzp="xms_par_app_azp";/// <summary>/// Claim type for subject function codes (space-separated integers)./// </summary>privateconststringXmsSubFct="xms_sub_fct";/// <summary>/// Retrieves the parent agent blueprint (xms_par_app_azp) value from a ClaimsPrincipal, if present./// </summary>/// <param name="claimsPrincipal">The claims principal.</param>/// <returns>The value of the xms_par_app_azp claim if it exists; otherwise, null.</returns>publicstaticstring?GetParentAgentBlueprint(thisClaimsPrincipalclaimsPrincipal){if(claimsPrincipalisnull){thrownewArgumentNullException(nameof(claimsPrincipal));}returnclaimsPrincipal.FindFirst(XmsParAppAzp)?.Value;}/// <summary>/// Retrieves the parent agent blueprint (xms_par_app_azp) value from a ClaimsIdentity, if present./// </summary>/// <param name="identity">The claims identity.</param>/// <returns>The value of the xms_par_app_azp claim if it exists; otherwise, null.</returns>publicstaticstring?GetParentAgentBlueprint(thisClaimsIdentityidentity){if(identityisnull){thrownewArgumentNullException(nameof(identity));}returnidentity.FindFirst(XmsParAppAzp)?.Value;}/// <summary>/// Determines whether the ClaimsPrincipal represents an agent user identity./// True if the xms_sub_fct claim exists, is a space-separated string of integers,/// and that collection contains the integer 13./// </summary>/// <param name="claimsPrincipal">The claims principal.</param>/// <returns>True if xms_sub_fct contains 13 and all tokens are integers; otherwise false.</returns>publicstaticboolIsAgentUserIdentity(thisClaimsPrincipalclaimsPrincipal){if(claimsPrincipalisnull){thrownewArgumentNullException(nameof(claimsPrincipal));}varvalue=claimsPrincipal.FindFirst(XmsSubFct)?.Value;returnContainsFunctionCode(value,13);}/// <summary>/// Determines whether the ClaimsIdentity represents an agent user identity./// True if the xms_sub_fct claim exists, is a space-separated string of integers,/// and that collection contains the integer 13./// </summary>/// <param name="identity">The claims identity.</param>/// <returns>True if xms_sub_fct contains 13 and all tokens are integers; otherwise false.</returns>publicstaticboolIsAgentUserIdentity(thisClaimsIdentityidentity){if(identityisnull){thrownewArgumentNullException(nameof(identity));}varvalue=identity.FindFirst(XmsSubFct)?.Value;returnContainsFunctionCode(value,13);}/// <summary>
...</details>*This pull request was created as a result of the following prompt from Copilot chat.*> Title: Add agent identity extension methods: GetParentAgentBlueprint and IsAgentUserIdentity
>> Summary
> Add extension methods on ClaimsPrincipal and ClaimsIdentity to help developers detect agent identities and retrieve the parent agent blueprint from token claims.>> Requirements
>1) Add the following extension methods in a new file under src/Microsoft.Identity.Web.TokenCache:>-ClaimsPrincipal.GetParentAgentBlueprint(): retrieves the value of the xms_par_app_azp claim if it exists, returns null otherwise.>-ClaimsIdentity.GetParentAgentBlueprint(): same as above for ClaimsIdentity.>-ClaimsPrincipal.IsAgentUserIdentity():returns true if the xms_sub_fct claim exists,is strictly a space-separated string of integers,every token is a valid integer,and the collection contains the integer 13; false otherwise.>-ClaimsIdentity.IsAgentUserIdentity():same as above for ClaimsIdentity.>>2)Add unit tests under tests/Microsoft.Identity.Web.Test to validate the above behaviors.>>Notes>- Follow project style used by existing ClaimsPrincipalExtensions.cs(namespaceMicrosoft.Identity.Web,XMLdoc comments,null checks).>- Do not depend on internal helpers like Throws.IfNull or ClaimConstants; use public BCL APIs to keep the new code self-contained.>- No changes expected to .csproj files.>> Proposed changes
>> ```csharp name=src/Microsoft.Identity.Web.TokenCache/AgentIdentityExtensions.cs
>// Copyright (c) Microsoft Corporation.>// Licensed under the MIT License.>>using System;>using System.Globalization;>using System.Security.Claims;>>namespace Microsoft.Identity.Web
>{>/// <summary>>/// Extensions to read agent identity-related claims.>/// </summary>>publicstaticclassAgentIdentityExtensions>{>/// <summary>>/// Claim type for the parent agent blueprint.>/// </summary>>privateconststringXmsParAppAzp="xms_par_app_azp";>>/// <summary>>/// Claim type for subject function codes (space-separated integers).>/// </summary>>privateconststringXmsSubFct="xms_sub_fct";>>/// <summary>>/// Retrieves the parent agent blueprint (xms_par_app_azp) value from a ClaimsPrincipal, if present.>/// </summary>>/// <param name="claimsPrincipal">The claims principal.</param>>/// <returns>The value of the xms_par_app_azp claim if it exists; otherwise, null.</returns>>publicstaticstring?GetParentAgentBlueprint(thisClaimsPrincipalclaimsPrincipal)>{>if(claimsPrincipalisnull)>{>thrownewArgumentNullException(nameof(claimsPrincipal));>}>>return claimsPrincipal.FindFirst(XmsParAppAzp)?.Value;>}>>/// <summary>>/// Retrieves the parent agent blueprint (xms_par_app_azp) value from a ClaimsIdentity, if present.>/// </summary>>/// <param name="identity">The claims identity.</param>>/// <returns>The value of the xms_par_app_azp claim if it exists; otherwise, null.</returns>>publicstatic string? GetParentAgentBlueprint(thisClaimsIdentityidentity)>{>if(identityisnull)>{>thrownewArgumentNullException(nameof(identity));>}>>returnidentity.FindFirst(XmsParAppAzp)?.Value;>}>>/// <summary>>/// Determines whether the ClaimsPrincipal represents an agent user identity.>/// True if the xms_sub_fct claim exists, is a space-separated string of integers,>/// and that collection contains the integer 13.>/// </summary>>/// <param name="claimsPrincipal">The claims principal.</param>>/// <returns>True if xms_sub_fct contains 13 and all tokens are integers; otherwise false.</returns>> public staticbool IsAgentUserIdentity(thisClaimsPrincipalclaimsPrincipal)>{>if(claimsPrincipalisnull)>{>thrownewArgumentNullException(nameof(claimsPrincipal));>}>>var value =claimsPrincipal.FindFirst(XmsSubFct)?.Value;>returnContainsFunctionCode(value,13);>}>>/// <summary>>/// Determines whether the ClaimsIdentity represents an agent user identity.>/// True if the xms_sub_fct claim exists, is a space-separated string of integers,>/// and that collection contains the integer 13.>/// </summary>>/// <param name="identity">The claims identity.</param>>/// <returns>True if xms_sub_fct contains 13 and all tokens are integers; otherwise false.</returns>> public staticboolIsAgentUserIdentity(thisClaimsIdentityidentity)>{>if(identity isnull)>{>thrownew ArgumentNullException(nameof(identity));>}>> var value = identity.FindFirst(XmsSubFct)?.Value;>return ContainsFunctionCode(value,13);>}>>/// <summary>>/// Parses a claim string representing a space-separated collection of integers and checks for a target code.>/// Returns true only if all tokens are valid integers and one equals the target code.>/// </summary>> private staticbool ContainsFunctionCode(string? raw,int targetCode)>{>if(string.IsNullOrWhiteSpace(raw))>{>return false;>}>> var tokens = raw.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);// split on whitespace>bool found = false;>>foreach(var token in tokens)>{>if(!int.TryParse(token, NumberStyles.Integer, CultureInfo.InvariantCulture,outint n))>{>// If any token is non-integer, the claim is not a valid collection of integers.>return false;>}>>if(n == targetCode)>{> found = true;>}>}>>return found;>}>}>}> ```
>> ```csharp name=tests/Microsoft.Identity.Web.Test/AgentIdentityExtensionsTests.cs
>// Copyright (c) Microsoft Corporation.>// Licensed under the MIT License.>>using System.Security.Claims;>using Microsoft.Identity.Web.Test.Common;>using Xunit;>>namespace Microsoft.Identity.Web.Test
>{> public class AgentIdentityExtensionsTests
>{> private const string ParentAgentBlueprintClaim = "xms_par_app_azp";> private const string ParentAgentBlueprintValue = "agent-blueprint-123";>> private const string SubjectFunctionClaim = "xms_sub_fct";>>[Fact]> public void GetParentAgentBlueprint_FromClaimsPrincipal_WithClaim_ReturnsValue()>{> var principal =new ClaimsPrincipal(>new CaseSensitiveClaimsIdentity(new[]>{>new Claim(ParentAgentBlueprintClaim, ParentAgentBlueprintValue),>}));>> Assert.Equal(ParentAgentBlueprintValue, principal.GetParentAgentBlueprint());>}>>[Fact]> public void GetParentAgentBlueprint_FromClaimsPrincipal_NoClaim_ReturnsNull()>{> var principal =new ClaimsPrincipal();> Assert.Null(principal.GetParentAgentBlueprint());>}>>[Fact]> public void GetParentAgentBlueprint_FromClaimsIdentity_WithClaim_ReturnsValue()>{> var identity =new CaseSensitiveClaimsIdentity(new[]>{>new Claim(ParentAgentBlueprintClaim, ParentAgentBlueprintValue),>});>> Assert.Equal(ParentAgentBlueprintValue, identity.GetParentAgentBlueprint());>}>>[Fact]> public void GetParentAgentBlueprint_FromClaimsIdentity_NoClaim_ReturnsNull()>{> var identity =new CaseSensitiveClaimsIdentity();> Assert.Null(identity.GetParentAgentBlueprint());>}>>[Fact]> public void IsAgentUserIdentity_Principal_SpaceSeparated_Contains13_ReturnsTrue()>{> var principal =new ClaimsPrincipal(>new CaseSensitiveClaimsIdentity(new[]>{>new Claim(SubjectFunctionClaim, "121315"),>}));>> Assert.True(principal.IsAgentUserIdentity());>}>>[Fact]> public void IsAgentUserIdentity_Principal_SpaceSeparated_No13_ReturnsFalse()>{> var principal =new ClaimsPrincipal(>new CaseSensitiveClaimsIdentity(new[]>{>new Claim(SubjectFunctionClaim, "1234"),>}));>> Assert.False(principal.IsAgentUserIdentity());>}>>[Fact]> public void IsAgentUserIdentity_Principal_ExtraWhitespace_Contains13_ReturnsTrue()>{> var principal =new ClaimsPrincipal(>new CaseSensitiveClaimsIdentity(new[]>{>new Claim(SubjectFunctionClaim, " 91321 "),>}));>> Assert.True(principal.IsAgentUserIdentity());>}>>[Fact]> public void IsAgentUserIdentity_Principal_InvalidToken_ReturnsFalse()>{> var principal =new ClaimsPrincipal(>new CaseSensitiveClaimsIdentity(new[]>{>new Claim(SubjectFunctionClaim, "12 a 13"),>}));>> Assert.False(principal.IsAgentUserIdentity());>}>>[Fact]> public void IsAgentUserIdentity_Principal_NoClaim_ReturnsFalse()>{> var principal =new ClaimsPrincipal();> Assert.False(principal.IsAgentUserIdentity());>}>>[Fact]> public void IsAgentUserIdentity_Identity_SpaceSeparated_Contains13_ReturnsTrue()>{> var identity =new CaseSensitiveClaimsIdentity(new[]>{>new Claim(SubjectFunctionClaim, "781321"),>});>> Assert.True(identity.IsAgentUserIdentity());>}>>[Fact]> public void IsAgentUserIdentity_Identity_EmptyString_ReturnsFalse()>{> var identity =new CaseSensitiveClaimsIdentity(new[]>{>new Claim(SubjectFunctionClaim, " "),>});>> Assert.False(identity.IsAgentUserIdentity());>}>>[Fact]> public void IsAgentUserIdentity_Identity_NoClaim_ReturnsFalse()>{> var identity =new CaseSensitiveClaimsIdentity();> Assert.False(identity.IsAgentUserIdentity());>}>}>}> ```
>> Build/test guidance
>- Build the solution.>- Run the test suite; the new tests are in AgentIdentityExtensionsTests.>> Acceptance criteria
>- All new tests pass.>- No regressions in existing tests.>- New methods are available for ClaimsPrincipal and ClaimsIdentity as specified.><!-- START COPILOT CODING AGENT TIPS -->---
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more [Copilot coding agent tips](https://gh.io/copilot-coding-agent-tips) in the docs.

Co-authored-by: jmprieur <13203188+jmprieur@users.noreply.github.com>
CopilotAI changed the title [WIP] Add agent identity extension methods and testsAdd agent identity extension methods for ClaimsPrincipal and ClaimsIdentityOct 2, 2025
…s project
Co-authored-by: jmprieur <13203188+jmprieur@users.noreply.github.com>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM

@jmprieur
Jean-Marc Prieur (jmprieur) marked this pull request as ready for review October 2, 2025 03:09
Comment threadsrc/Microsoft.Identity.Web.AgentIdentities/AgentIdentityExtensions.cs Outdated
Comment threadsrc/Microsoft.Identity.Web.AgentIdentities/AgentIdentityExtensions.cs Outdated
Co-authored-by: keegan-caruso <54109988+keegan-caruso@users.noreply.github.com>
@jmprieur

Copy link
Copy Markdown
Collaborator

Copilot : let's add other asserts using the ClaimsIdentity extension methods in the Agent identity end to end tests under tests/E2E%20Tests/AgentApplications

When the test gets the authorization header, extract the token, read it with JsonWebTokenHandler, and create an untrusted claims identity from the claims in the token.

Co-authored-by: jmprieur <13203188+jmprieur@users.noreply.github.com>

CopilotAI commented Oct 2, 2025

Copy link
Copy Markdown
ContributorAuthor

Copilot : let's add other asserts using the ClaimsIdentity extension methods in the Agent identity end to end tests under tests/E2E%20Tests/AgentApplications

When the test gets the authorization...

Added assertions in all four AgentUserIdentityTests methods (722ac3b). Each test now extracts the token from the authorization header, parses it with JwtSecurityTokenHandler, creates a CaseSensitiveClaimsIdentity from the claims, and uses IsAgentUserIdentity() and GetParentAgentBlueprint() extension methods to validate the token.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Copilot : this is great, I left comments.,
Could we also assert the 2 extension methods on ClaimsIndentity in Autonomous agent tests as well?

Comment threadtests/E2E Tests/AgentApplications/AgentUserIdentityTestscs.cs Outdated
Comment threadtests/E2E Tests/AgentApplications/AgentUserIdentityTestscs.cs Outdated
Comment threadtests/E2E Tests/AgentApplications/AgentUserIdentityTestscs.cs Outdated
Comment threadtests/E2E Tests/AgentApplications/AgentUserIdentityTestscs.cs Outdated
Sign up for freeto 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.

4 participants

@jmprieur@keegan-caruso@saurabhsathe-ms
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Add agent identity extension methods for ClaimsPrincipal and ClaimsIdentity by Copilot · Pull Request #3515 · AzureAD/microsoft-identity-web · GitHub
Skip to content

Add agent identity extension methods for ClaimsPrincipal and ClaimsIdentity - #3515

Merged
Jean-Marc Prieur (jmprieur) merged 11 commits into
masterfrom
copilot/fix-2b1c96f6-8d41-4b94-900e-30503306567e
Oct 8, 2025
Merged

Add agent identity extension methods for ClaimsPrincipal and ClaimsIdentity#3515
Jean-Marc Prieur (jmprieur) merged 11 commits into
masterfrom
copilot/fix-2b1c96f6-8d41-4b94-900e-30503306567e

Conversation

CopilotAI commented Oct 2, 2025

Copy link
Copy Markdown
Contributor
  • Create AgentIdentityExtensions.cs in src/Microsoft.Identity.Web.AgentIdentities with extension methods (moved from TokenCache)
    • GetParentAgentBlueprint for ClaimsPrincipal
    • GetParentAgentBlueprint for ClaimsIdentity
    • IsAgentUserIdentity for ClaimsPrincipal
    • IsAgentUserIdentity for ClaimsIdentity
  • Create AgentIdentityExtensionsTests.cs in tests/Microsoft.Identity.Web.Test with comprehensive tests
    • Tests for GetParentAgentBlueprint (both Principal and Identity)
    • Tests for IsAgentUserIdentity with various scenarios
  • Update PublicAPI.Unshipped.txt files to document new public APIs in correct project
  • Add project reference to Microsoft.Identity.Web.AgentIdentities in test project
  • Build and test to ensure no regressions
  • Verify all tests pass (12 new tests + 627 existing tests = 639 total)
  • Extract magic number 13 into AgentIdUser constant
  • Rename ContainsFunctionCode to ContainsSubjectFacet and update comments
  • Add token validation using ClaimsIdentity extension methods in E2E tests
    • Extract JWT tokens from authorization headers in AgentUserIdentityTests
    • Parse tokens with JwtSecurityTokenHandler and create CaseSensitiveClaimsIdentity
    • Validate tokens using IsAgentUserIdentity() and GetParentAgentBlueprint() extension methods in all four test methods
  • Assert parent blueprint equals agent application in all E2E tests
    • Added assertions in AgentUserIdentityTests (4 test methods)
    • Added assertions in AutonomousAgentTests
Original prompt

Title: Add agent identity extension methods: GetParentAgentBlueprint and IsAgentUserIdentity

Summary
Add extension methods on ClaimsPrincipal and ClaimsIdentity to help developers detect agent identities and retrieve the parent agent blueprint from token claims.

Requirements

  1. Add the following extension methods in a new file under src/Microsoft.Identity.Web.TokenCache:

    • ClaimsPrincipal.GetParentAgentBlueprint(): retrieves the value of the xms_par_app_azp claim if it exists, returns null otherwise.
    • ClaimsIdentity.GetParentAgentBlueprint(): same as above for ClaimsIdentity.
    • ClaimsPrincipal.IsAgentUserIdentity(): returns true if the xms_sub_fct claim exists, is strictly a space-separated string of integers, every token is a valid integer, and the collection contains the integer 13; false otherwise.
    • ClaimsIdentity.IsAgentUserIdentity(): same as above for ClaimsIdentity.
  2. Add unit tests under tests/Microsoft.Identity.Web.Test to validate the above behaviors.

Notes

  • Follow project style used by existing ClaimsPrincipalExtensions.cs (namespace Microsoft.Identity.Web, XML doc comments, null checks).
  • Do not depend on internal helpers like Throws.IfNull or ClaimConstants; use public BCL APIs to keep the new code self-contained.
  • No changes expected to .csproj files.

Proposed changes

// Copyright (c) Microsoft Corporation.// Licensed under the MIT License.usingSystem;usingSystem.Globalization;usingSystem.Security.Claims;namespaceMicrosoft.Identity.Web{/// <summary>/// Extensions to read agent identity-related claims./// </summary>publicstaticclassAgentIdentityExtensions{/// <summary>/// Claim type for the parent agent blueprint./// </summary>privateconststringXmsParAppAzp="xms_par_app_azp";/// <summary>/// Claim type for subject function codes (space-separated integers)./// </summary>privateconststringXmsSubFct="xms_sub_fct";/// <summary>/// Retrieves the parent agent blueprint (xms_par_app_azp) value from a ClaimsPrincipal, if present./// </summary>/// <param name="claimsPrincipal">The claims principal.</param>/// <returns>The value of the xms_par_app_azp claim if it exists; otherwise, null.</returns>publicstaticstring?GetParentAgentBlueprint(thisClaimsPrincipalclaimsPrincipal){if(claimsPrincipalisnull){thrownewArgumentNullException(nameof(claimsPrincipal));}returnclaimsPrincipal.FindFirst(XmsParAppAzp)?.Value;}/// <summary>/// Retrieves the parent agent blueprint (xms_par_app_azp) value from a ClaimsIdentity, if present./// </summary>/// <param name="identity">The claims identity.</param>/// <returns>The value of the xms_par_app_azp claim if it exists; otherwise, null.</returns>publicstaticstring?GetParentAgentBlueprint(thisClaimsIdentityidentity){if(identityisnull){thrownewArgumentNullException(nameof(identity));}returnidentity.FindFirst(XmsParAppAzp)?.Value;}/// <summary>/// Determines whether the ClaimsPrincipal represents an agent user identity./// True if the xms_sub_fct claim exists, is a space-separated string of integers,/// and that collection contains the integer 13./// </summary>/// <param name="claimsPrincipal">The claims principal.</param>/// <returns>True if xms_sub_fct contains 13 and all tokens are integers; otherwise false.</returns>publicstaticboolIsAgentUserIdentity(thisClaimsPrincipalclaimsPrincipal){if(claimsPrincipalisnull){thrownewArgumentNullException(nameof(claimsPrincipal));}varvalue=claimsPrincipal.FindFirst(XmsSubFct)?.Value;returnContainsFunctionCode(value,13);}/// <summary>/// Determines whether the ClaimsIdentity represents an agent user identity./// True if the xms_sub_fct claim exists, is a space-separated string of integers,/// and that collection contains the integer 13./// </summary>/// <param name="identity">The claims identity.</param>/// <returns>True if xms_sub_fct contains 13 and all tokens are integers; otherwise false.</returns>publicstaticboolIsAgentUserIdentity(thisClaimsIdentityidentity){if(identityisnull){thrownewArgumentNullException(nameof(identity));}varvalue=identity.FindFirst(XmsSubFct)?.Value;returnContainsFunctionCode(value,13);}/// <summary>
...</details>*This pull request was created as a result of the following prompt from Copilot chat.*> Title: Add agent identity extension methods: GetParentAgentBlueprint and IsAgentUserIdentity
>> Summary
> Add extension methods on ClaimsPrincipal and ClaimsIdentity to help developers detect agent identities and retrieve the parent agent blueprint from token claims.>> Requirements
>1) Add the following extension methods in a new file under src/Microsoft.Identity.Web.TokenCache:>-ClaimsPrincipal.GetParentAgentBlueprint(): retrieves the value of the xms_par_app_azp claim if it exists, returns null otherwise.>-ClaimsIdentity.GetParentAgentBlueprint(): same as above for ClaimsIdentity.>-ClaimsPrincipal.IsAgentUserIdentity():returns true if the xms_sub_fct claim exists,is strictly a space-separated string of integers,every token is a valid integer,and the collection contains the integer 13; false otherwise.>-ClaimsIdentity.IsAgentUserIdentity():same as above for ClaimsIdentity.>>2)Add unit tests under tests/Microsoft.Identity.Web.Test to validate the above behaviors.>>Notes>- Follow project style used by existing ClaimsPrincipalExtensions.cs(namespaceMicrosoft.Identity.Web,XMLdoc comments,null checks).>- Do not depend on internal helpers like Throws.IfNull or ClaimConstants; use public BCL APIs to keep the new code self-contained.>- No changes expected to .csproj files.>> Proposed changes
>> ```csharp name=src/Microsoft.Identity.Web.TokenCache/AgentIdentityExtensions.cs
>// Copyright (c) Microsoft Corporation.>// Licensed under the MIT License.>>using System;>using System.Globalization;>using System.Security.Claims;>>namespace Microsoft.Identity.Web
>{>/// <summary>>/// Extensions to read agent identity-related claims.>/// </summary>>publicstaticclassAgentIdentityExtensions>{>/// <summary>>/// Claim type for the parent agent blueprint.>/// </summary>>privateconststringXmsParAppAzp="xms_par_app_azp";>>/// <summary>>/// Claim type for subject function codes (space-separated integers).>/// </summary>>privateconststringXmsSubFct="xms_sub_fct";>>/// <summary>>/// Retrieves the parent agent blueprint (xms_par_app_azp) value from a ClaimsPrincipal, if present.>/// </summary>>/// <param name="claimsPrincipal">The claims principal.</param>>/// <returns>The value of the xms_par_app_azp claim if it exists; otherwise, null.</returns>>publicstaticstring?GetParentAgentBlueprint(thisClaimsPrincipalclaimsPrincipal)>{>if(claimsPrincipalisnull)>{>thrownewArgumentNullException(nameof(claimsPrincipal));>}>>return claimsPrincipal.FindFirst(XmsParAppAzp)?.Value;>}>>/// <summary>>/// Retrieves the parent agent blueprint (xms_par_app_azp) value from a ClaimsIdentity, if present.>/// </summary>>/// <param name="identity">The claims identity.</param>>/// <returns>The value of the xms_par_app_azp claim if it exists; otherwise, null.</returns>>publicstatic string? GetParentAgentBlueprint(thisClaimsIdentityidentity)>{>if(identityisnull)>{>thrownewArgumentNullException(nameof(identity));>}>>returnidentity.FindFirst(XmsParAppAzp)?.Value;>}>>/// <summary>>/// Determines whether the ClaimsPrincipal represents an agent user identity.>/// True if the xms_sub_fct claim exists, is a space-separated string of integers,>/// and that collection contains the integer 13.>/// </summary>>/// <param name="claimsPrincipal">The claims principal.</param>>/// <returns>True if xms_sub_fct contains 13 and all tokens are integers; otherwise false.</returns>> public staticbool IsAgentUserIdentity(thisClaimsPrincipalclaimsPrincipal)>{>if(claimsPrincipalisnull)>{>thrownewArgumentNullException(nameof(claimsPrincipal));>}>>var value =claimsPrincipal.FindFirst(XmsSubFct)?.Value;>returnContainsFunctionCode(value,13);>}>>/// <summary>>/// Determines whether the ClaimsIdentity represents an agent user identity.>/// True if the xms_sub_fct claim exists, is a space-separated string of integers,>/// and that collection contains the integer 13.>/// </summary>>/// <param name="identity">The claims identity.</param>>/// <returns>True if xms_sub_fct contains 13 and all tokens are integers; otherwise false.</returns>> public staticboolIsAgentUserIdentity(thisClaimsIdentityidentity)>{>if(identity isnull)>{>thrownew ArgumentNullException(nameof(identity));>}>> var value = identity.FindFirst(XmsSubFct)?.Value;>return ContainsFunctionCode(value,13);>}>>/// <summary>>/// Parses a claim string representing a space-separated collection of integers and checks for a target code.>/// Returns true only if all tokens are valid integers and one equals the target code.>/// </summary>> private staticbool ContainsFunctionCode(string? raw,int targetCode)>{>if(string.IsNullOrWhiteSpace(raw))>{>return false;>}>> var tokens = raw.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);// split on whitespace>bool found = false;>>foreach(var token in tokens)>{>if(!int.TryParse(token, NumberStyles.Integer, CultureInfo.InvariantCulture,outint n))>{>// If any token is non-integer, the claim is not a valid collection of integers.>return false;>}>>if(n == targetCode)>{> found = true;>}>}>>return found;>}>}>}> ```
>> ```csharp name=tests/Microsoft.Identity.Web.Test/AgentIdentityExtensionsTests.cs
>// Copyright (c) Microsoft Corporation.>// Licensed under the MIT License.>>using System.Security.Claims;>using Microsoft.Identity.Web.Test.Common;>using Xunit;>>namespace Microsoft.Identity.Web.Test
>{> public class AgentIdentityExtensionsTests
>{> private const string ParentAgentBlueprintClaim = "xms_par_app_azp";> private const string ParentAgentBlueprintValue = "agent-blueprint-123";>> private const string SubjectFunctionClaim = "xms_sub_fct";>>[Fact]> public void GetParentAgentBlueprint_FromClaimsPrincipal_WithClaim_ReturnsValue()>{> var principal =new ClaimsPrincipal(>new CaseSensitiveClaimsIdentity(new[]>{>new Claim(ParentAgentBlueprintClaim, ParentAgentBlueprintValue),>}));>> Assert.Equal(ParentAgentBlueprintValue, principal.GetParentAgentBlueprint());>}>>[Fact]> public void GetParentAgentBlueprint_FromClaimsPrincipal_NoClaim_ReturnsNull()>{> var principal =new ClaimsPrincipal();> Assert.Null(principal.GetParentAgentBlueprint());>}>>[Fact]> public void GetParentAgentBlueprint_FromClaimsIdentity_WithClaim_ReturnsValue()>{> var identity =new CaseSensitiveClaimsIdentity(new[]>{>new Claim(ParentAgentBlueprintClaim, ParentAgentBlueprintValue),>});>> Assert.Equal(ParentAgentBlueprintValue, identity.GetParentAgentBlueprint());>}>>[Fact]> public void GetParentAgentBlueprint_FromClaimsIdentity_NoClaim_ReturnsNull()>{> var identity =new CaseSensitiveClaimsIdentity();> Assert.Null(identity.GetParentAgentBlueprint());>}>>[Fact]> public void IsAgentUserIdentity_Principal_SpaceSeparated_Contains13_ReturnsTrue()>{> var principal =new ClaimsPrincipal(>new CaseSensitiveClaimsIdentity(new[]>{>new Claim(SubjectFunctionClaim, "121315"),>}));>> Assert.True(principal.IsAgentUserIdentity());>}>>[Fact]> public void IsAgentUserIdentity_Principal_SpaceSeparated_No13_ReturnsFalse()>{> var principal =new ClaimsPrincipal(>new CaseSensitiveClaimsIdentity(new[]>{>new Claim(SubjectFunctionClaim, "1234"),>}));>> Assert.False(principal.IsAgentUserIdentity());>}>>[Fact]> public void IsAgentUserIdentity_Principal_ExtraWhitespace_Contains13_ReturnsTrue()>{> var principal =new ClaimsPrincipal(>new CaseSensitiveClaimsIdentity(new[]>{>new Claim(SubjectFunctionClaim, " 91321 "),>}));>> Assert.True(principal.IsAgentUserIdentity());>}>>[Fact]> public void IsAgentUserIdentity_Principal_InvalidToken_ReturnsFalse()>{> var principal =new ClaimsPrincipal(>new CaseSensitiveClaimsIdentity(new[]>{>new Claim(SubjectFunctionClaim, "12 a 13"),>}));>> Assert.False(principal.IsAgentUserIdentity());>}>>[Fact]> public void IsAgentUserIdentity_Principal_NoClaim_ReturnsFalse()>{> var principal =new ClaimsPrincipal();> Assert.False(principal.IsAgentUserIdentity());>}>>[Fact]> public void IsAgentUserIdentity_Identity_SpaceSeparated_Contains13_ReturnsTrue()>{> var identity =new CaseSensitiveClaimsIdentity(new[]>{>new Claim(SubjectFunctionClaim, "781321"),>});>> Assert.True(identity.IsAgentUserIdentity());>}>>[Fact]> public void IsAgentUserIdentity_Identity_EmptyString_ReturnsFalse()>{> var identity =new CaseSensitiveClaimsIdentity(new[]>{>new Claim(SubjectFunctionClaim, " "),>});>> Assert.False(identity.IsAgentUserIdentity());>}>>[Fact]> public void IsAgentUserIdentity_Identity_NoClaim_ReturnsFalse()>{> var identity =new CaseSensitiveClaimsIdentity();> Assert.False(identity.IsAgentUserIdentity());>}>}>}> ```
>> Build/test guidance
>- Build the solution.>- Run the test suite; the new tests are in AgentIdentityExtensionsTests.>> Acceptance criteria
>- All new tests pass.>- No regressions in existing tests.>- New methods are available for ClaimsPrincipal and ClaimsIdentity as specified.><!-- START COPILOT CODING AGENT TIPS -->---
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more [Copilot coding agent tips](https://gh.io/copilot-coding-agent-tips) in the docs.

Co-authored-by: jmprieur <13203188+jmprieur@users.noreply.github.com>
CopilotAI changed the title [WIP] Add agent identity extension methods and testsAdd agent identity extension methods for ClaimsPrincipal and ClaimsIdentityOct 2, 2025
…s project
Co-authored-by: jmprieur <13203188+jmprieur@users.noreply.github.com>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM

@jmprieur
Jean-Marc Prieur (jmprieur) marked this pull request as ready for review October 2, 2025 03:09
Comment threadsrc/Microsoft.Identity.Web.AgentIdentities/AgentIdentityExtensions.cs Outdated
Comment threadsrc/Microsoft.Identity.Web.AgentIdentities/AgentIdentityExtensions.cs Outdated
Co-authored-by: keegan-caruso <54109988+keegan-caruso@users.noreply.github.com>
@jmprieur

Copy link
Copy Markdown
Collaborator

Copilot : let's add other asserts using the ClaimsIdentity extension methods in the Agent identity end to end tests under tests/E2E%20Tests/AgentApplications

When the test gets the authorization header, extract the token, read it with JsonWebTokenHandler, and create an untrusted claims identity from the claims in the token.

Co-authored-by: jmprieur <13203188+jmprieur@users.noreply.github.com>

CopilotAI commented Oct 2, 2025

Copy link
Copy Markdown
ContributorAuthor

Copilot : let's add other asserts using the ClaimsIdentity extension methods in the Agent identity end to end tests under tests/E2E%20Tests/AgentApplications

When the test gets the authorization...

Added assertions in all four AgentUserIdentityTests methods (722ac3b). Each test now extracts the token from the authorization header, parses it with JwtSecurityTokenHandler, creates a CaseSensitiveClaimsIdentity from the claims, and uses IsAgentUserIdentity() and GetParentAgentBlueprint() extension methods to validate the token.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Copilot : this is great, I left comments.,
Could we also assert the 2 extension methods on ClaimsIndentity in Autonomous agent tests as well?

Comment threadtests/E2E Tests/AgentApplications/AgentUserIdentityTestscs.cs Outdated
Comment threadtests/E2E Tests/AgentApplications/AgentUserIdentityTestscs.cs Outdated
Comment threadtests/E2E Tests/AgentApplications/AgentUserIdentityTestscs.cs Outdated
Comment threadtests/E2E Tests/AgentApplications/AgentUserIdentityTestscs.cs Outdated
Sign up for freeto 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.

4 participants

@jmprieur@keegan-caruso@saurabhsathe-ms
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Add agent identity extension methods for ClaimsPrincipal and ClaimsIdentity by Copilot · Pull Request #3515 · AzureAD/microsoft-identity-web · GitHub
Skip to content

Add agent identity extension methods for ClaimsPrincipal and ClaimsIdentity - #3515

Merged
Jean-Marc Prieur (jmprieur) merged 11 commits into
masterfrom
copilot/fix-2b1c96f6-8d41-4b94-900e-30503306567e
Oct 8, 2025
Merged

Add agent identity extension methods for ClaimsPrincipal and ClaimsIdentity#3515
Jean-Marc Prieur (jmprieur) merged 11 commits into
masterfrom
copilot/fix-2b1c96f6-8d41-4b94-900e-30503306567e

Conversation

CopilotAI commented Oct 2, 2025

Copy link
Copy Markdown
Contributor
  • Create AgentIdentityExtensions.cs in src/Microsoft.Identity.Web.AgentIdentities with extension methods (moved from TokenCache)
    • GetParentAgentBlueprint for ClaimsPrincipal
    • GetParentAgentBlueprint for ClaimsIdentity
    • IsAgentUserIdentity for ClaimsPrincipal
    • IsAgentUserIdentity for ClaimsIdentity
  • Create AgentIdentityExtensionsTests.cs in tests/Microsoft.Identity.Web.Test with comprehensive tests
    • Tests for GetParentAgentBlueprint (both Principal and Identity)
    • Tests for IsAgentUserIdentity with various scenarios
  • Update PublicAPI.Unshipped.txt files to document new public APIs in correct project
  • Add project reference to Microsoft.Identity.Web.AgentIdentities in test project
  • Build and test to ensure no regressions
  • Verify all tests pass (12 new tests + 627 existing tests = 639 total)
  • Extract magic number 13 into AgentIdUser constant
  • Rename ContainsFunctionCode to ContainsSubjectFacet and update comments
  • Add token validation using ClaimsIdentity extension methods in E2E tests
    • Extract JWT tokens from authorization headers in AgentUserIdentityTests
    • Parse tokens with JwtSecurityTokenHandler and create CaseSensitiveClaimsIdentity
    • Validate tokens using IsAgentUserIdentity() and GetParentAgentBlueprint() extension methods in all four test methods
  • Assert parent blueprint equals agent application in all E2E tests
    • Added assertions in AgentUserIdentityTests (4 test methods)
    • Added assertions in AutonomousAgentTests
Original prompt

Title: Add agent identity extension methods: GetParentAgentBlueprint and IsAgentUserIdentity

Summary
Add extension methods on ClaimsPrincipal and ClaimsIdentity to help developers detect agent identities and retrieve the parent agent blueprint from token claims.

Requirements

  1. Add the following extension methods in a new file under src/Microsoft.Identity.Web.TokenCache:

    • ClaimsPrincipal.GetParentAgentBlueprint(): retrieves the value of the xms_par_app_azp claim if it exists, returns null otherwise.
    • ClaimsIdentity.GetParentAgentBlueprint(): same as above for ClaimsIdentity.
    • ClaimsPrincipal.IsAgentUserIdentity(): returns true if the xms_sub_fct claim exists, is strictly a space-separated string of integers, every token is a valid integer, and the collection contains the integer 13; false otherwise.
    • ClaimsIdentity.IsAgentUserIdentity(): same as above for ClaimsIdentity.
  2. Add unit tests under tests/Microsoft.Identity.Web.Test to validate the above behaviors.

Notes

  • Follow project style used by existing ClaimsPrincipalExtensions.cs (namespace Microsoft.Identity.Web, XML doc comments, null checks).
  • Do not depend on internal helpers like Throws.IfNull or ClaimConstants; use public BCL APIs to keep the new code self-contained.
  • No changes expected to .csproj files.

Proposed changes

// Copyright (c) Microsoft Corporation.// Licensed under the MIT License.usingSystem;usingSystem.Globalization;usingSystem.Security.Claims;namespaceMicrosoft.Identity.Web{/// <summary>/// Extensions to read agent identity-related claims./// </summary>publicstaticclassAgentIdentityExtensions{/// <summary>/// Claim type for the parent agent blueprint./// </summary>privateconststringXmsParAppAzp="xms_par_app_azp";/// <summary>/// Claim type for subject function codes (space-separated integers)./// </summary>privateconststringXmsSubFct="xms_sub_fct";/// <summary>/// Retrieves the parent agent blueprint (xms_par_app_azp) value from a ClaimsPrincipal, if present./// </summary>/// <param name="claimsPrincipal">The claims principal.</param>/// <returns>The value of the xms_par_app_azp claim if it exists; otherwise, null.</returns>publicstaticstring?GetParentAgentBlueprint(thisClaimsPrincipalclaimsPrincipal){if(claimsPrincipalisnull){thrownewArgumentNullException(nameof(claimsPrincipal));}returnclaimsPrincipal.FindFirst(XmsParAppAzp)?.Value;}/// <summary>/// Retrieves the parent agent blueprint (xms_par_app_azp) value from a ClaimsIdentity, if present./// </summary>/// <param name="identity">The claims identity.</param>/// <returns>The value of the xms_par_app_azp claim if it exists; otherwise, null.</returns>publicstaticstring?GetParentAgentBlueprint(thisClaimsIdentityidentity){if(identityisnull){thrownewArgumentNullException(nameof(identity));}returnidentity.FindFirst(XmsParAppAzp)?.Value;}/// <summary>/// Determines whether the ClaimsPrincipal represents an agent user identity./// True if the xms_sub_fct claim exists, is a space-separated string of integers,/// and that collection contains the integer 13./// </summary>/// <param name="claimsPrincipal">The claims principal.</param>/// <returns>True if xms_sub_fct contains 13 and all tokens are integers; otherwise false.</returns>publicstaticboolIsAgentUserIdentity(thisClaimsPrincipalclaimsPrincipal){if(claimsPrincipalisnull){thrownewArgumentNullException(nameof(claimsPrincipal));}varvalue=claimsPrincipal.FindFirst(XmsSubFct)?.Value;returnContainsFunctionCode(value,13);}/// <summary>/// Determines whether the ClaimsIdentity represents an agent user identity./// True if the xms_sub_fct claim exists, is a space-separated string of integers,/// and that collection contains the integer 13./// </summary>/// <param name="identity">The claims identity.</param>/// <returns>True if xms_sub_fct contains 13 and all tokens are integers; otherwise false.</returns>publicstaticboolIsAgentUserIdentity(thisClaimsIdentityidentity){if(identityisnull){thrownewArgumentNullException(nameof(identity));}varvalue=identity.FindFirst(XmsSubFct)?.Value;returnContainsFunctionCode(value,13);}/// <summary>
...</details>*This pull request was created as a result of the following prompt from Copilot chat.*> Title: Add agent identity extension methods: GetParentAgentBlueprint and IsAgentUserIdentity
>> Summary
> Add extension methods on ClaimsPrincipal and ClaimsIdentity to help developers detect agent identities and retrieve the parent agent blueprint from token claims.>> Requirements
>1) Add the following extension methods in a new file under src/Microsoft.Identity.Web.TokenCache:>-ClaimsPrincipal.GetParentAgentBlueprint(): retrieves the value of the xms_par_app_azp claim if it exists, returns null otherwise.>-ClaimsIdentity.GetParentAgentBlueprint(): same as above for ClaimsIdentity.>-ClaimsPrincipal.IsAgentUserIdentity():returns true if the xms_sub_fct claim exists,is strictly a space-separated string of integers,every token is a valid integer,and the collection contains the integer 13; false otherwise.>-ClaimsIdentity.IsAgentUserIdentity():same as above for ClaimsIdentity.>>2)Add unit tests under tests/Microsoft.Identity.Web.Test to validate the above behaviors.>>Notes>- Follow project style used by existing ClaimsPrincipalExtensions.cs(namespaceMicrosoft.Identity.Web,XMLdoc comments,null checks).>- Do not depend on internal helpers like Throws.IfNull or ClaimConstants; use public BCL APIs to keep the new code self-contained.>- No changes expected to .csproj files.>> Proposed changes
>> ```csharp name=src/Microsoft.Identity.Web.TokenCache/AgentIdentityExtensions.cs
>// Copyright (c) Microsoft Corporation.>// Licensed under the MIT License.>>using System;>using System.Globalization;>using System.Security.Claims;>>namespace Microsoft.Identity.Web
>{>/// <summary>>/// Extensions to read agent identity-related claims.>/// </summary>>publicstaticclassAgentIdentityExtensions>{>/// <summary>>/// Claim type for the parent agent blueprint.>/// </summary>>privateconststringXmsParAppAzp="xms_par_app_azp";>>/// <summary>>/// Claim type for subject function codes (space-separated integers).>/// </summary>>privateconststringXmsSubFct="xms_sub_fct";>>/// <summary>>/// Retrieves the parent agent blueprint (xms_par_app_azp) value from a ClaimsPrincipal, if present.>/// </summary>>/// <param name="claimsPrincipal">The claims principal.</param>>/// <returns>The value of the xms_par_app_azp claim if it exists; otherwise, null.</returns>>publicstaticstring?GetParentAgentBlueprint(thisClaimsPrincipalclaimsPrincipal)>{>if(claimsPrincipalisnull)>{>thrownewArgumentNullException(nameof(claimsPrincipal));>}>>return claimsPrincipal.FindFirst(XmsParAppAzp)?.Value;>}>>/// <summary>>/// Retrieves the parent agent blueprint (xms_par_app_azp) value from a ClaimsIdentity, if present.>/// </summary>>/// <param name="identity">The claims identity.</param>>/// <returns>The value of the xms_par_app_azp claim if it exists; otherwise, null.</returns>>publicstatic string? GetParentAgentBlueprint(thisClaimsIdentityidentity)>{>if(identityisnull)>{>thrownewArgumentNullException(nameof(identity));>}>>returnidentity.FindFirst(XmsParAppAzp)?.Value;>}>>/// <summary>>/// Determines whether the ClaimsPrincipal represents an agent user identity.>/// True if the xms_sub_fct claim exists, is a space-separated string of integers,>/// and that collection contains the integer 13.>/// </summary>>/// <param name="claimsPrincipal">The claims principal.</param>>/// <returns>True if xms_sub_fct contains 13 and all tokens are integers; otherwise false.</returns>> public staticbool IsAgentUserIdentity(thisClaimsPrincipalclaimsPrincipal)>{>if(claimsPrincipalisnull)>{>thrownewArgumentNullException(nameof(claimsPrincipal));>}>>var value =claimsPrincipal.FindFirst(XmsSubFct)?.Value;>returnContainsFunctionCode(value,13);>}>>/// <summary>>/// Determines whether the ClaimsIdentity represents an agent user identity.>/// True if the xms_sub_fct claim exists, is a space-separated string of integers,>/// and that collection contains the integer 13.>/// </summary>>/// <param name="identity">The claims identity.</param>>/// <returns>True if xms_sub_fct contains 13 and all tokens are integers; otherwise false.</returns>> public staticboolIsAgentUserIdentity(thisClaimsIdentityidentity)>{>if(identity isnull)>{>thrownew ArgumentNullException(nameof(identity));>}>> var value = identity.FindFirst(XmsSubFct)?.Value;>return ContainsFunctionCode(value,13);>}>>/// <summary>>/// Parses a claim string representing a space-separated collection of integers and checks for a target code.>/// Returns true only if all tokens are valid integers and one equals the target code.>/// </summary>> private staticbool ContainsFunctionCode(string? raw,int targetCode)>{>if(string.IsNullOrWhiteSpace(raw))>{>return false;>}>> var tokens = raw.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);// split on whitespace>bool found = false;>>foreach(var token in tokens)>{>if(!int.TryParse(token, NumberStyles.Integer, CultureInfo.InvariantCulture,outint n))>{>// If any token is non-integer, the claim is not a valid collection of integers.>return false;>}>>if(n == targetCode)>{> found = true;>}>}>>return found;>}>}>}> ```
>> ```csharp name=tests/Microsoft.Identity.Web.Test/AgentIdentityExtensionsTests.cs
>// Copyright (c) Microsoft Corporation.>// Licensed under the MIT License.>>using System.Security.Claims;>using Microsoft.Identity.Web.Test.Common;>using Xunit;>>namespace Microsoft.Identity.Web.Test
>{> public class AgentIdentityExtensionsTests
>{> private const string ParentAgentBlueprintClaim = "xms_par_app_azp";> private const string ParentAgentBlueprintValue = "agent-blueprint-123";>> private const string SubjectFunctionClaim = "xms_sub_fct";>>[Fact]> public void GetParentAgentBlueprint_FromClaimsPrincipal_WithClaim_ReturnsValue()>{> var principal =new ClaimsPrincipal(>new CaseSensitiveClaimsIdentity(new[]>{>new Claim(ParentAgentBlueprintClaim, ParentAgentBlueprintValue),>}));>> Assert.Equal(ParentAgentBlueprintValue, principal.GetParentAgentBlueprint());>}>>[Fact]> public void GetParentAgentBlueprint_FromClaimsPrincipal_NoClaim_ReturnsNull()>{> var principal =new ClaimsPrincipal();> Assert.Null(principal.GetParentAgentBlueprint());>}>>[Fact]> public void GetParentAgentBlueprint_FromClaimsIdentity_WithClaim_ReturnsValue()>{> var identity =new CaseSensitiveClaimsIdentity(new[]>{>new Claim(ParentAgentBlueprintClaim, ParentAgentBlueprintValue),>});>> Assert.Equal(ParentAgentBlueprintValue, identity.GetParentAgentBlueprint());>}>>[Fact]> public void GetParentAgentBlueprint_FromClaimsIdentity_NoClaim_ReturnsNull()>{> var identity =new CaseSensitiveClaimsIdentity();> Assert.Null(identity.GetParentAgentBlueprint());>}>>[Fact]> public void IsAgentUserIdentity_Principal_SpaceSeparated_Contains13_ReturnsTrue()>{> var principal =new ClaimsPrincipal(>new CaseSensitiveClaimsIdentity(new[]>{>new Claim(SubjectFunctionClaim, "121315"),>}));>> Assert.True(principal.IsAgentUserIdentity());>}>>[Fact]> public void IsAgentUserIdentity_Principal_SpaceSeparated_No13_ReturnsFalse()>{> var principal =new ClaimsPrincipal(>new CaseSensitiveClaimsIdentity(new[]>{>new Claim(SubjectFunctionClaim, "1234"),>}));>> Assert.False(principal.IsAgentUserIdentity());>}>>[Fact]> public void IsAgentUserIdentity_Principal_ExtraWhitespace_Contains13_ReturnsTrue()>{> var principal =new ClaimsPrincipal(>new CaseSensitiveClaimsIdentity(new[]>{>new Claim(SubjectFunctionClaim, " 91321 "),>}));>> Assert.True(principal.IsAgentUserIdentity());>}>>[Fact]> public void IsAgentUserIdentity_Principal_InvalidToken_ReturnsFalse()>{> var principal =new ClaimsPrincipal(>new CaseSensitiveClaimsIdentity(new[]>{>new Claim(SubjectFunctionClaim, "12 a 13"),>}));>> Assert.False(principal.IsAgentUserIdentity());>}>>[Fact]> public void IsAgentUserIdentity_Principal_NoClaim_ReturnsFalse()>{> var principal =new ClaimsPrincipal();> Assert.False(principal.IsAgentUserIdentity());>}>>[Fact]> public void IsAgentUserIdentity_Identity_SpaceSeparated_Contains13_ReturnsTrue()>{> var identity =new CaseSensitiveClaimsIdentity(new[]>{>new Claim(SubjectFunctionClaim, "781321"),>});>> Assert.True(identity.IsAgentUserIdentity());>}>>[Fact]> public void IsAgentUserIdentity_Identity_EmptyString_ReturnsFalse()>{> var identity =new CaseSensitiveClaimsIdentity(new[]>{>new Claim(SubjectFunctionClaim, " "),>});>> Assert.False(identity.IsAgentUserIdentity());>}>>[Fact]> public void IsAgentUserIdentity_Identity_NoClaim_ReturnsFalse()>{> var identity =new CaseSensitiveClaimsIdentity();> Assert.False(identity.IsAgentUserIdentity());>}>}>}> ```
>> Build/test guidance
>- Build the solution.>- Run the test suite; the new tests are in AgentIdentityExtensionsTests.>> Acceptance criteria
>- All new tests pass.>- No regressions in existing tests.>- New methods are available for ClaimsPrincipal and ClaimsIdentity as specified.><!-- START COPILOT CODING AGENT TIPS -->---
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more [Copilot coding agent tips](https://gh.io/copilot-coding-agent-tips) in the docs.

Co-authored-by: jmprieur <13203188+jmprieur@users.noreply.github.com>
CopilotAI changed the title [WIP] Add agent identity extension methods and testsAdd agent identity extension methods for ClaimsPrincipal and ClaimsIdentityOct 2, 2025
…s project
Co-authored-by: jmprieur <13203188+jmprieur@users.noreply.github.com>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM

@jmprieur
Jean-Marc Prieur (jmprieur) marked this pull request as ready for review October 2, 2025 03:09
Comment threadsrc/Microsoft.Identity.Web.AgentIdentities/AgentIdentityExtensions.cs Outdated
Comment threadsrc/Microsoft.Identity.Web.AgentIdentities/AgentIdentityExtensions.cs Outdated
Co-authored-by: keegan-caruso <54109988+keegan-caruso@users.noreply.github.com>
@jmprieur

Copy link
Copy Markdown
Collaborator

Copilot : let's add other asserts using the ClaimsIdentity extension methods in the Agent identity end to end tests under tests/E2E%20Tests/AgentApplications

When the test gets the authorization header, extract the token, read it with JsonWebTokenHandler, and create an untrusted claims identity from the claims in the token.

Co-authored-by: jmprieur <13203188+jmprieur@users.noreply.github.com>

CopilotAI commented Oct 2, 2025

Copy link
Copy Markdown
ContributorAuthor

Copilot : let's add other asserts using the ClaimsIdentity extension methods in the Agent identity end to end tests under tests/E2E%20Tests/AgentApplications

When the test gets the authorization...

Added assertions in all four AgentUserIdentityTests methods (722ac3b). Each test now extracts the token from the authorization header, parses it with JwtSecurityTokenHandler, creates a CaseSensitiveClaimsIdentity from the claims, and uses IsAgentUserIdentity() and GetParentAgentBlueprint() extension methods to validate the token.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Copilot : this is great, I left comments.,
Could we also assert the 2 extension methods on ClaimsIndentity in Autonomous agent tests as well?

Comment threadtests/E2E Tests/AgentApplications/AgentUserIdentityTestscs.cs Outdated
Comment threadtests/E2E Tests/AgentApplications/AgentUserIdentityTestscs.cs Outdated
Comment threadtests/E2E Tests/AgentApplications/AgentUserIdentityTestscs.cs Outdated
Comment threadtests/E2E Tests/AgentApplications/AgentUserIdentityTestscs.cs Outdated
Sign up for freeto 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.

4 participants

@jmprieur@keegan-caruso@saurabhsathe-ms
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Add agent identity extension methods for ClaimsPrincipal and ClaimsIdentity by Copilot · Pull Request #3515 · AzureAD/microsoft-identity-web · GitHub
Skip to content

Add agent identity extension methods for ClaimsPrincipal and ClaimsIdentity - #3515

Merged
Jean-Marc Prieur (jmprieur) merged 11 commits into
masterfrom
copilot/fix-2b1c96f6-8d41-4b94-900e-30503306567e
Oct 8, 2025
Merged

Add agent identity extension methods for ClaimsPrincipal and ClaimsIdentity#3515
Jean-Marc Prieur (jmprieur) merged 11 commits into
masterfrom
copilot/fix-2b1c96f6-8d41-4b94-900e-30503306567e

Conversation

CopilotAI commented Oct 2, 2025

Copy link
Copy Markdown
Contributor
  • Create AgentIdentityExtensions.cs in src/Microsoft.Identity.Web.AgentIdentities with extension methods (moved from TokenCache)
    • GetParentAgentBlueprint for ClaimsPrincipal
    • GetParentAgentBlueprint for ClaimsIdentity
    • IsAgentUserIdentity for ClaimsPrincipal
    • IsAgentUserIdentity for ClaimsIdentity
  • Create AgentIdentityExtensionsTests.cs in tests/Microsoft.Identity.Web.Test with comprehensive tests
    • Tests for GetParentAgentBlueprint (both Principal and Identity)
    • Tests for IsAgentUserIdentity with various scenarios
  • Update PublicAPI.Unshipped.txt files to document new public APIs in correct project
  • Add project reference to Microsoft.Identity.Web.AgentIdentities in test project
  • Build and test to ensure no regressions
  • Verify all tests pass (12 new tests + 627 existing tests = 639 total)
  • Extract magic number 13 into AgentIdUser constant
  • Rename ContainsFunctionCode to ContainsSubjectFacet and update comments
  • Add token validation using ClaimsIdentity extension methods in E2E tests
    • Extract JWT tokens from authorization headers in AgentUserIdentityTests
    • Parse tokens with JwtSecurityTokenHandler and create CaseSensitiveClaimsIdentity
    • Validate tokens using IsAgentUserIdentity() and GetParentAgentBlueprint() extension methods in all four test methods
  • Assert parent blueprint equals agent application in all E2E tests
    • Added assertions in AgentUserIdentityTests (4 test methods)
    • Added assertions in AutonomousAgentTests
Original prompt

Title: Add agent identity extension methods: GetParentAgentBlueprint and IsAgentUserIdentity

Summary
Add extension methods on ClaimsPrincipal and ClaimsIdentity to help developers detect agent identities and retrieve the parent agent blueprint from token claims.

Requirements

  1. Add the following extension methods in a new file under src/Microsoft.Identity.Web.TokenCache:

    • ClaimsPrincipal.GetParentAgentBlueprint(): retrieves the value of the xms_par_app_azp claim if it exists, returns null otherwise.
    • ClaimsIdentity.GetParentAgentBlueprint(): same as above for ClaimsIdentity.
    • ClaimsPrincipal.IsAgentUserIdentity(): returns true if the xms_sub_fct claim exists, is strictly a space-separated string of integers, every token is a valid integer, and the collection contains the integer 13; false otherwise.
    • ClaimsIdentity.IsAgentUserIdentity(): same as above for ClaimsIdentity.
  2. Add unit tests under tests/Microsoft.Identity.Web.Test to validate the above behaviors.

Notes

  • Follow project style used by existing ClaimsPrincipalExtensions.cs (namespace Microsoft.Identity.Web, XML doc comments, null checks).
  • Do not depend on internal helpers like Throws.IfNull or ClaimConstants; use public BCL APIs to keep the new code self-contained.
  • No changes expected to .csproj files.

Proposed changes

// Copyright (c) Microsoft Corporation.// Licensed under the MIT License.usingSystem;usingSystem.Globalization;usingSystem.Security.Claims;namespaceMicrosoft.Identity.Web{/// <summary>/// Extensions to read agent identity-related claims./// </summary>publicstaticclassAgentIdentityExtensions{/// <summary>/// Claim type for the parent agent blueprint./// </summary>privateconststringXmsParAppAzp="xms_par_app_azp";/// <summary>/// Claim type for subject function codes (space-separated integers)./// </summary>privateconststringXmsSubFct="xms_sub_fct";/// <summary>/// Retrieves the parent agent blueprint (xms_par_app_azp) value from a ClaimsPrincipal, if present./// </summary>/// <param name="claimsPrincipal">The claims principal.</param>/// <returns>The value of the xms_par_app_azp claim if it exists; otherwise, null.</returns>publicstaticstring?GetParentAgentBlueprint(thisClaimsPrincipalclaimsPrincipal){if(claimsPrincipalisnull){thrownewArgumentNullException(nameof(claimsPrincipal));}returnclaimsPrincipal.FindFirst(XmsParAppAzp)?.Value;}/// <summary>/// Retrieves the parent agent blueprint (xms_par_app_azp) value from a ClaimsIdentity, if present./// </summary>/// <param name="identity">The claims identity.</param>/// <returns>The value of the xms_par_app_azp claim if it exists; otherwise, null.</returns>publicstaticstring?GetParentAgentBlueprint(thisClaimsIdentityidentity){if(identityisnull){thrownewArgumentNullException(nameof(identity));}returnidentity.FindFirst(XmsParAppAzp)?.Value;}/// <summary>/// Determines whether the ClaimsPrincipal represents an agent user identity./// True if the xms_sub_fct claim exists, is a space-separated string of integers,/// and that collection contains the integer 13./// </summary>/// <param name="claimsPrincipal">The claims principal.</param>/// <returns>True if xms_sub_fct contains 13 and all tokens are integers; otherwise false.</returns>publicstaticboolIsAgentUserIdentity(thisClaimsPrincipalclaimsPrincipal){if(claimsPrincipalisnull){thrownewArgumentNullException(nameof(claimsPrincipal));}varvalue=claimsPrincipal.FindFirst(XmsSubFct)?.Value;returnContainsFunctionCode(value,13);}/// <summary>/// Determines whether the ClaimsIdentity represents an agent user identity./// True if the xms_sub_fct claim exists, is a space-separated string of integers,/// and that collection contains the integer 13./// </summary>/// <param name="identity">The claims identity.</param>/// <returns>True if xms_sub_fct contains 13 and all tokens are integers; otherwise false.</returns>publicstaticboolIsAgentUserIdentity(thisClaimsIdentityidentity){if(identityisnull){thrownewArgumentNullException(nameof(identity));}varvalue=identity.FindFirst(XmsSubFct)?.Value;returnContainsFunctionCode(value,13);}/// <summary>
...</details>*This pull request was created as a result of the following prompt from Copilot chat.*> Title: Add agent identity extension methods: GetParentAgentBlueprint and IsAgentUserIdentity
>> Summary
> Add extension methods on ClaimsPrincipal and ClaimsIdentity to help developers detect agent identities and retrieve the parent agent blueprint from token claims.>> Requirements
>1) Add the following extension methods in a new file under src/Microsoft.Identity.Web.TokenCache:>-ClaimsPrincipal.GetParentAgentBlueprint(): retrieves the value of the xms_par_app_azp claim if it exists, returns null otherwise.>-ClaimsIdentity.GetParentAgentBlueprint(): same as above for ClaimsIdentity.>-ClaimsPrincipal.IsAgentUserIdentity():returns true if the xms_sub_fct claim exists,is strictly a space-separated string of integers,every token is a valid integer,and the collection contains the integer 13; false otherwise.>-ClaimsIdentity.IsAgentUserIdentity():same as above for ClaimsIdentity.>>2)Add unit tests under tests/Microsoft.Identity.Web.Test to validate the above behaviors.>>Notes>- Follow project style used by existing ClaimsPrincipalExtensions.cs(namespaceMicrosoft.Identity.Web,XMLdoc comments,null checks).>- Do not depend on internal helpers like Throws.IfNull or ClaimConstants; use public BCL APIs to keep the new code self-contained.>- No changes expected to .csproj files.>> Proposed changes
>> ```csharp name=src/Microsoft.Identity.Web.TokenCache/AgentIdentityExtensions.cs
>// Copyright (c) Microsoft Corporation.>// Licensed under the MIT License.>>using System;>using System.Globalization;>using System.Security.Claims;>>namespace Microsoft.Identity.Web
>{>/// <summary>>/// Extensions to read agent identity-related claims.>/// </summary>>publicstaticclassAgentIdentityExtensions>{>/// <summary>>/// Claim type for the parent agent blueprint.>/// </summary>>privateconststringXmsParAppAzp="xms_par_app_azp";>>/// <summary>>/// Claim type for subject function codes (space-separated integers).>/// </summary>>privateconststringXmsSubFct="xms_sub_fct";>>/// <summary>>/// Retrieves the parent agent blueprint (xms_par_app_azp) value from a ClaimsPrincipal, if present.>/// </summary>>/// <param name="claimsPrincipal">The claims principal.</param>>/// <returns>The value of the xms_par_app_azp claim if it exists; otherwise, null.</returns>>publicstaticstring?GetParentAgentBlueprint(thisClaimsPrincipalclaimsPrincipal)>{>if(claimsPrincipalisnull)>{>thrownewArgumentNullException(nameof(claimsPrincipal));>}>>return claimsPrincipal.FindFirst(XmsParAppAzp)?.Value;>}>>/// <summary>>/// Retrieves the parent agent blueprint (xms_par_app_azp) value from a ClaimsIdentity, if present.>/// </summary>>/// <param name="identity">The claims identity.</param>>/// <returns>The value of the xms_par_app_azp claim if it exists; otherwise, null.</returns>>publicstatic string? GetParentAgentBlueprint(thisClaimsIdentityidentity)>{>if(identityisnull)>{>thrownewArgumentNullException(nameof(identity));>}>>returnidentity.FindFirst(XmsParAppAzp)?.Value;>}>>/// <summary>>/// Determines whether the ClaimsPrincipal represents an agent user identity.>/// True if the xms_sub_fct claim exists, is a space-separated string of integers,>/// and that collection contains the integer 13.>/// </summary>>/// <param name="claimsPrincipal">The claims principal.</param>>/// <returns>True if xms_sub_fct contains 13 and all tokens are integers; otherwise false.</returns>> public staticbool IsAgentUserIdentity(thisClaimsPrincipalclaimsPrincipal)>{>if(claimsPrincipalisnull)>{>thrownewArgumentNullException(nameof(claimsPrincipal));>}>>var value =claimsPrincipal.FindFirst(XmsSubFct)?.Value;>returnContainsFunctionCode(value,13);>}>>/// <summary>>/// Determines whether the ClaimsIdentity represents an agent user identity.>/// True if the xms_sub_fct claim exists, is a space-separated string of integers,>/// and that collection contains the integer 13.>/// </summary>>/// <param name="identity">The claims identity.</param>>/// <returns>True if xms_sub_fct contains 13 and all tokens are integers; otherwise false.</returns>> public staticboolIsAgentUserIdentity(thisClaimsIdentityidentity)>{>if(identity isnull)>{>thrownew ArgumentNullException(nameof(identity));>}>> var value = identity.FindFirst(XmsSubFct)?.Value;>return ContainsFunctionCode(value,13);>}>>/// <summary>>/// Parses a claim string representing a space-separated collection of integers and checks for a target code.>/// Returns true only if all tokens are valid integers and one equals the target code.>/// </summary>> private staticbool ContainsFunctionCode(string? raw,int targetCode)>{>if(string.IsNullOrWhiteSpace(raw))>{>return false;>}>> var tokens = raw.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);// split on whitespace>bool found = false;>>foreach(var token in tokens)>{>if(!int.TryParse(token, NumberStyles.Integer, CultureInfo.InvariantCulture,outint n))>{>// If any token is non-integer, the claim is not a valid collection of integers.>return false;>}>>if(n == targetCode)>{> found = true;>}>}>>return found;>}>}>}> ```
>> ```csharp name=tests/Microsoft.Identity.Web.Test/AgentIdentityExtensionsTests.cs
>// Copyright (c) Microsoft Corporation.>// Licensed under the MIT License.>>using System.Security.Claims;>using Microsoft.Identity.Web.Test.Common;>using Xunit;>>namespace Microsoft.Identity.Web.Test
>{> public class AgentIdentityExtensionsTests
>{> private const string ParentAgentBlueprintClaim = "xms_par_app_azp";> private const string ParentAgentBlueprintValue = "agent-blueprint-123";>> private const string SubjectFunctionClaim = "xms_sub_fct";>>[Fact]> public void GetParentAgentBlueprint_FromClaimsPrincipal_WithClaim_ReturnsValue()>{> var principal =new ClaimsPrincipal(>new CaseSensitiveClaimsIdentity(new[]>{>new Claim(ParentAgentBlueprintClaim, ParentAgentBlueprintValue),>}));>> Assert.Equal(ParentAgentBlueprintValue, principal.GetParentAgentBlueprint());>}>>[Fact]> public void GetParentAgentBlueprint_FromClaimsPrincipal_NoClaim_ReturnsNull()>{> var principal =new ClaimsPrincipal();> Assert.Null(principal.GetParentAgentBlueprint());>}>>[Fact]> public void GetParentAgentBlueprint_FromClaimsIdentity_WithClaim_ReturnsValue()>{> var identity =new CaseSensitiveClaimsIdentity(new[]>{>new Claim(ParentAgentBlueprintClaim, ParentAgentBlueprintValue),>});>> Assert.Equal(ParentAgentBlueprintValue, identity.GetParentAgentBlueprint());>}>>[Fact]> public void GetParentAgentBlueprint_FromClaimsIdentity_NoClaim_ReturnsNull()>{> var identity =new CaseSensitiveClaimsIdentity();> Assert.Null(identity.GetParentAgentBlueprint());>}>>[Fact]> public void IsAgentUserIdentity_Principal_SpaceSeparated_Contains13_ReturnsTrue()>{> var principal =new ClaimsPrincipal(>new CaseSensitiveClaimsIdentity(new[]>{>new Claim(SubjectFunctionClaim, "121315"),>}));>> Assert.True(principal.IsAgentUserIdentity());>}>>[Fact]> public void IsAgentUserIdentity_Principal_SpaceSeparated_No13_ReturnsFalse()>{> var principal =new ClaimsPrincipal(>new CaseSensitiveClaimsIdentity(new[]>{>new Claim(SubjectFunctionClaim, "1234"),>}));>> Assert.False(principal.IsAgentUserIdentity());>}>>[Fact]> public void IsAgentUserIdentity_Principal_ExtraWhitespace_Contains13_ReturnsTrue()>{> var principal =new ClaimsPrincipal(>new CaseSensitiveClaimsIdentity(new[]>{>new Claim(SubjectFunctionClaim, " 91321 "),>}));>> Assert.True(principal.IsAgentUserIdentity());>}>>[Fact]> public void IsAgentUserIdentity_Principal_InvalidToken_ReturnsFalse()>{> var principal =new ClaimsPrincipal(>new CaseSensitiveClaimsIdentity(new[]>{>new Claim(SubjectFunctionClaim, "12 a 13"),>}));>> Assert.False(principal.IsAgentUserIdentity());>}>>[Fact]> public void IsAgentUserIdentity_Principal_NoClaim_ReturnsFalse()>{> var principal =new ClaimsPrincipal();> Assert.False(principal.IsAgentUserIdentity());>}>>[Fact]> public void IsAgentUserIdentity_Identity_SpaceSeparated_Contains13_ReturnsTrue()>{> var identity =new CaseSensitiveClaimsIdentity(new[]>{>new Claim(SubjectFunctionClaim, "781321"),>});>> Assert.True(identity.IsAgentUserIdentity());>}>>[Fact]> public void IsAgentUserIdentity_Identity_EmptyString_ReturnsFalse()>{> var identity =new CaseSensitiveClaimsIdentity(new[]>{>new Claim(SubjectFunctionClaim, " "),>});>> Assert.False(identity.IsAgentUserIdentity());>}>>[Fact]> public void IsAgentUserIdentity_Identity_NoClaim_ReturnsFalse()>{> var identity =new CaseSensitiveClaimsIdentity();> Assert.False(identity.IsAgentUserIdentity());>}>}>}> ```
>> Build/test guidance
>- Build the solution.>- Run the test suite; the new tests are in AgentIdentityExtensionsTests.>> Acceptance criteria
>- All new tests pass.>- No regressions in existing tests.>- New methods are available for ClaimsPrincipal and ClaimsIdentity as specified.><!-- START COPILOT CODING AGENT TIPS -->---
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more [Copilot coding agent tips](https://gh.io/copilot-coding-agent-tips) in the docs.

Co-authored-by: jmprieur <13203188+jmprieur@users.noreply.github.com>
CopilotAI changed the title [WIP] Add agent identity extension methods and testsAdd agent identity extension methods for ClaimsPrincipal and ClaimsIdentityOct 2, 2025
…s project
Co-authored-by: jmprieur <13203188+jmprieur@users.noreply.github.com>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM

@jmprieur
Jean-Marc Prieur (jmprieur) marked this pull request as ready for review October 2, 2025 03:09
Comment threadsrc/Microsoft.Identity.Web.AgentIdentities/AgentIdentityExtensions.cs Outdated
Comment threadsrc/Microsoft.Identity.Web.AgentIdentities/AgentIdentityExtensions.cs Outdated
Co-authored-by: keegan-caruso <54109988+keegan-caruso@users.noreply.github.com>
@jmprieur

Copy link
Copy Markdown
Collaborator

Copilot : let's add other asserts using the ClaimsIdentity extension methods in the Agent identity end to end tests under tests/E2E%20Tests/AgentApplications

When the test gets the authorization header, extract the token, read it with JsonWebTokenHandler, and create an untrusted claims identity from the claims in the token.

Co-authored-by: jmprieur <13203188+jmprieur@users.noreply.github.com>

CopilotAI commented Oct 2, 2025

Copy link
Copy Markdown
ContributorAuthor

Copilot : let's add other asserts using the ClaimsIdentity extension methods in the Agent identity end to end tests under tests/E2E%20Tests/AgentApplications

When the test gets the authorization...

Added assertions in all four AgentUserIdentityTests methods (722ac3b). Each test now extracts the token from the authorization header, parses it with JwtSecurityTokenHandler, creates a CaseSensitiveClaimsIdentity from the claims, and uses IsAgentUserIdentity() and GetParentAgentBlueprint() extension methods to validate the token.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Copilot : this is great, I left comments.,
Could we also assert the 2 extension methods on ClaimsIndentity in Autonomous agent tests as well?

Comment threadtests/E2E Tests/AgentApplications/AgentUserIdentityTestscs.cs Outdated
Comment threadtests/E2E Tests/AgentApplications/AgentUserIdentityTestscs.cs Outdated
Comment threadtests/E2E Tests/AgentApplications/AgentUserIdentityTestscs.cs Outdated
Comment threadtests/E2E Tests/AgentApplications/AgentUserIdentityTestscs.cs Outdated
Sign up for freeto 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.

4 participants

@jmprieur@keegan-caruso@saurabhsathe-ms
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); Add agent identity extension methods for ClaimsPrincipal and ClaimsIdentity by Copilot · Pull Request #3515 · AzureAD/microsoft-identity-web · GitHub
Skip to content

Add agent identity extension methods for ClaimsPrincipal and ClaimsIdentity - #3515

Merged
Jean-Marc Prieur (jmprieur) merged 11 commits into
masterfrom
copilot/fix-2b1c96f6-8d41-4b94-900e-30503306567e
Oct 8, 2025
Merged

Add agent identity extension methods for ClaimsPrincipal and ClaimsIdentity#3515
Jean-Marc Prieur (jmprieur) merged 11 commits into
masterfrom
copilot/fix-2b1c96f6-8d41-4b94-900e-30503306567e

Conversation

CopilotAI commented Oct 2, 2025

Copy link
Copy Markdown
Contributor
  • Create AgentIdentityExtensions.cs in src/Microsoft.Identity.Web.AgentIdentities with extension methods (moved from TokenCache)
    • GetParentAgentBlueprint for ClaimsPrincipal
    • GetParentAgentBlueprint for ClaimsIdentity
    • IsAgentUserIdentity for ClaimsPrincipal
    • IsAgentUserIdentity for ClaimsIdentity
  • Create AgentIdentityExtensionsTests.cs in tests/Microsoft.Identity.Web.Test with comprehensive tests
    • Tests for GetParentAgentBlueprint (both Principal and Identity)
    • Tests for IsAgentUserIdentity with various scenarios
  • Update PublicAPI.Unshipped.txt files to document new public APIs in correct project
  • Add project reference to Microsoft.Identity.Web.AgentIdentities in test project
  • Build and test to ensure no regressions
  • Verify all tests pass (12 new tests + 627 existing tests = 639 total)
  • Extract magic number 13 into AgentIdUser constant
  • Rename ContainsFunctionCode to ContainsSubjectFacet and update comments
  • Add token validation using ClaimsIdentity extension methods in E2E tests
    • Extract JWT tokens from authorization headers in AgentUserIdentityTests
    • Parse tokens with JwtSecurityTokenHandler and create CaseSensitiveClaimsIdentity
    • Validate tokens using IsAgentUserIdentity() and GetParentAgentBlueprint() extension methods in all four test methods
  • Assert parent blueprint equals agent application in all E2E tests
    • Added assertions in AgentUserIdentityTests (4 test methods)
    • Added assertions in AutonomousAgentTests
Original prompt

Title: Add agent identity extension methods: GetParentAgentBlueprint and IsAgentUserIdentity

Summary
Add extension methods on ClaimsPrincipal and ClaimsIdentity to help developers detect agent identities and retrieve the parent agent blueprint from token claims.

Requirements

  1. Add the following extension methods in a new file under src/Microsoft.Identity.Web.TokenCache:

    • ClaimsPrincipal.GetParentAgentBlueprint(): retrieves the value of the xms_par_app_azp claim if it exists, returns null otherwise.
    • ClaimsIdentity.GetParentAgentBlueprint(): same as above for ClaimsIdentity.
    • ClaimsPrincipal.IsAgentUserIdentity(): returns true if the xms_sub_fct claim exists, is strictly a space-separated string of integers, every token is a valid integer, and the collection contains the integer 13; false otherwise.
    • ClaimsIdentity.IsAgentUserIdentity(): same as above for ClaimsIdentity.
  2. Add unit tests under tests/Microsoft.Identity.Web.Test to validate the above behaviors.

Notes

  • Follow project style used by existing ClaimsPrincipalExtensions.cs (namespace Microsoft.Identity.Web, XML doc comments, null checks).
  • Do not depend on internal helpers like Throws.IfNull or ClaimConstants; use public BCL APIs to keep the new code self-contained.
  • No changes expected to .csproj files.

Proposed changes

// Copyright (c) Microsoft Corporation.// Licensed under the MIT License.usingSystem;usingSystem.Globalization;usingSystem.Security.Claims;namespaceMicrosoft.Identity.Web{/// <summary>/// Extensions to read agent identity-related claims./// </summary>publicstaticclassAgentIdentityExtensions{/// <summary>/// Claim type for the parent agent blueprint./// </summary>privateconststringXmsParAppAzp="xms_par_app_azp";/// <summary>/// Claim type for subject function codes (space-separated integers)./// </summary>privateconststringXmsSubFct="xms_sub_fct";/// <summary>/// Retrieves the parent agent blueprint (xms_par_app_azp) value from a ClaimsPrincipal, if present./// </summary>/// <param name="claimsPrincipal">The claims principal.</param>/// <returns>The value of the xms_par_app_azp claim if it exists; otherwise, null.</returns>publicstaticstring?GetParentAgentBlueprint(thisClaimsPrincipalclaimsPrincipal){if(claimsPrincipalisnull){thrownewArgumentNullException(nameof(claimsPrincipal));}returnclaimsPrincipal.FindFirst(XmsParAppAzp)?.Value;}/// <summary>/// Retrieves the parent agent blueprint (xms_par_app_azp) value from a ClaimsIdentity, if present./// </summary>/// <param name="identity">The claims identity.</param>/// <returns>The value of the xms_par_app_azp claim if it exists; otherwise, null.</returns>publicstaticstring?GetParentAgentBlueprint(thisClaimsIdentityidentity){if(identityisnull){thrownewArgumentNullException(nameof(identity));}returnidentity.FindFirst(XmsParAppAzp)?.Value;}/// <summary>/// Determines whether the ClaimsPrincipal represents an agent user identity./// True if the xms_sub_fct claim exists, is a space-separated string of integers,/// and that collection contains the integer 13./// </summary>/// <param name="claimsPrincipal">The claims principal.</param>/// <returns>True if xms_sub_fct contains 13 and all tokens are integers; otherwise false.</returns>publicstaticboolIsAgentUserIdentity(thisClaimsPrincipalclaimsPrincipal){if(claimsPrincipalisnull){thrownewArgumentNullException(nameof(claimsPrincipal));}varvalue=claimsPrincipal.FindFirst(XmsSubFct)?.Value;returnContainsFunctionCode(value,13);}/// <summary>/// Determines whether the ClaimsIdentity represents an agent user identity./// True if the xms_sub_fct claim exists, is a space-separated string of integers,/// and that collection contains the integer 13./// </summary>/// <param name="identity">The claims identity.</param>/// <returns>True if xms_sub_fct contains 13 and all tokens are integers; otherwise false.</returns>publicstaticboolIsAgentUserIdentity(thisClaimsIdentityidentity){if(identityisnull){thrownewArgumentNullException(nameof(identity));}varvalue=identity.FindFirst(XmsSubFct)?.Value;returnContainsFunctionCode(value,13);}/// <summary>
...</details>*This pull request was created as a result of the following prompt from Copilot chat.*> Title: Add agent identity extension methods: GetParentAgentBlueprint and IsAgentUserIdentity
>> Summary
> Add extension methods on ClaimsPrincipal and ClaimsIdentity to help developers detect agent identities and retrieve the parent agent blueprint from token claims.>> Requirements
>1) Add the following extension methods in a new file under src/Microsoft.Identity.Web.TokenCache:>-ClaimsPrincipal.GetParentAgentBlueprint(): retrieves the value of the xms_par_app_azp claim if it exists, returns null otherwise.>-ClaimsIdentity.GetParentAgentBlueprint(): same as above for ClaimsIdentity.>-ClaimsPrincipal.IsAgentUserIdentity():returns true if the xms_sub_fct claim exists,is strictly a space-separated string of integers,every token is a valid integer,and the collection contains the integer 13; false otherwise.>-ClaimsIdentity.IsAgentUserIdentity():same as above for ClaimsIdentity.>>2)Add unit tests under tests/Microsoft.Identity.Web.Test to validate the above behaviors.>>Notes>- Follow project style used by existing ClaimsPrincipalExtensions.cs(namespaceMicrosoft.Identity.Web,XMLdoc comments,null checks).>- Do not depend on internal helpers like Throws.IfNull or ClaimConstants; use public BCL APIs to keep the new code self-contained.>- No changes expected to .csproj files.>> Proposed changes
>> ```csharp name=src/Microsoft.Identity.Web.TokenCache/AgentIdentityExtensions.cs
>// Copyright (c) Microsoft Corporation.>// Licensed under the MIT License.>>using System;>using System.Globalization;>using System.Security.Claims;>>namespace Microsoft.Identity.Web
>{>/// <summary>>/// Extensions to read agent identity-related claims.>/// </summary>>publicstaticclassAgentIdentityExtensions>{>/// <summary>>/// Claim type for the parent agent blueprint.>/// </summary>>privateconststringXmsParAppAzp="xms_par_app_azp";>>/// <summary>>/// Claim type for subject function codes (space-separated integers).>/// </summary>>privateconststringXmsSubFct="xms_sub_fct";>>/// <summary>>/// Retrieves the parent agent blueprint (xms_par_app_azp) value from a ClaimsPrincipal, if present.>/// </summary>>/// <param name="claimsPrincipal">The claims principal.</param>>/// <returns>The value of the xms_par_app_azp claim if it exists; otherwise, null.</returns>>publicstaticstring?GetParentAgentBlueprint(thisClaimsPrincipalclaimsPrincipal)>{>if(claimsPrincipalisnull)>{>thrownewArgumentNullException(nameof(claimsPrincipal));>}>>return claimsPrincipal.FindFirst(XmsParAppAzp)?.Value;>}>>/// <summary>>/// Retrieves the parent agent blueprint (xms_par_app_azp) value from a ClaimsIdentity, if present.>/// </summary>>/// <param name="identity">The claims identity.</param>>/// <returns>The value of the xms_par_app_azp claim if it exists; otherwise, null.</returns>>publicstatic string? GetParentAgentBlueprint(thisClaimsIdentityidentity)>{>if(identityisnull)>{>thrownewArgumentNullException(nameof(identity));>}>>returnidentity.FindFirst(XmsParAppAzp)?.Value;>}>>/// <summary>>/// Determines whether the ClaimsPrincipal represents an agent user identity.>/// True if the xms_sub_fct claim exists, is a space-separated string of integers,>/// and that collection contains the integer 13.>/// </summary>>/// <param name="claimsPrincipal">The claims principal.</param>>/// <returns>True if xms_sub_fct contains 13 and all tokens are integers; otherwise false.</returns>> public staticbool IsAgentUserIdentity(thisClaimsPrincipalclaimsPrincipal)>{>if(claimsPrincipalisnull)>{>thrownewArgumentNullException(nameof(claimsPrincipal));>}>>var value =claimsPrincipal.FindFirst(XmsSubFct)?.Value;>returnContainsFunctionCode(value,13);>}>>/// <summary>>/// Determines whether the ClaimsIdentity represents an agent user identity.>/// True if the xms_sub_fct claim exists, is a space-separated string of integers,>/// and that collection contains the integer 13.>/// </summary>>/// <param name="identity">The claims identity.</param>>/// <returns>True if xms_sub_fct contains 13 and all tokens are integers; otherwise false.</returns>> public staticboolIsAgentUserIdentity(thisClaimsIdentityidentity)>{>if(identity isnull)>{>thrownew ArgumentNullException(nameof(identity));>}>> var value = identity.FindFirst(XmsSubFct)?.Value;>return ContainsFunctionCode(value,13);>}>>/// <summary>>/// Parses a claim string representing a space-separated collection of integers and checks for a target code.>/// Returns true only if all tokens are valid integers and one equals the target code.>/// </summary>> private staticbool ContainsFunctionCode(string? raw,int targetCode)>{>if(string.IsNullOrWhiteSpace(raw))>{>return false;>}>> var tokens = raw.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);// split on whitespace>bool found = false;>>foreach(var token in tokens)>{>if(!int.TryParse(token, NumberStyles.Integer, CultureInfo.InvariantCulture,outint n))>{>// If any token is non-integer, the claim is not a valid collection of integers.>return false;>}>>if(n == targetCode)>{> found = true;>}>}>>return found;>}>}>}> ```
>> ```csharp name=tests/Microsoft.Identity.Web.Test/AgentIdentityExtensionsTests.cs
>// Copyright (c) Microsoft Corporation.>// Licensed under the MIT License.>>using System.Security.Claims;>using Microsoft.Identity.Web.Test.Common;>using Xunit;>>namespace Microsoft.Identity.Web.Test
>{> public class AgentIdentityExtensionsTests
>{> private const string ParentAgentBlueprintClaim = "xms_par_app_azp";> private const string ParentAgentBlueprintValue = "agent-blueprint-123";>> private const string SubjectFunctionClaim = "xms_sub_fct";>>[Fact]> public void GetParentAgentBlueprint_FromClaimsPrincipal_WithClaim_ReturnsValue()>{> var principal =new ClaimsPrincipal(>new CaseSensitiveClaimsIdentity(new[]>{>new Claim(ParentAgentBlueprintClaim, ParentAgentBlueprintValue),>}));>> Assert.Equal(ParentAgentBlueprintValue, principal.GetParentAgentBlueprint());>}>>[Fact]> public void GetParentAgentBlueprint_FromClaimsPrincipal_NoClaim_ReturnsNull()>{> var principal =new ClaimsPrincipal();> Assert.Null(principal.GetParentAgentBlueprint());>}>>[Fact]> public void GetParentAgentBlueprint_FromClaimsIdentity_WithClaim_ReturnsValue()>{> var identity =new CaseSensitiveClaimsIdentity(new[]>{>new Claim(ParentAgentBlueprintClaim, ParentAgentBlueprintValue),>});>> Assert.Equal(ParentAgentBlueprintValue, identity.GetParentAgentBlueprint());>}>>[Fact]> public void GetParentAgentBlueprint_FromClaimsIdentity_NoClaim_ReturnsNull()>{> var identity =new CaseSensitiveClaimsIdentity();> Assert.Null(identity.GetParentAgentBlueprint());>}>>[Fact]> public void IsAgentUserIdentity_Principal_SpaceSeparated_Contains13_ReturnsTrue()>{> var principal =new ClaimsPrincipal(>new CaseSensitiveClaimsIdentity(new[]>{>new Claim(SubjectFunctionClaim, "121315"),>}));>> Assert.True(principal.IsAgentUserIdentity());>}>>[Fact]> public void IsAgentUserIdentity_Principal_SpaceSeparated_No13_ReturnsFalse()>{> var principal =new ClaimsPrincipal(>new CaseSensitiveClaimsIdentity(new[]>{>new Claim(SubjectFunctionClaim, "1234"),>}));>> Assert.False(principal.IsAgentUserIdentity());>}>>[Fact]> public void IsAgentUserIdentity_Principal_ExtraWhitespace_Contains13_ReturnsTrue()>{> var principal =new ClaimsPrincipal(>new CaseSensitiveClaimsIdentity(new[]>{>new Claim(SubjectFunctionClaim, " 91321 "),>}));>> Assert.True(principal.IsAgentUserIdentity());>}>>[Fact]> public void IsAgentUserIdentity_Principal_InvalidToken_ReturnsFalse()>{> var principal =new ClaimsPrincipal(>new CaseSensitiveClaimsIdentity(new[]>{>new Claim(SubjectFunctionClaim, "12 a 13"),>}));>> Assert.False(principal.IsAgentUserIdentity());>}>>[Fact]> public void IsAgentUserIdentity_Principal_NoClaim_ReturnsFalse()>{> var principal =new ClaimsPrincipal();> Assert.False(principal.IsAgentUserIdentity());>}>>[Fact]> public void IsAgentUserIdentity_Identity_SpaceSeparated_Contains13_ReturnsTrue()>{> var identity =new CaseSensitiveClaimsIdentity(new[]>{>new Claim(SubjectFunctionClaim, "781321"),>});>> Assert.True(identity.IsAgentUserIdentity());>}>>[Fact]> public void IsAgentUserIdentity_Identity_EmptyString_ReturnsFalse()>{> var identity =new CaseSensitiveClaimsIdentity(new[]>{>new Claim(SubjectFunctionClaim, " "),>});>> Assert.False(identity.IsAgentUserIdentity());>}>>[Fact]> public void IsAgentUserIdentity_Identity_NoClaim_ReturnsFalse()>{> var identity =new CaseSensitiveClaimsIdentity();> Assert.False(identity.IsAgentUserIdentity());>}>}>}> ```
>> Build/test guidance
>- Build the solution.>- Run the test suite; the new tests are in AgentIdentityExtensionsTests.>> Acceptance criteria
>- All new tests pass.>- No regressions in existing tests.>- New methods are available for ClaimsPrincipal and ClaimsIdentity as specified.><!-- START COPILOT CODING AGENT TIPS -->---
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more [Copilot coding agent tips](https://gh.io/copilot-coding-agent-tips) in the docs.

Co-authored-by: jmprieur <13203188+jmprieur@users.noreply.github.com>
CopilotAI changed the title [WIP] Add agent identity extension methods and testsAdd agent identity extension methods for ClaimsPrincipal and ClaimsIdentityOct 2, 2025
…s project
Co-authored-by: jmprieur <13203188+jmprieur@users.noreply.github.com>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM

@jmprieur
Jean-Marc Prieur (jmprieur) marked this pull request as ready for review October 2, 2025 03:09
Comment threadsrc/Microsoft.Identity.Web.AgentIdentities/AgentIdentityExtensions.cs Outdated
Comment threadsrc/Microsoft.Identity.Web.AgentIdentities/AgentIdentityExtensions.cs Outdated
Co-authored-by: keegan-caruso <54109988+keegan-caruso@users.noreply.github.com>
@jmprieur

Copy link
Copy Markdown
Collaborator

Copilot : let's add other asserts using the ClaimsIdentity extension methods in the Agent identity end to end tests under tests/E2E%20Tests/AgentApplications

When the test gets the authorization header, extract the token, read it with JsonWebTokenHandler, and create an untrusted claims identity from the claims in the token.

Co-authored-by: jmprieur <13203188+jmprieur@users.noreply.github.com>

CopilotAI commented Oct 2, 2025

Copy link
Copy Markdown
ContributorAuthor

Copilot : let's add other asserts using the ClaimsIdentity extension methods in the Agent identity end to end tests under tests/E2E%20Tests/AgentApplications

When the test gets the authorization...

Added assertions in all four AgentUserIdentityTests methods (722ac3b). Each test now extracts the token from the authorization header, parses it with JwtSecurityTokenHandler, creates a CaseSensitiveClaimsIdentity from the claims, and uses IsAgentUserIdentity() and GetParentAgentBlueprint() extension methods to validate the token.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Copilot : this is great, I left comments.,
Could we also assert the 2 extension methods on ClaimsIndentity in Autonomous agent tests as well?

Comment threadtests/E2E Tests/AgentApplications/AgentUserIdentityTestscs.cs Outdated
Comment threadtests/E2E Tests/AgentApplications/AgentUserIdentityTestscs.cs Outdated
Comment threadtests/E2E Tests/AgentApplications/AgentUserIdentityTestscs.cs Outdated
Comment threadtests/E2E Tests/AgentApplications/AgentUserIdentityTestscs.cs Outdated
Sign up for freeto 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.

4 participants

@jmprieur@keegan-caruso@saurabhsathe-ms