Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Oct 2, 2025

  • 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.

using System;
using System.Globalization;
using System.Security.Claims;

namespace Microsoft.Identity.Web
{
    /// <summary>
    /// Extensions to read agent identity-related claims.
    /// </summary>
    public static class AgentIdentityExtensions
    {
        /// <summary>
        /// Claim type for the parent agent blueprint.
        /// </summary>
        private const string XmsParAppAzp = "xms_par_app_azp";

        /// <summary>
        /// Claim type for subject function codes (space-separated integers).
        /// </summary>
        private const string XmsSubFct = "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>
        public static string? GetParentAgentBlueprint(this ClaimsPrincipal claimsPrincipal)
        {
            if (claimsPrincipal is null)
            {
                throw new ArgumentNullException(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>
        public static string? GetParentAgentBlueprint(this ClaimsIdentity identity)
        {
            if (identity is null)
            {
                throw new ArgumentNullException(nameof(identity));
            }

            return identity.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 static bool IsAgentUserIdentity(this ClaimsPrincipal claimsPrincipal)
        {
            if (claimsPrincipal is null)
            {
                throw new ArgumentNullException(nameof(claimsPrincipal));
            }

            var value = claimsPrincipal.FindFirst(XmsSubFct)?.Value;
            return ContainsFunctionCode(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 static bool IsAgentUserIdentity(this ClaimsIdentity identity)
        {
            if (identity is null)
            {
                throw new ArgumentNullException(nameof(identity));
            }

            var value = identity.FindFirst(XmsSubFct)?.Value;
            return ContainsFunctionCode(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 (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
> 
> ```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>
>     public static class AgentIdentityExtensions
>     {
>         /// <summary>
>         /// Claim type for the parent agent blueprint.
>         /// </summary>
>         private const string XmsParAppAzp = "xms_par_app_azp";
> 
>         /// <summary>
>         /// Claim type for subject function codes (space-separated integers).
>         /// </summary>
>         private const string XmsSubFct = "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>
>         public static string? GetParentAgentBlueprint(this ClaimsPrincipal claimsPrincipal)
>         {
>             if (claimsPrincipal is null)
>             {
>                 throw new ArgumentNullException(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>
>         public static string? GetParentAgentBlueprint(this ClaimsIdentity identity)
>         {
>             if (identity is null)
>             {
>                 throw new ArgumentNullException(nameof(identity));
>             }
> 
>             return identity.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 static bool IsAgentUserIdentity(this ClaimsPrincipal claimsPrincipal)
>         {
>             if (claimsPrincipal is null)
>             {
>                 throw new ArgumentNullException(nameof(claimsPrincipal));
>             }
> 
>             var value = claimsPrincipal.FindFirst(XmsSubFct)?.Value;
>             return ContainsFunctionCode(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 static bool IsAgentUserIdentity(this ClaimsIdentity identity)
>         {
>             if (identity is null)
>             {
>                 throw new 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 static bool 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, out int 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, "1 2 13 15"),
>                 }));
> 
>             Assert.True(principal.IsAgentUserIdentity());
>         }
> 
>         [Fact]
>         public void IsAgentUserIdentity_Principal_SpaceSeparated_No13_ReturnsFalse()
>         {
>             var principal = new ClaimsPrincipal(
>                 new CaseSensitiveClaimsIdentity(new[]
>                 {
>                     new Claim(SubjectFunctionClaim, "1 2 3 4"),
>                 }));
> 
>             Assert.False(principal.IsAgentUserIdentity());
>         }
> 
>         [Fact]
>         public void IsAgentUserIdentity_Principal_ExtraWhitespace_Contains13_ReturnsTrue()
>         {
>             var principal = new ClaimsPrincipal(
>                 new CaseSensitiveClaimsIdentity(new[]
>                 {
>                     new Claim(SubjectFunctionClaim, "   9   13   21  "),
>                 }));
> 
>             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, "7 8 13 21"),
>             });
> 
>             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.

Copilot AI changed the title [WIP] Add agent identity extension methods and tests Add agent identity extension methods for ClaimsPrincipal and ClaimsIdentity Oct 2, 2025
Copilot AI requested a review from jmprieur October 2, 2025 02:10
Copilot finished work on behalf of jmprieur October 2, 2025 02:10
Copilot AI requested a review from jmprieur October 2, 2025 02:45
Copilot finished work on behalf of jmprieur October 2, 2025 02:45
Copy link
Collaborator

@jmprieur jmprieur left a comment

Choose a reason for hiding this comment

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

LGTM

@jmprieur jmprieur marked this pull request as ready for review October 2, 2025 03:09
@jmprieur jmprieur requested a review from a team as a code owner October 2, 2025 03:09
Copilot AI requested a review from keegan-caruso October 2, 2025 16:45
Copilot finished work on behalf of keegan-caruso October 2, 2025 16:45
@jmprieur
Copy link
Collaborator

jmprieur commented Oct 2, 2025

@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.

Copy link
Contributor Author

Copilot AI commented Oct 2, 2025

@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.

Copilot AI requested a review from jmprieur October 2, 2025 18:11
Copilot finished work on behalf of jmprieur October 2, 2025 18:11
Copy link
Collaborator

@jmprieur jmprieur left a comment

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?

This was referenced Nov 1, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants