Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Oct 1, 2025

Fixes #3512

  • 1. Add UserIdKey constant to Constants.cs to carry user OID in ExtraParameters
  • 2. Add Guid-based overload WithAgentUserIdentity(string agentApplicationId, Guid userId) to AgentIdentitiesExtension.cs
  • 3. Update AgentUserIdentityMsalAddIn.cs to handle user_id parameter and implement UPN precedence
  • 4. Update TokenAcquisition.cs to recognize OID path (UserId + AgentIdentity) for ROPC-like flow
  • 5. Add InternalAPI.Unshipped.txt entries for the new constant across all target frameworks (net462, net472, net8.0, net9.0, netstandard2.0)
  • 6. Add PublicAPI.Unshipped.txt entry for the new public method in AgentIdentities
  • 7. Add E2E test AgentUserIdentityGetsTokenForGraphByUserIdAsync to verify OID-based flow
  • 8. Build validation completed successfully for all projects
  • 9. Validated implementation with custom test program confirming correct parameter handling
  • 10. Full solution build passed without errors
  • 11. Update README.AgentIdentities.md to document both UPN and OID usage patterns
  • 12. Update test user OID to match correct test user

Summary of Changes

Successfully implemented feature request #3512 to support passing either UPN or user OID (Guid) to WithAgentUserIdentity for Agent Identities.

Core Implementation:

  • Added Constants.UserIdKey = "IDWEB_USER_ID" internal constant
  • Created new public overload: WithAgentUserIdentity(string agentApplicationId, Guid userId)
  • Modified AgentUserIdentityMsalAddIn to check for both UsernameKey and UserIdKey
  • Updated token request handler to emit user_id when OID is provided, username when UPN is provided
  • Implemented UPN precedence: if both are present, UPN takes priority (as documented in remarks)
  • Modified TokenAcquisition.TryGetAuthenticationResultForConfidentialClientUsingRopcAsync to recognize OID path

Documentation:

  • All target frameworks updated with InternalAPI entries for the new constant
  • Public API entry added for the new overload
  • XML documentation clearly states UPN precedence behavior
  • Updated README.AgentIdentities.md with comprehensive examples showing both UPN and OID usage:
    • Agent User Identity section now shows both approaches
    • Microsoft Graph integration examples for both UPN and OID
    • Downstream API examples for both UPN and OID

Testing:

  • Added E2E test mirroring existing UPN test using Guid.Parse(userOid)
  • Updated test user OID to match correct test user (51c1aa1c-f6d0-4a92-936c-cadb27b717f2)
  • Validated implementation with standalone test program
  • Full solution builds successfully across all target frameworks (net462, net472, net8.0, net9.0, netstandard2.0)

Backward Compatibility:

  • Existing UPN-based behavior remains completely unchanged
  • New OID path is additive and does not affect existing code paths
  • Public API is extended via overload, maintaining binary compatibility
Original prompt

Implements feature request #3512 to allow passing either a UPN or a user OID (Guid) to WithAgentUserIdentity for Agent Identities.

Summary

  • Add a Guid-based overload WithAgentUserIdentity(this AuthorizationHeaderProviderOptions options, string agentApplicationId, Guid userId).
  • Introduce Constants.UserIdKey ("IDWEB_USER_ID") to carry the user OID through AcquireTokenOptions.ExtraParameters.
  • Update AgentUserIdentityMsalAddIn to emit user_id for user_fic when OID is provided. If both UPN and OID are present, UPN takes precedence.
  • Update TokenAcquisition to recognize the OID path and route via the same ROPC-like builder path (placeholder username/password later removed by the add-in).
  • Add XML doc comments for the new overload and clarify precedence between UPN and OID.
  • Add an end-to-end test mirroring the existing UPN test but using the OID overload.
  • If the repository enforces PublicAPI analyzers for internal constants, add InternalAPI.Unshipped entries for the new constant.

Acceptance criteria

  • Existing UPN-based behavior remains unchanged.
  • OID path sets user_id in the token request and omits username; user_fic flow succeeds.
  • If both keys are present, UPN wins.
  • New E2E test passes and existing tests remain green.

Edits to apply

