Skip to content
Bogdan Gavril edited this page Nov 3, 2023 · 13 revisions

Note: this is a design document and the functionality is not in any SDK.

Which SDKs support the new MSI features ?

SDK MSI CAE support MSI TB support FIC CAE FIC TB Ease of use
MSAL private preview N ? N Hard
ID.Web in progress work item N in progress work item N Easy
Azure.Identity N N N N ?

When to use which SDK?

We recommend using higher level SDKs where possible:

  • Microsoft.Identity.Web
  • Azure.Identity

Applications needing full control (e.g. other SDKs) can use MSAL directly.

How do higher level APIs help?

Microsoft.Identity.Web and Azure.Identity (?) control both:

  1. Getting tokens
  2. Calling protected APIs

MSAL only controls (1) Getting Tokens. CAE and TB require auth logic to cover (2) Calling Protected APIs, such as interpreting 401 responses, parsing WWW-Authenticate headers, opening MTLS connections etc.

Id.Web Experience

It is helpful to review the existing sample for performing client_credentials:

Vanilla MSI

Configuration

 "DownstreamApis": {
        "allUsers": {
            "BaseUrl": "https://graph.microsoft.com/v1.0",
            "RelativePath": "/users",
            "Scopes": [ "https://graph.microsoft.com/.default" ],
            "AcquireTokenOptions" : {
               "UseManagedIdentity": {
                    // No property = system-assigned
                    // For user-assigned set ClientID, ObjectId or HostResource
               }
             }
         }
       }

Code

[!NOTE]
The code is identical to client_credentials, only the configuration differs

// Get the Token acquirer factory instance. By default it reads an appsettings.json
// file if it exists in the same folder as the app (make sure that the 
// "Copy to Output Directory" property of the appsettings.json file is "Copy if newer").
var tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance();

// Create a downstream API service named 'MyApi' which comes loaded with several
// utility methods to make HTTP calls to the DownstreamApi configurations found
// in the "MyWebApi" section of your appsettings.json file.
tokenAcquirerFactory.Services.AddDownstreamApi("MyApi",
    tokenAcquirerFactory.Configuration.GetSection("MyWebApi"));
var sp = tokenAcquirerFactory.Build();

// Extract the downstream API service from the 'tokenAcquirerFactory' service provider.
var api = sp.GetRequiredService<IDownstreamApi>();

// You can use the API service to make direct HTTP calls to your API. Token
// acquisition is handled automatically based on the configurations in your
// appsettings.json file.
var result = await api.GetForAppAsync<IEnumerable<TodoItem>>("MyApi");
Console.WriteLine($"result = {result?.Count()}");
FIC

Config

