Skip to content

[DRAFT] MSI v2 e2e #5438

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

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
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 @@ -40,12 +40,30 @@ public async Task<AuthenticationResult> 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);
}


Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// 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).
/// </summary>
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)
{
}

/// <summary>
/// Main entry point for MSI V2 token acquisition.
/// </summary>
protected override async Task<AuthenticationResult> 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
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this will be cred-api-version=2.0

// - 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
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this will be cred-api-version=2.0

// - Body: { "csr": "<Base64 CSR>", "attestation_token": "<jwt>"? }
// - 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);
}
}

/// <summary>
/// Calls getPlatformMetadata endpoint.
/// TODO: Implement real HTTP call.
/// </summary>
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
private async Task<ManagedIdentityMetadataResponse> 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
};
}

/// <summary>
/// Calls /issuecredential endpoint with CSR (+ attestation token if applicable).
/// TODO: Implement CSR generation, attestation handling, and HTTP call.
/// </summary>
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
private async Task<ManagedIdentityCredentialResponse> 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<byte>(),
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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Legacy (non-credential-based) MI flow using ManagedIdentityClient.SendTokenRequestForManagedIdentityAsync.
/// </summary>
internal sealed class LegacyManagedIdentityAuthRequest : ManagedIdentityAuthRequestBase
{
public LegacyManagedIdentityAuthRequest(
IServiceBundle serviceBundle,
AuthenticationRequestParameters authenticationRequestParameters,
AcquireTokenForManagedIdentityParameters managedIdentityParameters)
: base(serviceBundle, authenticationRequestParameters, managedIdentityParameters)
{
}

protected override async Task<AuthenticationResult> 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<string, string>? GetCcsHeader(IDictionary<string, string> _) => null;
}
}
Loading
Loading