// Add a new internal constant to carry the OID in ExtraParameters
// Place alongside UsernameKey/AgentIdentityKey
internal const string UserIdKey = "IDWEB_USER_ID"; // carries user OID (Guid) for agent user identity flow
// Add a new overload and XML docs to accept a Guid userId
// Keep the existing UPN overload unchanged.

/// <summary>
/// Updates the options to acquire a token for the agent user identity using the user's object id (OID).
/// </summary>
/// <param name="options">Authorization header provider options.</param>
/// <param name="agentApplicationId">The agent identity application (client) ID.</param>
/// <param name="userId">The user's object id (OID).</param>
/// <returns>The updated authorization header provider options (in place; not a clone).</returns>
/// <remarks>
/// If both a UPN and an OID are present in the options (not expected via the public API), UPN takes precedence.
/// </remarks>
public static AuthorizationHeaderProviderOptions WithAgentUserIdentity(this AuthorizationHeaderProviderOptions options, string agentApplicationId, Guid userId)
{
    options ??= new AuthorizationHeaderProviderOptions();
    options.AcquireTokenOptions ??= new AcquireTokenOptions();
    options.AcquireTokenOptions.ExtraParameters ??= new Dictionary<string, object>();

    // Configure the agent application
    options.AcquireTokenOptions.ExtraParameters[Constants.MicrosoftIdentityOptionsParameter] = new MicrosoftEntraApplicationOptions
    {
        ClientId = agentApplicationId,
    };

    // Identity selection via OID
    options.AcquireTokenOptions.ExtraParameters[Constants.AgentIdentityKey] = agentApplicationId;
    options.AcquireTokenOptions.ExtraParameters[Constants.UserIdKey] = userId.ToString("D");

    return options;
}
// Read the new ExtraParameters key and emit user_id in the user_fic grant when OID is present
// Ensure UPN (username) takes precedence over user_id when both are present

options.ExtraParameters.TryGetValue(Constants.UserIdKey, out object? userIdObject);
if (agentIdentityObject is string agentIdentity && (usernameObject is string || userIdObject is string))
{
    // inside OnBeforeTokenRequestHandler, after setting common user_fic parameters
    if (usernameObject is string username && !string.IsNullOrEmpty(username))
    {
        request.BodyParameters["username"] = username;
        if (request.BodyParameters.ContainsKey("user_id"))
        {
            request.BodyParameters.Remove("user_id");
        }
    }
    else if (userIdObject is string userId && !string.IsNullOrEmpty(userId))
    {
        request.BodyParameters["user_id"] = userId;
        if (request.BodyParameters.ContainsKey("username"))
        {
            request.BodyParameters.Remove("username");
        }
    }
}
// Recognize the Agent User Identities OID path similarly t...

