Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions src/client/Microsoft.Identity.Client/AgentIdentity.cs
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
Expand Up @@ -33,6 +33,16 @@ internal AcquireTokenByUserFederatedIdentityCredentialParameterBuilder(
Parameters.Assertion = assertion;
}

internal AcquireTokenByUserFederatedIdentityCredentialParameterBuilder(
IConfidentialClientApplicationExecutor confidentialClientApplicationExecutor,
Guid userObjectId,
string assertion)
: base(confidentialClientApplicationExecutor)
{
Parameters.UserObjectId = userObjectId;
Parameters.Assertion = assertion;
}

internal static AcquireTokenByUserFederatedIdentityCredentialParameterBuilder Create(
IConfidentialClientApplicationExecutor confidentialClientApplicationExecutor,
IEnumerable<string> scopes,
Expand All @@ -54,6 +64,27 @@ internal static AcquireTokenByUserFederatedIdentityCredentialParameterBuilder Cr
.WithScopes(scopes);
}

internal static AcquireTokenByUserFederatedIdentityCredentialParameterBuilder Create(
IConfidentialClientApplicationExecutor confidentialClientApplicationExecutor,
IEnumerable<string> scopes,
Guid userObjectId,
string assertion)
{
if (userObjectId == Guid.Empty)
{
throw new ArgumentException("userObjectId must not be empty.", nameof(userObjectId));
}

if (string.IsNullOrEmpty(assertion))
{
throw new ArgumentNullException(nameof(assertion));
}

return new AcquireTokenByUserFederatedIdentityCredentialParameterBuilder(
confidentialClientApplicationExecutor, userObjectId, assertion)
.WithScopes(scopes);
}

/// <summary>
/// Forces MSAL to refresh the token from the identity provider even if a cached token is available.
/// </summary>
Expand Down
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
Expand Up @@ -187,5 +187,29 @@ public async Task<AuthenticationResult> ExecuteAsync(

return await handler.RunAsync(cancellationToken).ConfigureAwait(false);
}

public async Task<AuthenticationResult> ExecuteAsync(
AcquireTokenCommonParameters commonParameters,
AcquireTokenForAgentParameters agentParameters,
CancellationToken cancellationToken)
{
RequestContext requestContext = CreateRequestContextAndLogVersionInfo(commonParameters.CorrelationId, commonParameters.MtlsCertificate, cancellationToken);

AuthenticationRequestParameters requestParams = await _confidentialClientApplication.CreateRequestParametersAsync(
commonParameters,
requestContext,
_confidentialClientApplication.UserTokenCacheInternal,
cancellationToken).ConfigureAwait(false);

requestParams.SendX5C = agentParameters.SendX5C ?? false;

var handler = new AgentTokenRequest(
ServiceBundle,
requestParams,
agentParameters,
_confidentialClientApplication);

return await handler.RunAsync(cancellationToken).ConfigureAwait(false);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,10 @@ Task<AuthenticationResult> ExecuteAsync(
AcquireTokenCommonParameters commonParameters,
AcquireTokenByUserFederatedIdentityCredentialParameters userFicParameters,
CancellationToken cancellationToken);

Task<AuthenticationResult> ExecuteAsync(
AcquireTokenCommonParameters commonParameters,
AcquireTokenForAgentParameters agentParameters,
CancellationToken cancellationToken);
}
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,33 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Text;
using Microsoft.Identity.Client.Core;

namespace Microsoft.Identity.Client.ApiConfig.Parameters
{
internal class AcquireTokenByUserFederatedIdentityCredentialParameters : IAcquireTokenParameters
{
public string Username { get; set; }
public Guid? UserObjectId { get; set; }
public string Assertion { get; set; }
public bool? SendX5C { get; set; }
public bool ForceRefresh { get; set; }

/// <inheritdoc/>
public void LogParameters(ILoggerAdapter logger)
{
if (logger.IsLoggingEnabled(LogLevel.Info))
{
var builder = new StringBuilder();
builder.AppendLine("=== AcquireTokenByUserFederatedIdentityCredentialParameters ===");
builder.AppendLine("SendX5C: " + SendX5C);
builder.AppendLine("ForceRefresh: " + ForceRefresh);
builder.AppendLine("UserIdentifiedByOid: " + UserObjectId.HasValue);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Log username as well

builder.AppendLine("Assertion set: " + !string.IsNullOrEmpty(Assertion));
logger.Info(builder.ToString());
}
}
}
}
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());
}
}
}
}
Loading
Loading