-
Notifications
You must be signed in to change notification settings - Fork 400
Add agentic ID support #5883
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Add agentic ID support #5883
Changes from 7 commits
588c935
0950e3f
9f5ea5c
5f86f9a
9462880
a1c2246
d67bd28
ed0ceb7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||
| // Licensed under the MIT License. | ||
|
|
||
| using System; | ||
|
|
||
| namespace Microsoft.Identity.Client | ||
| { | ||
| /// <summary> | ||
| /// Represents the identity of an agent application and the user it acts on behalf of. | ||
| /// Used with <see cref="IConfidentialClientApplication.AcquireTokenForAgent(System.Collections.Generic.IEnumerable{string}, AgentIdentity)"/> | ||
| /// to acquire tokens for agent scenarios using Federated Managed Identity (FMI) and User Federated Identity Credentials (UserFIC). | ||
| /// </summary> | ||
| public sealed class AgentIdentity | ||
| { | ||
| private AgentIdentity(string agentApplicationId) | ||
| { | ||
| if (string.IsNullOrEmpty(agentApplicationId)) | ||
| { | ||
| throw new ArgumentNullException(nameof(agentApplicationId)); | ||
| } | ||
|
|
||
| AgentApplicationId = agentApplicationId; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Creates an <see cref="AgentIdentity"/> that identifies the user by their object ID (OID). | ||
| /// This is the recommended approach for identifying users in agent scenarios. | ||
| /// </summary> | ||
| /// <param name="agentApplicationId">The client ID of the agent application.</param> | ||
| /// <param name="userObjectId">The object ID (OID) of the user the agent acts on behalf of.</param> | ||
| /// <returns>An <see cref="AgentIdentity"/> configured with the user's OID.</returns> | ||
| public AgentIdentity(string agentApplicationId, Guid userObjectId) | ||
| : this(agentApplicationId) | ||
| { | ||
| if (userObjectId == Guid.Empty) | ||
| { | ||
| throw new ArgumentException("userObjectId must not be empty.", nameof(userObjectId)); | ||
| } | ||
|
|
||
| UserObjectId = userObjectId; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Creates an <see cref="AgentIdentity"/> that identifies the user by their UPN (User Principal Name). | ||
| /// </summary> | ||
| /// <param name="agentApplicationId">The client ID of the agent application.</param> | ||
| /// <param name="username">The UPN of the user the agent acts on behalf of.</param> | ||
| /// <returns>An <see cref="AgentIdentity"/> configured with the user's UPN.</returns> | ||
| public static AgentIdentity WithUsername(string agentApplicationId, string username) | ||
| { | ||
| if (string.IsNullOrEmpty(username)) | ||
| { | ||
| throw new ArgumentNullException(nameof(username)); | ||
| } | ||
|
|
||
| return new AgentIdentity(agentApplicationId) | ||
| { | ||
| Username = username | ||
| }; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Creates an <see cref="AgentIdentity"/> for app-only (no user) scenarios, where only Legs 1-2 of the | ||
| /// agent token acquisition are performed. | ||
| /// </summary> | ||
| /// <param name="agentApplicationId">The client ID of the agent application.</param> | ||
| /// <returns>An <see cref="AgentIdentity"/> configured for app-only access.</returns> | ||
| public static AgentIdentity AppOnly(string agentApplicationId) | ||
| { | ||
| return new AgentIdentity(agentApplicationId); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Gets the client ID of the agent application. | ||
| /// </summary> | ||
| public string AgentApplicationId { get; } | ||
|
|
||
| /// <summary> | ||
| /// Gets the object ID (OID) of the user, if specified. | ||
| /// </summary> | ||
| public Guid? UserObjectId { get; private set; } | ||
|
|
||
| /// <summary> | ||
| /// Gets the UPN of the user, if specified. | ||
| /// </summary> | ||
| public string Username { get; private set; } | ||
|
|
||
| /// <summary> | ||
| /// Gets a value indicating whether this identity includes a user identifier (OID or UPN). | ||
| /// </summary> | ||
| internal bool HasUserIdentifier => UserObjectId.HasValue || !string.IsNullOrEmpty(Username); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||
| // Licensed under the MIT License. | ||
|
|
||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.Threading; | ||
| using System.Threading.Tasks; | ||
| using Microsoft.Identity.Client.ApiConfig.Executors; | ||
| using Microsoft.Identity.Client.ApiConfig.Parameters; | ||
| using Microsoft.Identity.Client.TelemetryCore.Internal.Events; | ||
|
|
||
| namespace Microsoft.Identity.Client | ||
| { | ||
| /// <summary> | ||
| /// Builder for AcquireTokenForAgent, used to acquire tokens for agent scenarios involving | ||
| /// Federated Managed Identity (FMI) and User Federated Identity Credentials (UserFIC). | ||
| /// This orchestrates the multi-leg token acquisition automatically. | ||
| /// </summary> | ||
| #if !SUPPORTS_CONFIDENTIAL_CLIENT | ||
| [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] // hide confidential client on mobile | ||
| #endif | ||
| public sealed class AcquireTokenForAgentParameterBuilder : | ||
| AbstractConfidentialClientAcquireTokenParameterBuilder<AcquireTokenForAgentParameterBuilder> | ||
| { | ||
| internal AcquireTokenForAgentParameters Parameters { get; } = new AcquireTokenForAgentParameters(); | ||
|
|
||
| /// <inheritdoc/> | ||
| internal AcquireTokenForAgentParameterBuilder( | ||
| IConfidentialClientApplicationExecutor confidentialClientApplicationExecutor, | ||
| AgentIdentity agentIdentity) | ||
| : base(confidentialClientApplicationExecutor) | ||
| { | ||
| Parameters.AgentIdentity = agentIdentity; | ||
| } | ||
|
|
||
| internal static AcquireTokenForAgentParameterBuilder Create( | ||
| IConfidentialClientApplicationExecutor confidentialClientApplicationExecutor, | ||
| IEnumerable<string> scopes, | ||
| AgentIdentity agentIdentity) | ||
| { | ||
| if (agentIdentity == null) | ||
| { | ||
| throw new ArgumentNullException(nameof(agentIdentity)); | ||
| } | ||
|
|
||
| return new AcquireTokenForAgentParameterBuilder( | ||
| confidentialClientApplicationExecutor, | ||
| agentIdentity) | ||
| .WithScopes(scopes); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Specifies if the client application should ignore access tokens when reading the token cache. | ||
| /// New tokens will still be written to the token cache. | ||
| /// By default the token is taken from the cache (forceRefresh=false). | ||
| /// </summary> | ||
| /// <param name="forceRefresh"> | ||
| /// If <c>true</c>, the request will ignore cached access tokens on read, but will still write them to the cache once obtained from the identity provider. The default is <c>false</c>. | ||
| /// </param> | ||
| /// <returns>The builder to chain the .With methods.</returns> | ||
| public AcquireTokenForAgentParameterBuilder WithForceRefresh(bool forceRefresh) | ||
| { | ||
| Parameters.ForceRefresh = forceRefresh; | ||
| return this; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Specifies if the x5c claim (public key of the certificate) should be sent to the identity provider, | ||
| /// which enables subject name/issuer based authentication for the client credential. | ||
| /// This is useful for certificate rollover scenarios. See https://aka.ms/msal-net-sni. | ||
| /// </summary> | ||
| /// <param name="withSendX5C"><c>true</c> if the x5c should be sent. Otherwise <c>false</c>. | ||
| /// The default is <c>false</c>.</param> | ||
| /// <returns>The builder to chain the .With methods.</returns> | ||
| public AcquireTokenForAgentParameterBuilder WithSendX5C(bool withSendX5C) | ||
| { | ||
| Parameters.SendX5C = withSendX5C; | ||
| return this; | ||
| } | ||
|
|
||
| /// <inheritdoc/> | ||
| internal override Task<AuthenticationResult> ExecuteInternalAsync(CancellationToken cancellationToken) | ||
| { | ||
| return ConfidentialClientApplicationExecutor.ExecuteAsync(CommonParameters, Parameters, cancellationToken); | ||
| } | ||
|
|
||
| /// <inheritdoc/> | ||
| protected override void Validate() | ||
| { | ||
| base.Validate(); | ||
|
|
||
| if (Parameters.SendX5C == null) | ||
| { | ||
| Parameters.SendX5C = this.ServiceBundle.Config.SendX5C; | ||
| } | ||
| } | ||
|
|
||
| /// <inheritdoc/> | ||
| internal override ApiEvent.ApiIds CalculateApiEventId() | ||
| { | ||
| return ApiEvent.ApiIds.AcquireTokenForAgent; | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||
| // Licensed under the MIT License. | ||
|
|
||
| using System.Text; | ||
| using Microsoft.Identity.Client.Core; | ||
|
|
||
| namespace Microsoft.Identity.Client.ApiConfig.Parameters | ||
| { | ||
| internal class AcquireTokenForAgentParameters : AbstractAcquireTokenConfidentialClientParameters, IAcquireTokenParameters | ||
| { | ||
| public AgentIdentity AgentIdentity { get; set; } | ||
|
|
||
| public bool ForceRefresh { get; set; } | ||
|
|
||
| /// <inheritdoc/> | ||
| public void LogParameters(ILoggerAdapter logger) | ||
| { | ||
| if (logger.IsLoggingEnabled(LogLevel.Info)) | ||
| { | ||
| var builder = new StringBuilder(); | ||
| builder.AppendLine("=== AcquireTokenForAgentParameters ==="); | ||
| builder.AppendLine("SendX5C: " + SendX5C); | ||
| builder.AppendLine("ForceRefresh: " + ForceRefresh); | ||
| builder.AppendLine("AgentApplicationId: " + AgentIdentity?.AgentApplicationId); | ||
| builder.AppendLine("HasUserIdentifier: " + (AgentIdentity?.HasUserIdentifier ?? false)); | ||
| logger.Info(builder.ToString()); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2,6 +2,7 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Licensed under the MIT License. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| using System; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| using System.Collections.Concurrent; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| using System.Collections.Generic; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| using System.Security.Cryptography.X509Certificates; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| using System.Threading; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -197,6 +198,19 @@ AcquireTokenByUserFederatedIdentityCredentialParameterBuilder IByUserFederatedId | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| assertion); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// <inheritdoc/> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| AcquireTokenByUserFederatedIdentityCredentialParameterBuilder IByUserFederatedIdentityCredential.AcquireTokenByUserFederatedIdentityCredential( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| IEnumerable<string> scopes, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Guid userObjectId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| string assertion) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return AcquireTokenByUserFederatedIdentityCredentialParameterBuilder.Create( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ClientExecutorFactory.CreateConfidentialClientExecutor(this), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| scopes, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| userObjectId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assertion); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| AcquireTokenByRefreshTokenParameterBuilder IByRefreshToken.AcquireTokenByRefreshToken( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| IEnumerable<string> scopes, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| string refreshToken) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -207,6 +221,17 @@ AcquireTokenByRefreshTokenParameterBuilder IByRefreshToken.AcquireTokenByRefresh | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| refreshToken); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// <inheritdoc/> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public AcquireTokenForAgentParameterBuilder AcquireTokenForAgent( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| IEnumerable<string> scopes, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| AgentIdentity agentIdentity) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return AcquireTokenForAgentParameterBuilder.Create( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ClientExecutorFactory.CreateConfidentialClientExecutor(this), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| scopes, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| agentIdentity); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// <inheritdoc/> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public ITokenCache AppTokenCache => AppTokenCacheInternal; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -218,6 +243,13 @@ AcquireTokenByRefreshTokenParameterBuilder IByRefreshToken.AcquireTokenByRefresh | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Stores all app tokens | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| internal ITokenCacheInternal AppTokenCacheInternal { get; } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// <summary> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Caches internal CCA instances created by <see cref="AgentTokenRequest"/> so that | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// subsequent AcquireTokenForAgent calls for the same agent reuse the same CCA | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// (and its in-memory token cache) instead of rebuilding from scratch each time. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// </summary> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| internal ConcurrentDictionary<string, IConfidentialClientApplication> AgentCcaCache { get; } = new(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// <summary> | |
| /// Clears all cached agent <see cref="IConfidentialClientApplication"/> instances. | |
| /// This can be used by long-lived hosts to avoid unbounded growth of the in-memory cache. | |
| /// </summary> | |
| internal void ClearAgentCcaCache() | |
| { | |
| AgentCcaCache.Clear(); | |
| } | |
| /// <summary> | |
| /// Removes a specific agent <see cref="IConfidentialClientApplication"/> instance | |
| /// from the cache by its agent application identifier, if present. | |
| /// Returns <c>true</c> if an entry was removed; otherwise, <c>false</c>. | |
| /// </summary> | |
| /// <param name="agentApplicationId">The agent application identifier used as the cache key.</param> | |
| internal bool RemoveAgentCcaCacheEntry(string agentApplicationId) | |
| { | |
| if (string.IsNullOrEmpty(agentApplicationId)) | |
| { | |
| return false; | |
| } | |
| return AgentCcaCache.TryRemove(agentApplicationId, out _); | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't believe this will be a problem: each entry in the cache is pegged to a specific application ID, and customers likely won't have enough Entra apps to cause any memory issues here.
Uh oh!
There was an error while loading. Please reload this page.