</details>
*This pull request was created as a result of the following prompt from Copilot chat.*
> Implements feature request AzureAD/microsoft-identity-web#3512 to allow passing either a UPN or a user OID (Guid) to WithAgentUserIdentity for Agent Identities.
> 
> Summary
> - Add a Guid-based overload WithAgentUserIdentity(this AuthorizationHeaderProviderOptions options, string agentApplicationId, Guid userId).
> - Introduce Constants.UserIdKey ("IDWEB_USER_ID") to carry the user OID through AcquireTokenOptions.ExtraParameters.
> - Update AgentUserIdentityMsalAddIn to emit user_id for user_fic when OID is provided. If both UPN and OID are present, UPN takes precedence.
> - Update TokenAcquisition to recognize the OID path and route via the same ROPC-like builder path (placeholder username/password later removed by the add-in).
> - Add XML doc comments for the new overload and clarify precedence between UPN and OID.
> - Add an end-to-end test mirroring the existing UPN test but using the OID overload.
> - If the repository enforces PublicAPI analyzers for internal constants, add InternalAPI.Unshipped entries for the new constant.
> 
> Acceptance criteria
> - Existing UPN-based behavior remains unchanged.
> - OID path sets user_id in the token request and omits username; user_fic flow succeeds.
> - If both keys are present, UPN wins.
> - New E2E test passes and existing tests remain green.
> 
> Edits to apply
> 
> ```csharp name=src/Microsoft.Identity.Web.TokenAcquisition/Constants.cs url=https://github.com/AzureAD/microsoft-identity-web/blob/master/src/Microsoft.Identity.Web.TokenAcquisition/Constants.cs
> // Add a new internal constant to carry the OID in ExtraParameters
> // Place alongside UsernameKey/AgentIdentityKey
> internal const string UserIdKey = "IDWEB_USER_ID"; // carries user OID (Guid) for agent user identity flow
> ```
> 
> ```csharp name=src/Microsoft.Identity.Web.AgentIdentities/AgentIdentitiesExtension.cs url=https://github.com/AzureAD/microsoft-identity-web/blob/master/src/Microsoft.Identity.Web.AgentIdentities/AgentIdentitiesExtension.cs
> // Add a new overload and XML docs to accept a Guid userId
> // Keep the existing UPN overload unchanged.
> 
> /// <summary>
> /// Updates the options to acquire a token for the agent user identity using the user's object id (OID).
> /// </summary>
> /// <param name="options">Authorization header provider options.</param>
> /// <param name="agentApplicationId">The agent identity application (client) ID.</param>
> /// <param name="userId">The user's object id (OID).</param>
> /// <returns>The updated authorization header provider options (in place; not a clone).</returns>
> /// <remarks>
> /// If both a UPN and an OID are present in the options (not expected via the public API), UPN takes precedence.
> /// </remarks>
> public static AuthorizationHeaderProviderOptions WithAgentUserIdentity(this AuthorizationHeaderProviderOptions options, string agentApplicationId, Guid userId)
> {
>     options ??= new AuthorizationHeaderProviderOptions();
>     options.AcquireTokenOptions ??= new AcquireTokenOptions();
>     options.AcquireTokenOptions.ExtraParameters ??= new Dictionary<string, object>();
> 
>     // Configure the agent application
>     options.AcquireTokenOptions.ExtraParameters[Constants.MicrosoftIdentityOptionsParameter] = new MicrosoftEntraApplicationOptions
>     {
>         ClientId = agentApplicationId,
>     };
> 
>     // Identity selection via OID
>     options.AcquireTokenOptions.ExtraParameters[Constants.AgentIdentityKey] = agentApplicationId;
>     options.AcquireTokenOptions.ExtraParameters[Constants.UserIdKey] = userId.ToString("D");
> 
>     return options;
> }
> ```
> 
> ```csharp name=src/Microsoft.Identity.Web.AgentIdentities/AgentUserIdentityMsalAddIn.cs url=https://github.com/AzureAD/microsoft-identity-web/blob/master/src/Microsoft.Identity.Web.AgentIdentities/AgentUserIdentityMsalAddIn.cs
> // Read the new ExtraParameters key and emit user_id in the user_fic grant when OID is present
> // Ensure UPN (username) takes precedence over user_id when both are present
> 
> options.ExtraParameters.TryGetValue(Constants.UserIdKey, out object? userIdObject);
> if (agentIdentityObject is string agentIdentity && (usernameObject is string || userIdObject is string))
> {
>     // inside OnBeforeTokenRequestHandler, after setting common user_fic parameters
>     if (usernameObject is string username && !string.IsNullOrEmpty(username))
>     {
>         request.BodyParameters["username"] = username;
>         if (request.BodyParameters.ContainsKey("user_id"))
>         {
>             request.BodyParameters.Remove("user_id");
>         }
>     }
>     else if (userIdObject is string userId && !string.IsNullOrEmpty(userId))
>     {
>         request.BodyParameters["user_id"] = userId;
>         if (request.BodyParameters.ContainsKey("username"))
>         {
>             request.BodyParameters.Remove("username");
>         }
>     }
> }
> ```
> 
> ```csharp name=src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs url=https://github.com/AzureAD/microsoft-identity-web/blob/master/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs
> // Recognize the Agent User Identities OID path similarly to the UPN path
> // In TryGetAuthenticationResultForConfidentialClientUsingRopcAsync, after existing block that checks UsernameKey
> else if (extraParameters != null && extraParameters.ContainsKey(Constants.AgentIdentityKey) && extraParameters.ContainsKey(Constants.UserIdKey))
> {
>     username = extraParameters[Constants.UserIdKey]?.ToString();
>     agentIdentity = extraParameters[Constants.AgentIdentityKey] as string;
>     password = "password"; // placeholder removed by add-in
> }
> ```
> 
> ```csharp name=tests/E2E Tests/AgentApplications/AgentUserIdentityTestscs.cs url=https://github.com/AzureAD/microsoft-identity-web/blob/master/tests/E2E%20Tests/AgentApplications/AgentUserIdentityTestscs.cs
> // Add a new test mirroring the UPN flow that uses the Guid-based overload
> [Fact]
> public async Task AgentUserIdentityGetsTokenForGraphByUserIdAsync()
> {
>     // Arrange test settings and configure services similarly to existing UPN-based test
>     // Use .WithAgentUserIdentity(agentIdentity, Guid.Parse(userOid))
>     // Assert that the authorization header is returned and GraphClient.Me works
> }
> ```
> 
> ```text name=src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/InternalAPI.Unshipped.txt
> const Microsoft.Identity.Web.Constants.UserIdKey = "IDWEB_USER_ID" -> string!
> ```
> 
> ```text name=src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/InternalAPI.Unshipped.txt
> const Microsoft.Identity.Web.Constants.UserIdKey = "IDWEB_USER_ID" -> string!
> ```
> 
> Notes
> - Ensure using directives include System for Guid where needed.
> - Keep existing UPN overload and behavior unchanged for backward compatibility.
> - The E2E test can reuse the same lab setup as the existing AgentUserIdentity tests; skip or guard if secrets/config not present.
> 