{

 "AzureAd": {

   "Instance": "https://login.microsoftonline.com/",
   "TenantId": "7f58f645-c190-4ce5-9de4-e2b7acd2a6ab",
   "ClientId": "56c9a633-236e-45ee-9af1-a53d9811fbd6",
   "ClientCredentials": [
    {
      "SourceType": "SignedAssertionFromManagedIdentity",
      "ManagedIdentityClientId": "02c0b640-8e3d-405e-999d-4781f2f0438a" # ignore for System Assigned 

    }]

},

[!NOTE]
The code is identical to the client_credentials code / MSI code above. It's all config driven.

// Get the Token acquirer factory instance. By default it reads an appsettings.json
// file if it exists in the same folder as the app (make sure that the 
// "Copy to Output Directory" property of the appsettings.json file is "Copy if newer").
var tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance();

// Create a downstream API service named 'MyApi' which comes loaded with several
// utility methods to make HTTP calls to the DownstreamApi configurations found
// in the "MyWebApi" section of your appsettings.json file.
tokenAcquirerFactory.Services.AddDownstreamApi("MyApi",
    tokenAcquirerFactory.Configuration.GetSection("MyWebApi"));
var sp = tokenAcquirerFactory.Build();

// Extract the downstream API service from the 'tokenAcquirerFactory' service provider.
var api = sp.GetRequiredService<IDownstreamApi>();

// You can use the API service to make direct HTTP calls to your API. Token
// acquisition is handled automatically based on the configurations in your
// appsettings.json file.
var result = await api.GetForAppAsync<IEnumerable<TodoItem>>("MyApi");
Console.WriteLine($"result = {result?.Count()}");

MSAL experience

Vanilla MSI
public async AuthenticationResult GetMsiTokenAsync(string resource, string claims )
{
    ManagedIdentityApplication mia = ManagedIdentityApplication
                                         .Create(ManagedIdentityType.SystemAssigned)
                                         .WithClientCapabilities(new[] {"cp1"} ); // NEW: claims support for MSI

    // if claims are not null, cache is bypassed and a new token is acquired.
    var authResult = await mia.AcquireToken(resource)
                              // NEW: claims support
                              .WithClaims(claims) 
                              // NEW: MTLS POP support (if EITHER client or resource don't support MTLS, Bearer token will be issued)
                              .TryMtlsProofOfPosession() 
                              .ExecuteAsync();

    // App may choose to monitor the following:

    // Claim challenges are only supported when new IMDS SLC endpoint is deployed (TBD on key provisioning)
    Metrics.Log("cae_support", mia.IsClaimsChallengeSupported());

    // MTLS is supported by the client if IMDS SLC endpoint is deployed and a KeyGuard key is found (TBD on key provisioning)
    Metrics.Log("mtls_client_support", mia.IsMtlsSupportedByClient());

    // For POP token to be issued, both client and RP must provide support 
    Metrics.Log("token_type", authResult.TokenType); // Bearer or MTLS-POP (?)

    return authResult;
}

public async HttpResponse CallProtectedApiAsync(AuthenticationResult ar, HttpRequest request)
{
    // 1. Add auth header which can be "Bearer eyXYZ" or "MTLS? eyXYZ"
    request.Headers.Authorization.Add(ar.GetAuthorizationHeader());

    // 2. Create HTTP client with client MTLS cert to talk to resource
    var httpClient = GetHttpClient(ar.MtlsCertificate);    
    HttpResponse response = await s_httpClient.SendAsync(request);

    // 3. If response is 401, perform the CAE dance
    if (response.StatusCode == 401)
    { 
        // parse the WWWAuthenticate header and look for claim challenge
        string claims = WWWAuthenticateHeaders.Parse(response);
        if (claims != null) 
        {
            AuthenticationResult arWithClaims = GetMsiTokenAsync(claims);

            // TODO: break the recursion loop after 1-2 attempts
            return CallProtectedApi(arWithClaims);  
        }
    }
 
    return response;
    
}

private static ConcurrentDictionary<string, HttpClient> s_httpClients = 
   new ConcurrentDictionary<string, HttpClient>();

public static HttpClient GetHttpClient(X509Certificate2 certificate)
{
   if (certificate=null) { 
      return s_httpClients.GetOrAdd("non_mtls", (cert) => return new HttpClient());
   }
   
    return s_httpClients.GetOrAdd(cert.Thumbprint, (cert) => return new HttpClient() { ClientCertificate = cert);
   
}

// ORCHESTRATION LOGIC

var ar = await GetMsiTokenAsync("https://arm.management.com/.default", claims: null);
HttpRequest request = new HttpRequest("https://arm.management.com/list_stuff");
HttpResponse response = await CallProtectedApiAsync(ar, request);

Open questions

  1. What is the Authenticate header scheme for mtls? I don't think it can be POP as it will conflict with SHR POP.
  2. Will there be a separate MTLS endpoint on each resource, similar to ESTS endpoint?

FIC + CAE + TB

IN progress...

Getting started with MSAL.NET

Acquiring tokens

Web Apps / Web APIs / daemon apps

Desktop/Mobile apps

Advanced topics

FAQ

Other resources

Clone this wiki locally