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
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ private static void ApplyMtlsPopAndAttestation(
AcquireTokenForManagedIdentityParameters acquireTokenForManagedIdentityParameters)
{
acquireTokenForManagedIdentityParameters.IsMtlsPopRequested = acquireTokenCommonParameters.IsMtlsPopRequested;
acquireTokenForManagedIdentityParameters.IsMtlsBearerRequested = acquireTokenCommonParameters.IsMtlsBearerRequested;
acquireTokenForManagedIdentityParameters.MtlsPopMinStrength = acquireTokenCommonParameters.MtlsPopMinStrength;
acquireTokenForManagedIdentityParameters.AttestationTokenProvider = acquireTokenCommonParameters.AttestationTokenProvider;

Expand Down Expand Up @@ -158,6 +159,18 @@ private static void ApplyMtlsPopAndAttestation(
acquireTokenCommonParameters.CacheKeyComponents.Remove(MiMinStrengthCacheKeyComponent);
}
}

// mTLS-bearer requests also need a cache key component so they are stored separately from
// plain IMDSv1 bearer tokens and from mTLS PoP tokens for the same resource.
if (acquireTokenCommonParameters.IsMtlsBearerRequested)
{
acquireTokenCommonParameters.CacheKeyComponents ??=
new SortedList<string, Func<CancellationToken, Task<string>>>();

const string MtlsBearerKey = "mtls_bearer";
acquireTokenCommonParameters.CacheKeyComponents[MtlsBearerKey] =
_ => acquireTokenCommonParameters.AttestationTokenProvider != null ? s_att1 : s_att0;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ internal class AcquireTokenCommonParameters
public string ClientAssertionFmiPath { get; internal set; }
public bool IsMtlsPopRequested { get; set; }

/// <summary>
/// When true, MSAL uses the full IMDSv2 attested flow (mTLS connection to ESTS via a
/// Credential Guard–issued certificate) but requests <c>token_type=bearer</c> from the
/// token endpoint, returning a standard bearer token with no binding certificate.
/// </summary>
public bool IsMtlsBearerRequested { get; set; }

/// <summary>
/// The minimum mTLS binding strength the host must support for the request to succeed.
/// Set via <see cref="AppConfig.PoPOptions.MinStrength"/>. Defaults to
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ internal class AcquireTokenForManagedIdentityParameters : IAcquireTokenParameter

public bool IsMtlsPopRequested { get; set; }

/// <summary>
/// When true, MSAL uses the full IMDSv2 attested flow (mTLS connection to ESTS via a
/// Credential Guard–issued certificate) but requests <c>token_type=bearer</c> from the
/// token endpoint, returning a standard bearer token with no binding certificate.
/// </summary>
public bool IsMtlsBearerRequested { get; set; }

/// <summary>
/// The minimum mTLS binding strength the host must support for the request to succeed.
/// Defaults to <see cref="MtlsBindingStrength.None"/> (no floor).
Expand Down Expand Up @@ -61,6 +68,7 @@ public void LogParameters(ILoggerAdapter logger)
ClientClaims: {!string.IsNullOrEmpty(ClientClaims)}
RevokedTokenHash: {!string.IsNullOrEmpty(RevokedTokenHash)}
IsMtlsPopRequested: {IsMtlsPopRequested}
IsMtlsBearerRequested: {IsMtlsBearerRequested}
MtlsPopMinStrength: {MtlsPopMinStrength}
""");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ public X509Certificate2 MtlsCertificate
}

public bool IsMtlsPopRequested => _commonParameters.IsMtlsPopRequested;
public bool IsMtlsBearerRequested => _commonParameters.IsMtlsBearerRequested;
public MtlsBindingStrength MtlsPopMinStrength => _commonParameters.MtlsPopMinStrength;
public bool? SendOfflineAccessScope => _commonParameters.SendOfflineAccessScope;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ private async Task<AuthenticationResult> SendTokenRequestForManagedIdentityAsync
await ResolveAuthorityAsync().ConfigureAwait(false);

_managedIdentityParameters.IsMtlsPopRequested = AuthenticationRequestParameters.IsMtlsPopRequested;
_managedIdentityParameters.IsMtlsBearerRequested = AuthenticationRequestParameters.IsMtlsBearerRequested;

// Propagate client-originated claims to the MI parameters for transport.
// Unlike server-issued Claims (which bypass the cache), ClientClaims participate in caching
Expand All @@ -230,11 +231,11 @@ private async Task<AuthenticationResult> SendTokenRequestForManagedIdentityAsync
_managedIdentityParameters.ClientClaims = AuthenticationRequestParameters.ClientClaims;
}

// mTLS PoP is served exclusively by IMDSv2. Mint the binding certificate, then delegate the
// token leg to MSAL's internal TokenClient exchange (the same path CCA uses) so client-originated
// claims, client-capability (CP1) merge, claims-based cache keying, and ESTS error handling are
// inherited rather than re-implemented in a bespoke MI token POST.
if (AuthenticationRequestParameters.IsMtlsPopRequested)
// mTLS PoP and mTLS ****** both served exclusively by IMDSv2. Mint the binding certificate,
// then delegate the token leg to MSAL's internal TokenClient exchange (the same path CCA uses)
// so client-originated claims, client-capability (CP1) merge, claims-based cache keying, and
// ESTS error handling are inherited rather than re-implemented in a bespoke MI token POST.
if (AuthenticationRequestParameters.IsMtlsPopRequested || AuthenticationRequestParameters.IsMtlsBearerRequested)
{
return await SendDelegatedImdsV2TokenRequestAsync(logger, cancellationToken).ConfigureAwait(false);
}
Expand Down Expand Up @@ -297,14 +298,21 @@ private async Task<MsalTokenResponse> DelegateImdsV2TokenLegAsync(
cancellationToken)
.ConfigureAwait(false);

// Inject the IMDS-minted cert as the request mTLS transport cert and apply the mtls_pop scheme
// so TokenClient emits token_type=mtls_pop and the resulting token is bound to the certificate.
// Inject the IMDS-minted cert as the request mTLS transport cert.
// For PoP: apply the mtls_pop scheme so TokenClient emits token_type=mtls_pop and the
// resulting token is bound to the certificate.
// For Bearer: inject the cert only for the mTLS channel (no PoP scheme); the resulting
// token is a standard bearer token with no binding certificate.
AuthenticationRequestParameters.MtlsCertificate = binding.Certificate;
AuthenticationRequestParameters.AuthenticationScheme =
new MtlsPopAuthenticationOperation(binding.Certificate);
bool isMtlsBearer = AuthenticationRequestParameters.IsMtlsBearerRequested;
if (!isMtlsBearer)
{
AuthenticationRequestParameters.AuthenticationScheme =
new MtlsPopAuthenticationOperation(binding.Certificate);

// Remember the cert so subsequent cache lookups compute the same x5t#S256 cache key.
_managedIdentityClient.SetRuntimeMtlsBindingCertificate(binding.Certificate);
// Remember the cert so subsequent cache lookups compute the same x5t#S256 cache key.
_managedIdentityClient.SetRuntimeMtlsBindingCertificate(binding.Certificate);
}

// grant_type is not added by TokenClient; client_id overrides AppConfig.ClientId
// (the SAMI placeholder) with the canonical GUID from the binding. Client-originated claims
Expand All @@ -315,6 +323,13 @@ private async Task<MsalTokenResponse> DelegateImdsV2TokenLegAsync(
[OAuth2Parameter.ClientId] = binding.ClientId
};

// For mTLS Bearer, explicitly request token_type=bearer so ESTS returns a standard
// bearer token (no cnf claim, no binding certificate).
if (isMtlsBearer)
{
bodyParameters[OAuth2Parameter.TokenType] = Constants.BearerTokenType;
}

string tokenEndpoint = binding.Endpoint.TrimEnd('/') + ImdsV2ManagedIdentitySource.AcquireEntraTokenPath;
string scopeOverride = resource.TrimEnd('/') + "/.default";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ internal abstract class AbstractManagedIdentity

protected bool _isMtlsPopRequested;

/// <summary>
/// True when <see cref="ManagedIdentityPopExtensions.WithMtlsBearerToken"/> was called.
/// The IMDSv2 attested mTLS flow is used, but <c>token_type=bearer</c> is requested from ESTS.
/// </summary>
protected bool _isMtlsBearerRequested;

internal const string TimeoutError = "[Managed Identity] Authentication unavailable. The request to the managed identity endpoint timed out.";
internal readonly ManagedIdentitySource _sourceType;

Expand Down Expand Up @@ -61,6 +67,7 @@ public virtual async Task<ManagedIdentityResponse> AuthenticateAsync(
string resource = parameters.Resource;

_isMtlsPopRequested = parameters.IsMtlsPopRequested;
_isMtlsBearerRequested = parameters.IsMtlsBearerRequested;

ManagedIdentityRequest request = await CreateRequestAsync(resource).ConfigureAwait(false);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ internal async Task<ManagedIdentityResponse> SendTokenRequestForManagedIdentityA
AcquireTokenForManagedIdentityParameters parameters,
CancellationToken cancellationToken)
{
AbstractManagedIdentity msi = await GetOrSelectManagedIdentitySourceAsync(requestContext, parameters.IsMtlsPopRequested, cancellationToken).ConfigureAwait(false);
AbstractManagedIdentity msi = await GetOrSelectManagedIdentitySourceAsync(requestContext, parameters.IsMtlsPopRequested || parameters.IsMtlsBearerRequested, cancellationToken).ConfigureAwait(false);
return await msi.AuthenticateAsync(parameters, cancellationToken).ConfigureAwait(false);
}

Expand Down Expand Up @@ -185,7 +185,7 @@ private Task<AbstractManagedIdentity> GetOrSelectManagedIdentitySourceAsync(
// We do NOT latch this state; future PoP requests can still leverage the cached ImdsV2 discovery.
if (isImdsV2 && !isMtlsPopRequested)
{
requestContext.Logger.Info("[Managed Identity] ImdsV2 detected, but mTLS PoP was not requested. Using ImdsV1 for this request only. Please use the \"WithMtlsProofOfPossession\" API to request a token via ImdsV2.");
requestContext.Logger.Info("[Managed Identity] ImdsV2 detected, but neither mTLS PoP nor mTLS Bearer requested. Using IMDSv1 for this request only. Please use the \"WithMtlsProofOfPossession\" or \"WithMtlsBearerToken\" API to request a token via ImdsV2.");

// Do NOT modify s_cachedSourceResult; keep cached ImdsV2 so future PoP
// requests can leverage it. Route this request through IMDSv1 only.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,37 @@ public static AcquireTokenForManagedIdentityParameterBuilder WithMtlsProofOfPoss
builder.CommonParameters.IsMtlsPopRequested = true;
builder.CommonParameters.MtlsPopMinStrength = options.MinStrength;
return builder;
#endif
}

/// <summary>
/// Uses the IMDSv2 attested flow (Credential Guard–issued certificate over mTLS) to acquire
/// a standard bearer token. The mTLS certificate authenticates the connection to the ESTS
/// token endpoint, but the returned token carries <c>token_type=bearer</c> and has no
/// binding certificate in the <see cref="AuthenticationResult"/>.
/// Requires Windows Credential Guard (VBS) to be enabled on the host.
/// When attestation is required, call <c>.WithAttestationSupport()</c> (from the
/// <c>Microsoft.Identity.Client.KeyAttestation</c> package) after this method.
/// </summary>
/// <param name="builder">The AcquireTokenForManagedIdentityParameterBuilder instance.</param>
/// <returns>The builder to chain .With methods.</returns>
public static AcquireTokenForManagedIdentityParameterBuilder WithMtlsBearerToken(
this AcquireTokenForManagedIdentityParameterBuilder builder)
{
if (!DesktopOsHelper.IsWindows())
{
throw new MsalClientException(
MsalError.MtlsNotSupportedForManagedIdentity,
MsalErrorMessage.MtlsNotSupportedForNonWindowsMessage);
}

#if NET462
throw new MsalClientException(
MsalError.MtlsNotSupportedForManagedIdentity,
MsalErrorMessage.MtlsNotSupportedForManagedIdentityMessage);
#else
builder.CommonParameters.IsMtlsBearerRequested = true;
return builder;
#endif
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ internal class ImdsV2ManagedIdentitySource : IImdsV2MtlsBindingSource
private readonly RequestContext _requestContext;
private readonly IMtlsCertificateCache _mtlsCache;
private bool _isMtlsPopRequested;
private bool _isMtlsBearerRequested;
private Func<string, SafeHandle, string, string, ILoggerAdapter, CancellationToken, Task<string>> _attestationTokenProvider;

// used in unit tests
Expand Down Expand Up @@ -309,11 +310,11 @@ private async Task<MtlsBindingInfo> AcquireMtlsBindingAsync()
{
CsrMetadata csrMetadata = await GetCsrMetadataAsync(_requestContext).ConfigureAwait(false);

// Early validation: Fail-fast if mTLS PoP was requested but KeyGuard is unavailable.
// Early validation: Fail-fast if KeyGuard is required (mTLS PoP or mTLS Bearer) but unavailable.
// This check happens before any network calls to avoid wasted round-trips.
// Note: This creates/retrieves the key, but on cache hit scenarios (below),
// this may be the only key access needed.
if (_isMtlsPopRequested)
if (_isMtlsPopRequested || _isMtlsBearerRequested)
{
IManagedIdentityKeyProvider keyProvider = _requestContext.ServiceBundle.PlatformProxy.ManagedIdentityKeyProvider;
ManagedIdentityKeyInfo keyInfo = await keyProvider
Expand All @@ -322,12 +323,13 @@ private async Task<MtlsBindingInfo> AcquireMtlsBindingAsync()

if (keyInfo.Type != ManagedIdentityKeyType.KeyGuard)
{
string flowName = _isMtlsPopRequested ? "mTLS Proof-of-Possession" : "mTLS Bearer";
throw new MsalClientException(
"mtls_pop_requires_keyguard",
$"[ImdsV2] mTLS Proof-of-Possession currently requires a KeyGuard key, but this host produced a '{keyInfo.Type}' key. " +
"credential_guard_not_available",
$"[ImdsV2] {flowName} currently requires a KeyGuard key, but this host produced a '{keyInfo.Type}' key. " +
"The host may report Software-strength binding capability (which means it can bind a token to a key), " +
"but the IMDSv2 PoP token flow only accepts VBS-isolated KeyGuard keys today. " +
"Ensure Virtualization-based Security (VBS)/KeyGuard is enabled on the host, or request a bearer token instead.");
"but the IMDSv2 attested flow only accepts VBS-isolated KeyGuard keys today. " +
"Ensure Virtualization-based Security (VBS)/KeyGuard is enabled on the host.");
}
}

Expand Down Expand Up @@ -403,7 +405,15 @@ public async Task<MtlsBindingInfo> AcquireMtlsBindingForDelegationAsync(
CancellationToken cancellationToken)
{
_attestationTokenProvider = parameters.AttestationTokenProvider;
_isMtlsPopRequested = true;
_isMtlsPopRequested = parameters.IsMtlsPopRequested;
_isMtlsBearerRequested = parameters.IsMtlsBearerRequested;

// Ensure at least one IMDSv2 attested flag is set; default to PoP for backward compatibility
// with callers that do not set either flag explicitly.
if (!_isMtlsPopRequested && !_isMtlsBearerRequested)
{
_isMtlsPopRequested = true;
}

if (forceRemint && _mtlsCache is MtlsBindingCache bindingCache)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ Microsoft.Identity.Client.AppConfig.PoPOptions.MinStrength.get -> Microsoft.Iden
Microsoft.Identity.Client.AppConfig.PoPOptions.MinStrength.set -> void
Microsoft.Identity.Client.AppConfig.PoPOptions.PoPOptions() -> void
static Microsoft.Identity.Client.Extensibility.AcquireTokenParameterBuilderExtensions.WithCachePartitionKey<T>(this Microsoft.Identity.Client.BaseAbstractAcquireTokenParameterBuilder<T> builder, string key, string value, bool partitionRefreshToken) -> T
static Microsoft.Identity.Client.ManagedIdentityPopExtensions.WithMtlsBearerToken(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder
static Microsoft.Identity.Client.ManagedIdentityPopExtensions.WithMtlsProofOfPossession(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder builder, Microsoft.Identity.Client.AppConfig.PoPOptions options) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ Microsoft.Identity.Client.AppConfig.PoPOptions.MinStrength.get -> Microsoft.Iden
Microsoft.Identity.Client.AppConfig.PoPOptions.MinStrength.set -> void
Microsoft.Identity.Client.AppConfig.PoPOptions.PoPOptions() -> void
static Microsoft.Identity.Client.Extensibility.AcquireTokenParameterBuilderExtensions.WithCachePartitionKey<T>(this Microsoft.Identity.Client.BaseAbstractAcquireTokenParameterBuilder<T> builder, string key, string value, bool partitionRefreshToken) -> T
static Microsoft.Identity.Client.ManagedIdentityPopExtensions.WithMtlsBearerToken(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder
static Microsoft.Identity.Client.ManagedIdentityPopExtensions.WithMtlsProofOfPossession(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder builder, Microsoft.Identity.Client.AppConfig.PoPOptions options) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ Microsoft.Identity.Client.AppConfig.PoPOptions.MinStrength.get -> Microsoft.Iden
Microsoft.Identity.Client.AppConfig.PoPOptions.MinStrength.set -> void
Microsoft.Identity.Client.AppConfig.PoPOptions.PoPOptions() -> void
static Microsoft.Identity.Client.Extensibility.AcquireTokenParameterBuilderExtensions.WithCachePartitionKey<T>(this Microsoft.Identity.Client.BaseAbstractAcquireTokenParameterBuilder<T> builder, string key, string value, bool partitionRefreshToken) -> T
static Microsoft.Identity.Client.ManagedIdentityPopExtensions.WithMtlsBearerToken(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder
static Microsoft.Identity.Client.ManagedIdentityPopExtensions.WithMtlsProofOfPossession(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder builder, Microsoft.Identity.Client.AppConfig.PoPOptions options) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder
Loading