<!-- START COPILOT CODING AGENT TIPS -->
---

💬 Share your feedback on Copilot coding agent for the chance to win a $200 gift card! Click [here](https://survey3.medallia.com/?EAHeSx-AP01bZqG0Ld9QLQ) to start the survey.

Copilot AI changed the title [WIP] Add Guid-based overload for WithAgentUserIdentity and support user_id in user_fic flow (XML docs + E2E test) Add Guid-based overload for Agent User Identity to support OID in addition to UPN Oct 1, 2025
Copilot AI requested a review from jmprieur October 1, 2025 20:19
Copilot finished work on behalf of jmprieur October 1, 2025 20:19
@jmprieur jmprieur marked this pull request as ready for review October 1, 2025 20:35
@jmprieur jmprieur requested a review from a team as a code owner October 1, 2025 20:35
@jmprieur
Copy link
Collaborator

jmprieur commented Oct 1, 2025

Very good start, @copilot
Please also update the src/Microsoft.Identity.Web.AgentIdentities/README.AgentIdentities.md file

Copy link
Contributor Author

Copilot AI commented Oct 1, 2025

Very good start, @copilot
Please also update the src/Microsoft.Identity.Web.AgentIdentities/README.AgentIdentities.md file

Updated README.AgentIdentities.md to document the new OID-based overload. The README now shows both UPN and OID usage patterns for Agent User Identity across all integration scenarios (direct API calls, Microsoft Graph, and Downstream APIs). Commit: 0d568e2

Copilot finished work on behalf of jmprieur October 1, 2025 20:41
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
Thanks

@jmprieur jmprieur removed their assignment Oct 1, 2025
Copilot AI requested a review from jmprieur October 1, 2025 20:56
Copilot finished work on behalf of jmprieur October 1, 2025 20:56
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

Copy link
Member

@bgavrilMS bgavrilMS left a comment

Choose a reason for hiding this comment

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

LGTM, small comment on the caching and naming

Copy link
Contributor

@keegan-caruso keegan-caruso left a comment

Choose a reason for hiding this comment

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

other than @bgavrilMS's comments LGTM

@jmprieur jmprieur merged commit 94a6a82 into master Oct 1, 2025
8 checks passed
@jmprieur jmprieur deleted the copilot/fix-a5819f00-fb0b-4d24-a181-0b98280a5688 branch October 1, 2025 21:49
This was referenced Oct 24, 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.

Allow WithAgentUserIdentity to accept either UPN or OID (Guid) for agent user identity selection (with XML doc updates)

4 participants