diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/Executors/ManagedIdentityExecutor.cs b/src/client/Microsoft.Identity.Client/ApiConfig/Executors/ManagedIdentityExecutor.cs index 6b56a13a0b..11564fee5a 100644 --- a/src/client/Microsoft.Identity.Client/ApiConfig/Executors/ManagedIdentityExecutor.cs +++ b/src/client/Microsoft.Identity.Client/ApiConfig/Executors/ManagedIdentityExecutor.cs @@ -40,12 +40,30 @@ public async Task ExecuteAsync( requestContext, _managedIdentityApplication.AppTokenCacheInternal).ConfigureAwait(false); - var handler = new ManagedIdentityAuthRequest( - ServiceBundle, - requestParams, - managedIdentityParameters); + // Determine the Managed Identity Source + ManagedIdentitySource managedIdentitySource = + await ManagedIdentityClient.GetManagedIdentitySourceAsync(ServiceBundle, cancellationToken) + .ConfigureAwait(false); - return await handler.RunAsync(cancellationToken).ConfigureAwait(false); + ManagedIdentityAuthRequestBase authRequest; + + if (managedIdentitySource == ManagedIdentitySource.Credential) + { + authRequest = new CredentialManagedIdentityAuthRequest( + ServiceBundle, + requestParams, + managedIdentityParameters); + } + else + { + authRequest = new LegacyManagedIdentityAuthRequest( + ServiceBundle, + requestParams, + managedIdentityParameters); + } + + // Execute the request + return await authRequest.RunAsync(cancellationToken).ConfigureAwait(false); } diff --git a/src/client/Microsoft.Identity.Client/Internal/Requests/CredentialManagedIdentityAuthRequest.cs b/src/client/Microsoft.Identity.Client/Internal/Requests/CredentialManagedIdentityAuthRequest.cs new file mode 100644 index 0000000000..24038c2874 --- /dev/null +++ b/src/client/Microsoft.Identity.Client/Internal/Requests/CredentialManagedIdentityAuthRequest.cs @@ -0,0 +1,211 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Identity.Client.ApiConfig.Parameters; +using Microsoft.Identity.Client.Cache.Items; +using Microsoft.Identity.Client.Core; +using Microsoft.Identity.Client.Http; +using Microsoft.Identity.Client.OAuth2; +using Microsoft.Identity.Client.Utils; +using Microsoft.Identity.Client.ManagedIdentity; +using System.Security.Cryptography.X509Certificates; + +namespace Microsoft.Identity.Client.Internal.Requests +{ + /// + /// Implements MSI V2 token acquisition flow for VM/VMSS using the `/issuecredential` endpoint. + /// This request uses a short-lived binding certificate to perform mTLS authentication against ESTS. + /// + /// Flow Overview: + /// 1. Call getPlatformMetadata to retrieve tenant_id, client_id (UAID), CUID, and MAA endpoint. + /// 2. Generate or load key material and build a CSR (with CUID attribute). + /// 3. If attestation is required (attestable CU), obtain attestation_token from MAA. + /// 4. Call /issuecredential endpoint with CSR (+ attestation_token if applicable) to obtain: + /// - Binding certificate (valid ~7 days) + /// - Regional token endpoint URL + /// 5. Perform mTLS token request to ESTS regional endpoint to acquire access token. + /// 6. Cache and return AuthenticationResult (access token + cert if needed by caller). + /// + internal sealed class CredentialManagedIdentityAuthRequest : ManagedIdentityAuthRequestBase + { + internal const string IdentityUnavailableError = "[Managed Identity] Authentication unavailable. Either the requested identity has not been assigned to this resource, or other errors could be present. See inner exception."; + internal const string GatewayError = "[Managed Identity] Authentication unavailable. The request failed due to a gateway error."; + + public CredentialManagedIdentityAuthRequest( + IServiceBundle serviceBundle, + AuthenticationRequestParameters authenticationRequestParameters, + AcquireTokenForManagedIdentityParameters managedIdentityParameters) + : base(serviceBundle, authenticationRequestParameters, managedIdentityParameters) + { + } + + /// + /// Main entry point for MSI V2 token acquisition. + /// + protected override async Task SendTokenRequestAsync( + ILoggerAdapter logger, + CancellationToken cancellationToken) + { + Exception exception = null; + string message = string.Empty; + + try + { + // + // STEP 1: Retrieve platform metadata + // - Endpoint: GET /metadata/identity/getPlatformMetadata?api-version=2025-05-01 + // - Required headers: Metadata=true + // - Returns: UAID (client_id), tenant_id, CUID, MAA endpoint (if attestable) + // + ManagedIdentityMetadataResponse metadata = + await GetMetaDataAsync().ConfigureAwait(false); + + // + // STEP 2: Generate or load key & build CSR + // - CSR subject: CN={client_id}, DC={tenant_id} + // - Attribute OID 1.2.840.113549.1.9.7 = CUID (PrintableString) + // - Signed with: RSA 2048 + // - Durable key if from KeyGuard KSP (Windows attested) + // + // TODO: Implement KeyStore selection based on OS and attestation capability. + // + + // + // STEP 3: (Optional) Obtain attestation token + // - Required for attested compute units (KeyGuard) + // - POST to MAA /attest/keyguard endpoint with key info + // - Skip for unattested flows + // - Next Commit will have this implemented. (Owner - Gladwin) + + // + // STEP 4: Call /issuecredential endpoint + // - POST /metadata/identity/issuecredential?cid={CUID}&uaid={client_id}&api-version=2025-05-01 + // - Body: { "csr": "", "attestation_token": ""? } + // - Returns: client_credential (Base64 DER cert), regional_token_url + // + ManagedIdentityCredentialResponse credentialResponse = + await GetCredentialCertificateAsync().ConfigureAwait(false); + + var bindingCert = new X509Certificate2( + credentialResponse.CertificateForMtls, + (string)null, + X509KeyStorageFlags.MachineKeySet); + + // + // STEP 5: Build OAuth2 client for mTLS token request + // + OAuth2Client mtlsClient = CreateMtlsClientRequest( + AuthenticationRequestParameters.RequestContext.ServiceBundle.HttpManager, + credentialResponse, + bindingCert); + + // + // STEP 6: Perform mTLS token request to ESTS + // - Endpoint: {regional_token_url}/{tenant_id}/oauth2/v2.0/token + // - grant_type=client_credentials + // - scope=.../.default + // - token_type=mtls_pop for attested flows, default bearer otherwise + // + Uri tokenUrl = new Uri(credentialResponse.RegionalTokenUrl); // from /issuecredential + + MsalTokenResponse msalTokenResponse = await mtlsClient.GetTokenAsync( + tokenUrl, + AuthenticationRequestParameters.RequestContext, + true, + AuthenticationRequestParameters.OnBeforeTokenRequestHandler) + .ConfigureAwait(false); + + msalTokenResponse.Scope = AuthenticationRequestParameters.Scope.AsSingleString(); + + logger.Info("[CredentialManagedIdentityAuthRequest] Successfully acquired token via MSI V2 mTLS flow."); + + // + // STEP 7: Cache and return AuthenticationResult + // + return await CacheTokenResponseAndCreateAuthenticationResultAsync(msalTokenResponse) + .ConfigureAwait(false); + } + catch (Exception ex) + { + logger.Error($"[CredentialManagedIdentityAuthRequest] Exception: {ex}"); + message = IdentityUnavailableError; + exception = ex; + + throw MsalServiceExceptionFactory.CreateManagedIdentityException( + MsalError.ManagedIdentityRequestFailed, + message, + exception, + ManagedIdentitySource.Credential, + null); + } + } + + /// + /// Calls getPlatformMetadata endpoint. + /// TODO: Implement real HTTP call. + /// +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + private async Task GetMetaDataAsync() +#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously + { + // Placeholder: populate with actual HTTP call to IMDS getPlatformMetadata endpoint. + return new ManagedIdentityMetadataResponse + { + ClientId = "TODO", + TenantId = "TODO", + //Other properties + }; + } + + /// + /// Calls /issuecredential endpoint with CSR (+ attestation token if applicable). + /// TODO: Implement CSR generation, attestation handling, and HTTP call. + /// +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + private async Task GetCredentialCertificateAsync() +#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously + { + // Placeholder: populate with actual HTTP call to /issuecredential endpoint. + return new ManagedIdentityCredentialResponse + { + CertificateForMtls = Array.Empty(), + ClientId = "TODO", + RegionalTokenUrl = "TODO" + }; + } + + private OAuth2Client CreateMtlsClientRequest( + IHttpManager httpManager, + ManagedIdentityCredentialResponse credentialResponse, + X509Certificate2 x509Certificate2) + { + var client = new OAuth2Client( + AuthenticationRequestParameters.RequestContext.Logger, + httpManager, + x509Certificate2); + + // Ensure scope ends with /.default for client_credential flows + string scopes = AuthenticationRequestParameters.Scope.AsSingleString(); + if (!scopes.EndsWith("/.default", StringComparison.OrdinalIgnoreCase)) + { + scopes += "/.default"; + } + + client.AddBodyParameter(OAuth2Parameter.GrantType, OAuth2GrantType.ClientCredentials); + client.AddBodyParameter(OAuth2Parameter.Scope, scopes); + client.AddBodyParameter(OAuth2Parameter.ClientId, credentialResponse.ClientId); + + if (!string.IsNullOrWhiteSpace(AuthenticationRequestParameters.ClaimsAndClientCapabilities)) + { + client.AddBodyParameter(OAuth2Parameter.Claims, AuthenticationRequestParameters.ClaimsAndClientCapabilities); + } + + // TODO: Add token_type=mtls_pop requested. + return client; + } + } +} diff --git a/src/client/Microsoft.Identity.Client/Internal/Requests/LegacyManagedIdentityAuthRequest.cs b/src/client/Microsoft.Identity.Client/Internal/Requests/LegacyManagedIdentityAuthRequest.cs new file mode 100644 index 0000000000..a64cecfef7 --- /dev/null +++ b/src/client/Microsoft.Identity.Client/Internal/Requests/LegacyManagedIdentityAuthRequest.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Identity.Client.ApiConfig.Parameters; +using Microsoft.Identity.Client.Core; +using Microsoft.Identity.Client.ManagedIdentity; +using Microsoft.Identity.Client.OAuth2; +using Microsoft.Identity.Client.Utils; + +namespace Microsoft.Identity.Client.Internal.Requests +{ + /// + /// Legacy (non-credential-based) MI flow using ManagedIdentityClient.SendTokenRequestForManagedIdentityAsync. + /// + internal sealed class LegacyManagedIdentityAuthRequest : ManagedIdentityAuthRequestBase + { + public LegacyManagedIdentityAuthRequest( + IServiceBundle serviceBundle, + AuthenticationRequestParameters authenticationRequestParameters, + AcquireTokenForManagedIdentityParameters managedIdentityParameters) + : base(serviceBundle, authenticationRequestParameters, managedIdentityParameters) + { + } + + protected override async Task SendTokenRequestAsync( + ILoggerAdapter logger, + CancellationToken cancellationToken) + { + logger.Info("[ManagedIdentityRequest:Legacy] Acquiring a token from the managed identity endpoint."); + + ManagedIdentityClient managedIdentityClient = + await ManagedIdentityClient.CreateAsync( + AuthenticationRequestParameters.RequestContext, + cancellationToken).ConfigureAwait(false); + + ManagedIdentityResponse managedIdentityResponse = + await managedIdentityClient + .SendTokenRequestForManagedIdentityAsync(_managedIdentityParameters, cancellationToken) + .ConfigureAwait(false); + + var msalTokenResponse = MsalTokenResponse.CreateFromManagedIdentityResponse(managedIdentityResponse); + msalTokenResponse.Scope = AuthenticationRequestParameters.Scope.AsSingleString(); + + return await CacheTokenResponseAndCreateAuthenticationResultAsync(msalTokenResponse) + .ConfigureAwait(false); + } + + protected override KeyValuePair? GetCcsHeader(IDictionary _) => null; + } +} diff --git a/src/client/Microsoft.Identity.Client/Internal/Requests/ManagedIdentityAuthRequest.cs b/src/client/Microsoft.Identity.Client/Internal/Requests/ManagedIdentityAuthRequest.cs deleted file mode 100644 index 67e9590e6a..0000000000 --- a/src/client/Microsoft.Identity.Client/Internal/Requests/ManagedIdentityAuthRequest.cs +++ /dev/null @@ -1,245 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System.Collections.Generic; -using System.Net; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Identity.Client.ApiConfig.Parameters; -using Microsoft.Identity.Client.Cache.Items; -using Microsoft.Identity.Client.Core; -using Microsoft.Identity.Client.ManagedIdentity; -using Microsoft.Identity.Client.OAuth2; -using Microsoft.Identity.Client.PlatformsCommon.Interfaces; -using Microsoft.Identity.Client.Utils; - -namespace Microsoft.Identity.Client.Internal.Requests -{ - internal class ManagedIdentityAuthRequest : RequestBase - { - private readonly AcquireTokenForManagedIdentityParameters _managedIdentityParameters; - private static readonly SemaphoreSlim s_semaphoreSlim = new SemaphoreSlim(1, 1); - private readonly ICryptographyManager _cryptoManager; - - public ManagedIdentityAuthRequest( - IServiceBundle serviceBundle, - AuthenticationRequestParameters authenticationRequestParameters, - AcquireTokenForManagedIdentityParameters managedIdentityParameters) - : base(serviceBundle, authenticationRequestParameters, managedIdentityParameters) - { - _managedIdentityParameters = managedIdentityParameters; - _cryptoManager = serviceBundle.PlatformProxy.CryptographyManager; - } - - protected override async Task ExecuteAsync(CancellationToken cancellationToken) - { - AuthenticationResult authResult = null; - ILoggerAdapter logger = AuthenticationRequestParameters.RequestContext.Logger; - - // 1. FIRST, handle ForceRefresh - if (_managedIdentityParameters.ForceRefresh) - { - //log a warning if Claims are also set - if (!string.IsNullOrEmpty(AuthenticationRequestParameters.Claims)) - { - logger.Warning("[ManagedIdentityRequest] Both ForceRefresh and Claims are set. Using ForceRefresh to skip cache."); - } - - AuthenticationRequestParameters.RequestContext.ApiEvent.CacheInfo = CacheRefreshReason.ForceRefreshOrClaims; - logger.Info("[ManagedIdentityRequest] Skipped using the cache because ForceRefresh was set."); - - // Straight to the MI endpoint - authResult = await GetAccessTokenAsync(cancellationToken, logger).ConfigureAwait(false); - return authResult; - } - - // 2. Otherwise, look for a cached token - MsalAccessTokenCacheItem cachedAccessTokenItem = await GetCachedAccessTokenAsync() - .ConfigureAwait(false); - - // If we have claims, we do NOT use the cached token (but we still need it to compute the hash). - if (!string.IsNullOrEmpty(AuthenticationRequestParameters.Claims)) - { - _managedIdentityParameters.Claims = AuthenticationRequestParameters.Claims; - AuthenticationRequestParameters.RequestContext.ApiEvent.CacheInfo = CacheRefreshReason.ForceRefreshOrClaims; - - // If there is a cached token, compute its hash for the “revoked token” scenario - if (cachedAccessTokenItem != null) - { - string cachedTokenHash = _cryptoManager.CreateSha256HashHex(cachedAccessTokenItem.Secret); - _managedIdentityParameters.RevokedTokenHash = cachedTokenHash; - - logger.Info("[ManagedIdentityRequest] Claims are present. Computed hash of the cached (revoked) token. " + - "Will now request a fresh token from the MI endpoint."); - } - else - { - logger.Info("[ManagedIdentityRequest] Claims are present, but no cached token was found. " + - "Requesting a fresh token from the MI endpoint without a revoked-token hash."); - } - - // In both cases, we skip using the cached token and get a new one - authResult = await GetAccessTokenAsync(cancellationToken, logger).ConfigureAwait(false); - return authResult; - } - - // 3. If we have no ForceRefresh and no claims, we can use the cache - if (cachedAccessTokenItem != null) - { - authResult = CreateAuthenticationResultFromCache(cachedAccessTokenItem); - - logger.Info("[ManagedIdentityRequest] Access token retrieved from cache."); - - try - { - var proactivelyRefresh = SilentRequestHelper.NeedsRefresh(cachedAccessTokenItem); - - // If needed, refreshes token in the background - if (proactivelyRefresh) - { - logger.Info("[ManagedIdentityRequest] Initiating a proactive refresh."); - - AuthenticationRequestParameters.RequestContext.ApiEvent.CacheInfo = CacheRefreshReason.ProactivelyRefreshed; - - SilentRequestHelper.ProcessFetchInBackground( - cachedAccessTokenItem, - () => - { - // Use a linked token source, in case the original cancellation token source is disposed before this background task completes. - using var tokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - return GetAccessTokenAsync(tokenSource.Token, logger); - }, logger, ServiceBundle, AuthenticationRequestParameters.RequestContext.ApiEvent, - AuthenticationRequestParameters.RequestContext.ApiEvent.CallerSdkApiId, - AuthenticationRequestParameters.RequestContext.ApiEvent.CallerSdkVersion); - } - } - catch (MsalServiceException e) - { - // If background refresh fails, we handle the exception - return await HandleTokenRefreshErrorAsync(e, cachedAccessTokenItem).ConfigureAwait(false); - } - } - else - { - // No cached token - if (AuthenticationRequestParameters.RequestContext.ApiEvent.CacheInfo != CacheRefreshReason.Expired) - { - AuthenticationRequestParameters.RequestContext.ApiEvent.CacheInfo = CacheRefreshReason.NoCachedAccessToken; - } - - logger.Info("[ManagedIdentityRequest] No cached access token found. " + - "Getting a token from the managed identity endpoint."); - - authResult = await GetAccessTokenAsync(cancellationToken, logger).ConfigureAwait(false); - } - - return authResult; - } - - private async Task GetAccessTokenAsync( - CancellationToken cancellationToken, - ILoggerAdapter logger) - { - AuthenticationResult authResult; - MsalAccessTokenCacheItem cachedAccessTokenItem = null; - - // Requests to a managed identity endpoint must be throttled; - // otherwise, the endpoint will throw a HTTP 429. - logger.Verbose(() => "[ManagedIdentityRequest] Entering managed identity request semaphore."); - await s_semaphoreSlim.WaitAsync(cancellationToken).ConfigureAwait(false); - logger.Verbose(() => "[ManagedIdentityRequest] Entered managed identity request semaphore."); - - try - { - // While holding the semaphore, decide whether to bypass the cache. - // Re-check because another thread may have filled the cache while we waited. - // Bypass when: - // 1) ForceRefresh is requested - // 2) Proactive refresh is in effect - // 3) Claims are present (revocation flow) - if (_managedIdentityParameters.ForceRefresh || - AuthenticationRequestParameters.RequestContext.ApiEvent.CacheInfo == CacheRefreshReason.ProactivelyRefreshed || - !string.IsNullOrEmpty(_managedIdentityParameters.Claims)) - { - authResult = await SendTokenRequestForManagedIdentityAsync(logger, cancellationToken).ConfigureAwait(false); - } - else - { - logger.Info("[ManagedIdentityRequest] Checking for a cached access token."); - cachedAccessTokenItem = await GetCachedAccessTokenAsync().ConfigureAwait(false); - - // Check the cache again after acquiring the semaphore in case the previous request cached a new token. - if (cachedAccessTokenItem != null) - { - authResult = CreateAuthenticationResultFromCache(cachedAccessTokenItem); - } - else - { - authResult = await SendTokenRequestForManagedIdentityAsync(logger, cancellationToken).ConfigureAwait(false); - } - } - - return authResult; - } - finally - { - s_semaphoreSlim.Release(); - logger.Verbose(() => "[ManagedIdentityRequest] Released managed identity request semaphore."); - } - } - - private async Task SendTokenRequestForManagedIdentityAsync(ILoggerAdapter logger, CancellationToken cancellationToken) - { - logger.Info("[ManagedIdentityRequest] Acquiring a token from the managed identity endpoint."); - - await ResolveAuthorityAsync().ConfigureAwait(false); - - ManagedIdentityClient managedIdentityClient = - new ManagedIdentityClient(AuthenticationRequestParameters.RequestContext); - - ManagedIdentityResponse managedIdentityResponse = - await managedIdentityClient - .SendTokenRequestForManagedIdentityAsync(_managedIdentityParameters, cancellationToken) - .ConfigureAwait(false); - - var msalTokenResponse = MsalTokenResponse.CreateFromManagedIdentityResponse(managedIdentityResponse); - msalTokenResponse.Scope = AuthenticationRequestParameters.Scope.AsSingleString(); - - return await CacheTokenResponseAndCreateAuthenticationResultAsync(msalTokenResponse).ConfigureAwait(false); - } - - private async Task GetCachedAccessTokenAsync() - { - MsalAccessTokenCacheItem cachedAccessTokenItem = await CacheManager.FindAccessTokenAsync().ConfigureAwait(false); - - if (cachedAccessTokenItem != null) - { - AuthenticationRequestParameters.RequestContext.ApiEvent.IsAccessTokenCacheHit = true; - Metrics.IncrementTotalAccessTokensFromCache(); - return cachedAccessTokenItem; - } - - return null; - } - - private AuthenticationResult CreateAuthenticationResultFromCache(MsalAccessTokenCacheItem cachedAccessTokenItem) - { - AuthenticationResult authResult = new AuthenticationResult( - cachedAccessTokenItem, - null, - AuthenticationRequestParameters.AuthenticationScheme, - AuthenticationRequestParameters.RequestContext.CorrelationId, - TokenSource.Cache, - AuthenticationRequestParameters.RequestContext.ApiEvent, - account: null, - spaAuthCode: null, - additionalResponseParameters: null); - return authResult; - } - - protected override KeyValuePair? GetCcsHeader(IDictionary additionalBodyParameters) - { - return null; - } - } -} diff --git a/src/client/Microsoft.Identity.Client/Internal/Requests/ManagedIdentityAuthRequestBase.cs b/src/client/Microsoft.Identity.Client/Internal/Requests/ManagedIdentityAuthRequestBase.cs new file mode 100644 index 0000000000..20f6c73fc4 --- /dev/null +++ b/src/client/Microsoft.Identity.Client/Internal/Requests/ManagedIdentityAuthRequestBase.cs @@ -0,0 +1,192 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Identity.Client.ApiConfig.Parameters; +using Microsoft.Identity.Client.Cache.Items; +using Microsoft.Identity.Client.Core; +using Microsoft.Identity.Client.OAuth2; +using Microsoft.Identity.Client.PlatformsCommon.Interfaces; +using Microsoft.Identity.Client.Utils; + +namespace Microsoft.Identity.Client.Internal.Requests +{ + /// + /// Shared Managed Identity request pipeline: + /// - ForceRefresh / Claims handling (incl. revoked-token hash) + /// - Cache hit path + proactive background refresh + /// - Single-flight semaphore for endpoint calls + /// Derived classes only implement . + /// + internal abstract class ManagedIdentityAuthRequestBase : RequestBase + { + protected readonly AcquireTokenForManagedIdentityParameters _managedIdentityParameters; + protected static readonly SemaphoreSlim s_semaphoreSlim = new(1, 1); + protected readonly ICryptographyManager _cryptoManager; + + protected ManagedIdentityAuthRequestBase( + IServiceBundle serviceBundle, + AuthenticationRequestParameters authenticationRequestParameters, + AcquireTokenForManagedIdentityParameters managedIdentityParameters) + : base(serviceBundle, authenticationRequestParameters, managedIdentityParameters) + { + _managedIdentityParameters = managedIdentityParameters; + _cryptoManager = serviceBundle.PlatformProxy.CryptographyManager; + } + + protected override async Task ExecuteAsync(CancellationToken cancellationToken) + { + AuthenticationResult authResult = null; + ILoggerAdapter logger = AuthenticationRequestParameters.RequestContext.Logger; + + // 1) ForceRefresh wins (even if claims present) + if (_managedIdentityParameters.ForceRefresh) + { + if (!string.IsNullOrEmpty(AuthenticationRequestParameters.Claims)) + { + logger.Warning("[ManagedIdentityRequest] Both ForceRefresh and Claims are set. Using ForceRefresh to skip cache."); + } + + AuthenticationRequestParameters.RequestContext.ApiEvent.CacheInfo = CacheRefreshReason.ForceRefreshOrClaims; + logger.Info("[ManagedIdentityRequest] Skipped using the cache because ForceRefresh was set."); + return await GetAccessTokenWithSemaphoreAsync(cancellationToken, logger).ConfigureAwait(false); + } + + // 2) Try cache first + var cachedAccessTokenItem = await GetCachedAccessTokenAsync().ConfigureAwait(false); + + // If claims are present, we don’t return the cached AT; we compute its hash (if any) for revocation and fetch fresh. + if (!string.IsNullOrEmpty(AuthenticationRequestParameters.Claims)) + { + _managedIdentityParameters.Claims = AuthenticationRequestParameters.Claims; + AuthenticationRequestParameters.RequestContext.ApiEvent.CacheInfo = CacheRefreshReason.ForceRefreshOrClaims; + + if (cachedAccessTokenItem != null) + { + string cachedTokenHash = _cryptoManager.CreateSha256HashHex(cachedAccessTokenItem.Secret); + _managedIdentityParameters.RevokedTokenHash = cachedTokenHash; + + logger.Info("[ManagedIdentityRequest] Claims present. Computed hash of cached (revoked) token; requesting fresh token."); + } + else + { + logger.Info("[ManagedIdentityRequest] Claims present, but no cached token. Requesting fresh token."); + } + + return await GetAccessTokenWithSemaphoreAsync(cancellationToken, logger).ConfigureAwait(false); + } + + // 3) No ForceRefresh and no claims → cache path + if (cachedAccessTokenItem != null) + { + authResult = CreateAuthenticationResultFromCache(cachedAccessTokenItem); + logger.Info("[ManagedIdentityRequest] Access token retrieved from cache."); + + try + { + var proactivelyRefresh = SilentRequestHelper.NeedsRefresh(cachedAccessTokenItem); + + if (proactivelyRefresh) + { + logger.Info("[ManagedIdentityRequest] Initiating a proactive refresh."); + AuthenticationRequestParameters.RequestContext.ApiEvent.CacheInfo = CacheRefreshReason.ProactivelyRefreshed; + + SilentRequestHelper.ProcessFetchInBackground( + cachedAccessTokenItem, + () => + { + using var tokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + return GetAccessTokenWithSemaphoreAsync(tokenSource.Token, logger); + }, + logger, + ServiceBundle, + AuthenticationRequestParameters.RequestContext.ApiEvent, + AuthenticationRequestParameters.RequestContext.ApiEvent.CallerSdkApiId, + AuthenticationRequestParameters.RequestContext.ApiEvent.CallerSdkVersion); + } + } + catch (MsalServiceException e) + { + return await HandleTokenRefreshErrorAsync(e, cachedAccessTokenItem).ConfigureAwait(false); + } + + return authResult; + } + + // 4) No cache + if (AuthenticationRequestParameters.RequestContext.ApiEvent.CacheInfo != CacheRefreshReason.Expired) + { + AuthenticationRequestParameters.RequestContext.ApiEvent.CacheInfo = CacheRefreshReason.NoCachedAccessToken; + } + + logger.Info("[ManagedIdentityRequest] No cached access token found. Getting a token from the managed identity endpoint."); + return await GetAccessTokenWithSemaphoreAsync(cancellationToken, logger).ConfigureAwait(false); + } + + private async Task GetAccessTokenWithSemaphoreAsync( + CancellationToken cancellationToken, + ILoggerAdapter logger) + { + logger.Verbose(() => "[ManagedIdentityRequest] Entering managed identity request semaphore."); + await s_semaphoreSlim.WaitAsync(cancellationToken).ConfigureAwait(false); + logger.Verbose(() => "[ManagedIdentityRequest] Entered managed identity request semaphore."); + + try + { + // Re-check cache policy while inside the semaphore in case another thread updated it. + if (_managedIdentityParameters.ForceRefresh || + AuthenticationRequestParameters.RequestContext.ApiEvent.CacheInfo == CacheRefreshReason.ProactivelyRefreshed || + !string.IsNullOrEmpty(_managedIdentityParameters.Claims)) + { + await ResolveAuthorityAsync().ConfigureAwait(false); + return await SendTokenRequestAsync(logger, cancellationToken).ConfigureAwait(false); + } + + var cached = await GetCachedAccessTokenAsync().ConfigureAwait(false); + if (cached != null) + { + return CreateAuthenticationResultFromCache(cached); + } + + await ResolveAuthorityAsync().ConfigureAwait(false); + return await SendTokenRequestAsync(logger, cancellationToken).ConfigureAwait(false); + } + finally + { + s_semaphoreSlim.Release(); + logger.Verbose(() => "[ManagedIdentityRequest] Released managed identity request semaphore."); + } + } + + protected abstract Task SendTokenRequestAsync(ILoggerAdapter logger, CancellationToken cancellationToken); + + protected async Task GetCachedAccessTokenAsync() + { + var cached = await CacheManager.FindAccessTokenAsync().ConfigureAwait(false); + if (cached != null) + { + AuthenticationRequestParameters.RequestContext.ApiEvent.IsAccessTokenCacheHit = true; + Metrics.IncrementTotalAccessTokensFromCache(); + } + return cached; + } + + protected AuthenticationResult CreateAuthenticationResultFromCache(MsalAccessTokenCacheItem cached) + { + return new AuthenticationResult( + cached, + null, + AuthenticationRequestParameters.AuthenticationScheme, + AuthenticationRequestParameters.RequestContext.CorrelationId, + TokenSource.Cache, + AuthenticationRequestParameters.RequestContext.ApiEvent, + account: null, + spaAuthCode: null, + additionalResponseParameters: null); + } + + protected override KeyValuePair? GetCcsHeader(IDictionary _) => null; + } +} diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/CredentialManagedIdentitySource.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/CredentialManagedIdentitySource.cs new file mode 100644 index 0000000000..f7294426ac --- /dev/null +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/CredentialManagedIdentitySource.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Net.Http; +using Microsoft.Identity.Client.Core; +using Microsoft.Identity.Client.Internal; + +namespace Microsoft.Identity.Client.ManagedIdentity +{ + internal class CredentialManagedIdentitySource : AbstractManagedIdentity + { + /// + /// Factory method to create an instance of `CredentialManagedIdentitySource`. + /// + public static AbstractManagedIdentity Create(RequestContext requestContext) + { + requestContext.Logger.Info(() => "[Managed Identity] Using credential based managed identity."); + + return new CredentialManagedIdentitySource(requestContext); + } + + private CredentialManagedIdentitySource(RequestContext requestContext) : + base(requestContext, ManagedIdentitySource.Credential) + { + } + + /// + /// Even though the Credential flow does not use this request, we need to satisfy the abstract contract. + /// Return a minimal, valid ManagedIdentityRequest using the fixed credential endpoint. + /// + /// The resource identifier (ignored in this flow). + /// A ManagedIdentityRequest instance using the credential endpoint. + protected override ManagedIdentityRequest CreateRequest(string resource) + { + // Return a minimal request with the fixed credential endpoint. + return new ManagedIdentityRequest( + HttpMethod.Post, + new Uri("http://169.254.169.254/metadata/identity/credential?cred-api-version=1.0")); + } + } +} diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/ImdsCredentialProbeManager.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/ImdsCredentialProbeManager.cs new file mode 100644 index 0000000000..49bfd2ec8e --- /dev/null +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/ImdsCredentialProbeManager.cs @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Identity.Client.Core; +using Microsoft.Identity.Client.Http; +using Microsoft.Identity.Client.Http.Retry; +using Microsoft.Identity.Client.Internal; + +namespace Microsoft.Identity.Client.ManagedIdentity +{ + /// + /// Uses the old IMDS credential endpoint to probe for the availability of the IMDS credential endpoint. + /// Easier to test in a VM + /// This can be the new /issuecredential endpoint or the metadata endpoint. + /// + internal class ImdsCredentialProbeManager + { + private const string CredentialEndpoint = "http://169.254.169.254/metadata/identity/credential"; + private const string ProbeBody = "."; + private const string ImdsHeader = "IMDS/"; + private readonly IServiceBundle _serviceBundle; + private readonly ILoggerAdapter _logger; + + public ImdsCredentialProbeManager(IServiceBundle serviceBundle) + { + _serviceBundle = serviceBundle ?? throw new ArgumentNullException(nameof(serviceBundle)); + _logger = serviceBundle.ApplicationLogger; + } + + public async Task ExecuteProbeAsync(CancellationToken cancellationToken = default) + { + _logger.Info("[Probe] Initiating probe to IMDS credential endpoint."); + + var request = new ManagedIdentityRequest(HttpMethod.Post, new Uri($"{CredentialEndpoint}?cred-api-version=1.0")) + { + Content = ProbeBody + }; + + HttpContent httpContent = request.CreateHttpContent(); + + _logger.Info($"[Probe] Sending request to {CredentialEndpoint}"); + _logger.Verbose(() => $"[Probe] Request Headers: {string.Join(", ", request.Headers)}"); + _logger.Verbose(() => $"[Probe] Request Body: {ProbeBody}"); + + IRetryPolicyFactory retryPolicyFactory = _serviceBundle.Config.RetryPolicyFactory; + IRetryPolicy retryPolicy = retryPolicyFactory.GetRetryPolicy(RequestType.ManagedIdentityDefault); + + try + { + HttpResponse response = await _serviceBundle.HttpManager.SendRequestAsync( + request.ComputeUri(), + request.Headers, + httpContent, + request.Method, + _logger, + doNotThrow: true, + mtlsCertificate: null, + validateServerCertificate: null, + cancellationToken, + retryPolicy: retryPolicy) + .ConfigureAwait(false); + + LogResponseDetails(response); + + return EvaluateProbeResponse(response); + } + catch (Exception ex) + { + _logger.Error($"[Probe] Exception during probe: {ex.Message}"); + _logger.Error($"[Probe] Stack Trace: {ex.StackTrace}"); + return false; + } + } + + private void LogResponseDetails(HttpResponse response) + { + if (response == null) + { + _logger.Error("[Probe] No response received from the server."); + return; + } + + _logger.Info($"[Probe] Response Status Code: {response.StatusCode}"); + _logger.Verbose(() => $"[Probe] Response Headers: {string.Join(", ", response.HeadersAsDictionary)}"); + + if (response.Body != null) + { + _logger.Verbose(() => $"[Probe] Response Body: {response.Body}"); + } + } + + private bool EvaluateProbeResponse(HttpResponse response) + { + if (response == null) + { + _logger.Error("[Probe] No response received from the server."); + return false; + } + + _logger.Info($"[Probe] Evaluating response from credential endpoint. Status Code: {response.StatusCode}"); + + if (response.HeadersAsDictionary.TryGetValue("Server", out string serverHeader) && + serverHeader.StartsWith(ImdsHeader, StringComparison.OrdinalIgnoreCase)) + { + _logger.Info($"[Probe] Credential endpoint supported. Server: {serverHeader}"); + return true; + } + + _logger.Warning($"[Probe] Credential endpoint not supported. Status Code: {response.StatusCode}"); + return false; + } + } +} diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityClient.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityClient.cs index 80a45bb0da..84c8031999 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityClient.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityClient.cs @@ -21,29 +21,50 @@ internal class ManagedIdentityClient private const string LinuxHimdsFilePath = "/opt/azcmagent/bin/himds"; private readonly AbstractManagedIdentity _identitySource; - public ManagedIdentityClient(RequestContext requestContext) + // Cache for the managed identity source + private static ManagedIdentitySource? s_cachedManagedIdentitySource; + private static readonly SemaphoreSlim s_credentialSemaphore = new(1, 1); + + internal static async Task CreateAsync(RequestContext requestContext, CancellationToken cancellationToken = default) { - using (requestContext.Logger.LogMethodDuration()) + if (requestContext == null) { - _identitySource = SelectManagedIdentitySource(requestContext); + throw new ArgumentNullException(nameof(requestContext), "RequestContext cannot be null."); } + + requestContext.Logger?.Info("[ManagedIdentityClient] Creating ManagedIdentityClient."); + + AbstractManagedIdentity identitySource = await SelectManagedIdentitySourceAsync(requestContext, cancellationToken).ConfigureAwait(false); + + requestContext.Logger?.Info($"[ManagedIdentityClient] Managed identity source selected: {identitySource.GetType().Name}."); + + return new ManagedIdentityClient(identitySource); } - internal Task SendTokenRequestForManagedIdentityAsync(AcquireTokenForManagedIdentityParameters parameters, CancellationToken cancellationToken) + private ManagedIdentityClient(AbstractManagedIdentity identitySource) { - return _identitySource.AuthenticateAsync(parameters, cancellationToken); + _identitySource = identitySource ?? throw new ArgumentNullException(nameof(identitySource), "Identity source cannot be null."); } - // This method tries to create managed identity source for different sources, if none is created then defaults to IMDS. - private static AbstractManagedIdentity SelectManagedIdentitySource(RequestContext requestContext) + /// + /// This method tries to create managed identity source for different sources. + /// If none is created then defaults to IMDS. + /// + /// + /// + /// + private static async Task SelectManagedIdentitySourceAsync(RequestContext requestContext, CancellationToken cancellationToken = default) { - return GetManagedIdentitySource(requestContext.Logger) switch + ManagedIdentitySource source = await GetManagedIdentitySourceAsync(requestContext.ServiceBundle, cancellationToken).ConfigureAwait(false); + + return source switch { ManagedIdentitySource.ServiceFabric => ServiceFabricManagedIdentitySource.Create(requestContext), ManagedIdentitySource.AppService => AppServiceManagedIdentitySource.Create(requestContext), ManagedIdentitySource.MachineLearning => MachineLearningManagedIdentitySource.Create(requestContext), ManagedIdentitySource.CloudShell => CloudShellManagedIdentitySource.Create(requestContext), ManagedIdentitySource.AzureArc => AzureArcManagedIdentitySource.Create(requestContext), + ManagedIdentitySource.Credential => CredentialManagedIdentitySource.Create(requestContext), _ => new ImdsManagedIdentitySource(requestContext) }; } @@ -97,6 +118,134 @@ internal static ManagedIdentitySource GetManagedIdentitySource(ILoggerAdapter lo } } + /// + /// Compute the managed identity source based on the environment variables and the probe. + /// + /// + /// + /// + /// + public static async Task GetManagedIdentitySourceAsync( + IServiceBundle serviceBundle, + CancellationToken cancellationToken = default) + { + if (serviceBundle == null) + { + throw new ArgumentNullException(nameof(serviceBundle), "ServiceBundle is required to initialize the probe manager."); + } + + ILoggerAdapter logger = serviceBundle.ApplicationLogger; + + logger.Verbose(() => s_cachedManagedIdentitySource.HasValue + ? "[Managed Identity] Using cached managed identity source." + : "[Managed Identity] Computing managed identity source asynchronously."); + + if (s_cachedManagedIdentitySource.HasValue) + { + return s_cachedManagedIdentitySource.Value; + } + + // Use SemaphoreSlim to prevent multiple threads from computing at the same time + logger.Verbose(() => "[Managed Identity] Entering managed identity source semaphore."); + await s_credentialSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + logger.Verbose(() => "[Managed Identity] Entered managed identity source semaphore."); + + try + { + // Ensure another thread didn't set this while waiting on semaphore + if (s_cachedManagedIdentitySource.HasValue) + { + return s_cachedManagedIdentitySource.Value; + } + + // Initialize probe manager + var probeManager = new ImdsCredentialProbeManager(serviceBundle); + + // Compute the managed identity source + s_cachedManagedIdentitySource = await ComputeManagedIdentitySourceAsync( + probeManager, + serviceBundle.ApplicationLogger, + cancellationToken).ConfigureAwait(false); + + logger.Info($"[Managed Identity] Managed identity source determined: {s_cachedManagedIdentitySource.Value}."); + + return s_cachedManagedIdentitySource.Value; + } + finally + { + s_credentialSemaphore.Release(); + logger.Verbose(() => "[Managed Identity] Released managed identity source semaphore."); + } + } + + /// + /// Compute the managed identity source based on the environment variables and the probe. + /// + /// + /// + /// + /// + private static async Task ComputeManagedIdentitySourceAsync( + ImdsCredentialProbeManager imdsCredentialProbeManager, + ILoggerAdapter logger, + CancellationToken cancellationToken) + { + string identityEndpoint = EnvironmentVariables.IdentityEndpoint; + string identityHeader = EnvironmentVariables.IdentityHeader; + string identityServerThumbprint = EnvironmentVariables.IdentityServerThumbprint; + string msiEndpoint = EnvironmentVariables.MsiEndpoint; + string imdsEndpoint = EnvironmentVariables.ImdsEndpoint; + string msiSecretMachineLearning = EnvironmentVariables.MsiSecret; + + if (!string.IsNullOrEmpty(identityEndpoint) && !string.IsNullOrEmpty(identityHeader)) + { + if (!string.IsNullOrEmpty(identityServerThumbprint)) + { + return ManagedIdentitySource.ServiceFabric; + } + else + { + return ManagedIdentitySource.AppService; + } + } + else if (!string.IsNullOrEmpty(msiSecretMachineLearning) && !string.IsNullOrEmpty(msiEndpoint)) + { + return ManagedIdentitySource.MachineLearning; + } + else if (!string.IsNullOrEmpty(msiEndpoint)) + { + return ManagedIdentitySource.CloudShell; + } + else if (ValidateAzureArcEnvironment(identityEndpoint, imdsEndpoint, logger)) + { + return ManagedIdentitySource.AzureArc; + } + else + { + logger?.Info("[Managed Identity] Probing for credential endpoint."); + bool isSuccess = await imdsCredentialProbeManager.ExecuteProbeAsync(cancellationToken).ConfigureAwait(false); + + if (isSuccess) + { + logger?.Info("[Managed Identity] Credential endpoint detected."); + return ManagedIdentitySource.Credential; + } + else + { + logger?.Verbose(() => "[Managed Identity] Defaulting to IMDS as credential endpoint not detected."); + return ManagedIdentitySource.DefaultToImds; + } + } + } + + /// + /// Resets the cached managed identity source. Used only for testing purposes. + /// + internal static void ResetManagedIdentitySourceCache() + { + s_cachedManagedIdentitySource = null; + } + // Method to return true if a file exists and is not empty to validate the Azure arc environment. private static bool ValidateAzureArcEnvironment(string identityEndpoint, string imdsEndpoint, ILoggerAdapter logger) { @@ -123,5 +272,10 @@ private static bool ValidateAzureArcEnvironment(string identityEndpoint, string logger?.Verbose(() => "[Managed Identity] Azure Arc managed identity is not available."); return false; } + + internal Task SendTokenRequestForManagedIdentityAsync(AcquireTokenForManagedIdentityParameters parameters, CancellationToken cancellationToken) + { + return _identitySource.AuthenticateAsync(parameters, cancellationToken); + } } } diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityCredentialResponse.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityCredentialResponse.cs new file mode 100644 index 0000000000..a5767a90aa --- /dev/null +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityCredentialResponse.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +#if SUPPORTS_SYSTEM_TEXT_JSON +using Microsoft.Identity.Client.Platforms.net; +using JsonProperty = System.Text.Json.Serialization.JsonPropertyNameAttribute; +#else +using Microsoft.Identity.Json; +#endif + +namespace Microsoft.Identity.Client.ManagedIdentity +{ + [JsonObject] + [Preserve(AllMembers = true)] + internal class ManagedIdentityCredentialResponse + { + [JsonProperty("client_id")] + public string ClientId { get; set; } + + [JsonProperty("client_credential")] + public byte[] CertificateForMtls { get; set; } + + [JsonProperty("regional_token_url")] + public string RegionalTokenUrl { get; set; } + + [JsonProperty("tenant_id")] + public string TenantId { get; set; } + } +} diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityMetadataResponse.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityMetadataResponse.cs new file mode 100644 index 0000000000..b5a4d75e55 --- /dev/null +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityMetadataResponse.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +#if SUPPORTS_SYSTEM_TEXT_JSON +using Microsoft.Identity.Client.Platforms.net; +using JsonProperty = System.Text.Json.Serialization.JsonPropertyNameAttribute; +#else +using Microsoft.Identity.Json; +#endif + +namespace Microsoft.Identity.Client.ManagedIdentity +{ + [JsonObject] + [Preserve(AllMembers = true)] + internal class ManagedIdentityMetadataResponse + { + [JsonProperty("client_id")] + public string ClientId { get; set; } + + [JsonProperty("client_credential")] + public byte[] CertificateForMtls { get; set; } + + [JsonProperty("regional_token_url")] + public string RegionalTokenUrl { get; set; } + + [JsonProperty("tenant_id")] + public string TenantId { get; set; } + } +} diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityRequest.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityRequest.cs index c5b9af2b73..6b828c018a 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityRequest.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityRequest.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using System.Net.Http; +using System.Text; using Microsoft.Identity.Client.ApiConfig.Parameters; using Microsoft.Identity.Client.Core; using Microsoft.Identity.Client.OAuth2; @@ -23,6 +24,7 @@ internal class ManagedIdentityRequest public IDictionary BodyParameters { get; } public IDictionary QueryParameters { get; } + public string Content { get; set; } public RequestType RequestType { get; set; } @@ -64,5 +66,21 @@ internal void AddClaimsAndCapabilities( logger.Info("[Managed Identity] Passing SHA-256 of the 'revoked' token to Managed Identity endpoint."); } } + + public HttpContent CreateHttpContent() + { + if (!string.IsNullOrEmpty(Content)) + { + return new StringContent(Content, Encoding.UTF8, "application/json"); + } + + if (BodyParameters.Count > 0) + { + var formData = new FormUrlEncodedContent(BodyParameters); + return formData; + } + + return null; // No body content + } } } diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentitySource.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentitySource.cs index 69e3471bdf..6263aa6402 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentitySource.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentitySource.cs @@ -53,6 +53,12 @@ public enum ManagedIdentitySource /// /// The source to acquire token for managed identity is Machine Learning Service. /// - MachineLearning + MachineLearning, + + /// + /// Indicates that the source is credential endpoint based on the probe. + /// This is used to detect the new managed identity credential source. + /// + Credential } } diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentityApplication.cs b/src/client/Microsoft.Identity.Client/ManagedIdentityApplication.cs index eded64dc91..23ecfb11f9 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentityApplication.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentityApplication.cs @@ -8,7 +8,9 @@ using System.Threading.Tasks; using Microsoft.Identity.Client.ApiConfig.Executors; using Microsoft.Identity.Client.ApiConfig.Parameters; +using Microsoft.Identity.Client.AppConfig; using Microsoft.Identity.Client.Core; +using Microsoft.Identity.Client.Http.Retry; using Microsoft.Identity.Client.Internal; using Microsoft.Identity.Client.Internal.Requests; using Microsoft.Identity.Client.ManagedIdentity; @@ -63,5 +65,17 @@ public static ManagedIdentitySource GetManagedIdentitySource() { return ManagedIdentityClient.GetManagedIdentitySource(); } + + /// + /// Detects and returns the managed identity source available on the environment asynchronously. + /// + /// A task representing the asynchronous operation. The task result contains the managed identity source detected on the environment if any. + public static async Task GetManagedIdentitySourceAsync(CancellationToken cancellationToken = default) + { + var config = new ApplicationConfiguration(MsalClientType.ManagedIdentityClient); + config.RetryPolicyFactory = new RetryPolicyFactory(); + var serviceBundle = new ServiceBundle(config); + return await ManagedIdentityClient.GetManagedIdentitySourceAsync(serviceBundle, cancellationToken).ConfigureAwait(false); + } } } diff --git a/src/client/Microsoft.Identity.Client/Platforms/net/MsalJsonSerializerContext.cs b/src/client/Microsoft.Identity.Client/Platforms/net/MsalJsonSerializerContext.cs index d36f036282..7e254031ff 100644 --- a/src/client/Microsoft.Identity.Client/Platforms/net/MsalJsonSerializerContext.cs +++ b/src/client/Microsoft.Identity.Client/Platforms/net/MsalJsonSerializerContext.cs @@ -40,6 +40,8 @@ namespace Microsoft.Identity.Client.Platforms.net [JsonSerializable(typeof(ManagedIdentityResponse))] [JsonSerializable(typeof(ManagedIdentityErrorResponse))] [JsonSerializable(typeof(OidcMetadata))] + [JsonSerializable(typeof(ManagedIdentityCredentialResponse))] + [JsonSerializable(typeof(ManagedIdentityMetadataResponse))] [JsonSourceGenerationOptions] internal partial class MsalJsonSerializerContext : JsonSerializerContext { diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt index e69de29bb2..f6c9a411ea 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt @@ -0,0 +1,2 @@ +Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource.Credential = 8 -> Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource +static Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task \ No newline at end of file diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt index e69de29bb2..9f2731d05f 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt @@ -0,0 +1,2 @@ +Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource.Credential = 8 -> Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource +static Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt index e69de29bb2..9f2731d05f 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt @@ -0,0 +1,2 @@ +Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource.Credential = 8 -> Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource +static Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt index e69de29bb2..9f2731d05f 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt @@ -0,0 +1,2 @@ +Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource.Credential = 8 -> Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource +static Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt index e69de29bb2..9f2731d05f 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt @@ -0,0 +1,2 @@ +Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource.Credential = 8 -> Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource +static Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task diff --git a/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt index e69de29bb2..9f2731d05f 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt @@ -0,0 +1,2 @@ +Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource.Credential = 8 -> Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource +static Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task diff --git a/tests/devapps/Managed Identity apps/ManagedIdentityAppVM/Program.cs b/tests/devapps/Managed Identity apps/ManagedIdentityAppVM/Program.cs index 427b7ca149..792efeba16 100644 --- a/tests/devapps/Managed Identity apps/ManagedIdentityAppVM/Program.cs +++ b/tests/devapps/Managed Identity apps/ManagedIdentityAppVM/Program.cs @@ -7,6 +7,11 @@ IIdentityLogger identityLogger = new IdentityLogger(); +//Get Managed Identity Source +Console.WriteLine("Managed Identity Source is {0}", + await ManagedIdentityApplication.GetManagedIdentitySourceAsync() + .ConfigureAwait(false)); + IManagedIdentityApplication mi = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) .WithLogging(identityLogger, true) .Build();