Skip to content
jennyf19 edited this page May 9, 2023 · 42 revisions

Microsoft.Identity.Web v2.1.0

One API for all .NET platforms and all confidential client scenarios

Microsoft.Identity.Web 1.x was only handling web apps and protected web APIs with Azure AD, or Azure AD B2C, running on ASP.NET Core, calling or not downstream APIs. Microsoft.Identity.Web 2.0.0 now supports more scenarios (daemon apps) and more platforms (ASP.NET OWIN, .NET Framework, or .NET Core in addition to ASP.NET Core). You can now use the same code, and the same configuration code to call (downstream) web APIs:

  • If you want to call Microsoft graph, get a GraphServiceClient
  • If you want to use an Azure SDK, get a TokenAcquirerTokenCredential or a TokenAcquirerAppTokenCredential.
  • If you want to call a downstream web API other than Microsoft Graph and don't use an SDK provided by this service, use IDownstreamApi. Microsoft Identity Web takes care of the details about authentication tokens and protocols.
  • If you need to use a specific SDK, or can't use IDownstreamApi, use:
    • IAuthorizationHeaderProvider that computes the authorization header to call a downstream API (any protocol)
    • or ITokenAcquirer if your SDK requires a token. In that case, the SDK probably only supports the bearer protocol.

You can use one configuration for all the platforms, in JSON, and inspired from the ASP.NET Core configuration.

Common configuration

The following sample configuration file is understood by ASP.NET Core, but also OWIN web apps and web APIs using Microsoft.Identity.Web, and daemon applications. This simplifies a lot the code.

{
 "AzureAd": {
  "Instance": "https://login.microsoftonline.com",
  "TenantId": "common",
  "ClientId": "GUID", // from app registration
  "EnablePiiLogging": false, // set to true to enable PII

  // Computed by Id.Web, but overridable (and also to support other IdPs than Azure AD)
  // "Authority": "https://login.microsoftonline.com/common/v2.0",

  // If the app calls downstream APIs
  "ClientCredentials": [
   {
    // Typically used with Federation identity with MSI
    "SourceType": "SignedAssertionFromManagedIdentity",
    "ManagedIdentityClientId": "optional ClientID (GUID) for a user assigned Managed identity"
   },
   {
    // Typically used with AKS (signed assertion from Identity federation for Kubernetes
    // for instance, if your app runs on AKS)
    "SourceType": "SignedAssertionFilePath",
    // If SignedAssertionFileDiskPath is not provided (below), uses the 
    // content of the AZURE_FEDERATED_TOKEN_FILE environment variable.
    // "SignedAssertionFileDiskPath": "path to file on disc.",
   },
   {
    "SourceType": "KeyVault",
    "KeyVaultUrl": "https://webappsapistests.vault.azure.net",
    "KeyVaultCertificateName": "Self-Signed-5-5-22"
   },
   {
    "SourceType": "ClientSecret",
    "ClientSecret": "***"
   }
  ],
  "SendX5C": false,

  // If the app is a web API
  "TokenDecryptionCredentials": [
   {
    "SourceType": "KeyVault",
    "KeyVaultUrl": "https://webappsapistests.vault.azure.net",
    "KeyVaultCertificateName": "Self-Signed-5-5-22"
   }
  ],
  "AllowWebApiToBeAuthorizedByACL": false,

  // If the app is a web app:
  "ResetPasswordPath": "/MicrosoftIdentity/Account/ResetPassword",
  "ErrorPath": "/MicrosoftIdentity/Account/Error",
  "WithSpaAuthCode": false,

  "Audience": "Audience of your web API",
  "Audiences": [],
  "AzureRegion" : "TryAutoDetect",

  // Azure AD B2C Specific
  "Domain": "microsoft.com",
  "EditProfilePolicyId": null,
  "SignUpSignInPolicyId": null,
  "ResetPasswordPolicyId": null,
  "DefaultUserFlow": null
 },

 // Downstream APIs
 "DownstreamApis": [
  {
   "GraphBeta": {
    "BaseUrl": "https://graph.microsoft.com/beta",
    "Scopes": ["user.read"]
   }
  }
 ],


 "Logging": {

 }
}

ASP.NET Core

For ASP.NET Core, see web apps and web APIs

OWIN

IAppBuilder now has extension methods:

  • AddMicrosoftIdentityWebApp (which also supports guest accounts)
  • AddMicrosoftIdentityWebApi These methods take as a parameter an instance of OwinTokenAcquirerFactory that you get calling OwinTokenAcquirerFactory.GetDefaultInstance()

From the Controllers, it's possible to get an instance of GraphServiceClient, IDownstreamApi, and IAuthorizationHeaderProvider through using methods

OWIN Web app

The full code is available from tests/DevApps/aspnet-mvc/OwinWebApp

The Startup.auth.cs can now be:

using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Owin;
using Microsoft.Identity.Web;
using Microsoft.Identity.Web.TokenCacheProviders.InMemory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Identity.Client;
using Microsoft.Identity.Abstractions;
using Microsoft.Identity.Web.OWIN;
using System.Web.Services.Description;

namespace OwinWebApp
{
    public partial class Startup
    {
        public void ConfigureAuth(IAppBuilder app)
        {
            app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
            app.UseCookieAuthentication(new CookieAuthenticationOptions());

            OwinTokenAcquirerFactory factory = TokenAcquirerFactory.GetDefaultInstance<OwinTokenAcquirerFactory>();

            app.AddMicrosoftIdentityWebApp(factory);
            factory.Services
                .Configure<ConfidentialClientApplicationOptions>(options => { options.RedirectUri = "https://localhost:44386/"; })
                .AddMicrosoftGraph()
                .AddDownstreamApi("DownstreamAPI1", factory.Configuration.GetSection("DownstreamAPI"))
                .AddInMemoryTokenCaches();
            factory.Build();
        }
    }
}

OWIN Web API

The full code is available from tests\DevApps\aspnet-mvc\OwinWebApi

Here is the Startup.auth.cs file:

using Microsoft.Identity.Web;
using Microsoft.Identity.Web.OWIN;
using Owin;

namespace OwinWebApi
{
    public partial class Startup
    {
        // For more information on configuring authentication, please visit https://go.microsoft.com/fwlink/?LinkId=301864
        public void ConfigureAuth(IAppBuilder app)
        {
            OwinTokenAcquirerFactory factory = TokenAcquirerFactory.GetDefaultInstance<OwinTokenAcquirerFactory>();
            app.AddMicrosoftIdentityWebApi(factory);
            factory.Services
                .AddMicrosoftGraph()
                .AddDownstreamApi("DownstreamAPI", factory.Configuration.GetSection("DownstreamAPI"));
            factory.Build();
        }
    }
}

OWIN Controllers

Microsoft.Identity.Web.OWIN proposes two extension classes for System.Web.Http.ApiController and System.Web.Mvc.ControllerBase to get the GraphServiceClient (if you added it to the OwinTokenAcquirerFactory.Services), IDownstreamApi (same thing), and IAuthorizationHeaderProvider.

using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Security.Claims;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http;
using Microsoft.Graph;
using Microsoft.Identity.Abstractions;
using Microsoft.Identity.Web;
using Todo = OwinWebApi.Models.Todo;

namespace OwinWebApi.Controllers
{
    [Authorize]
    public class TodoListController : ApiController
    {
        private static readonly Dictionary<int, Todo> todoStore = new Dictionary<int, Todo>();

        // GET api/values
        public async Task<IEnumerable<Todo>> Get()
        {
            // EITHER - Example calling Graph
            // graphServiceClient won't be null if you added
            // services.AddMicrosoftGraph() in the Startup.auth.cs
            GraphServiceClient graphServiceClient = this.GetGraphServiceClient();
            var me = await graphServiceClient.Me.Request().GetAsync();

            // OR - Example calling a downstream directly with the IDownstreamApi helper (uses the
            // authorization header provider, encapsulates MSAL.NET)
            // downstreamApi won't be null if you added services.AddMicrosoftGraph()
            // in the Startup.auth.cs
            IDownstreamApi downstreamApi = this.GetDownstreamApi();
            var result = await downstreamApi.CallApiForUserAsync("DownstreamAPI");

            // OR - Get an authorization header (uses the token acquirer)
            IAuthorizationHeaderProvider authorizationHeaderProvider =
                    this.GetAuthorizationHeaderProvider();
            string authorizationHeader = await authorizationHeaderProvider.CreateAuthorizationHeaderForUserAsync(
                    new[] { "user.read" },
                    new AuthorizationHeaderProviderOptions
                    {
                        BaseUrl = "https://graph.microsoft.com/v1.0/me"
                    });

            HttpClient client = new HttpClient();
            client.DefaultRequestHeaders.Add("Authorization", authorizationHeader);
            HttpResponseMessage response = await client.GetAsync("https://graph.microsoft.com/v1.0/me");

            // OR - Get a token if an SDK needs it (uses MSAL.NET)
            ITokenAcquirerFactory tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance();
            ITokenAcquirer acquirer = tokenAcquirerFactory.GetTokenAcquirer()!;
            AcquireTokenResult tokenResult = await acquirer.GetTokenForUserAsync(
               new[] { "user.read" });
            string accessToken = tokenResult.AccessToken!;

            // return the item
            string? owner = (HttpContext.Current.User as ClaimsPrincipal)?.GetDisplayName();
            return todoStore.Values.Where(x => x.Owner == owner);

        }
}

Daemon app

The full code is available from tests/DevApps/daemon-app/daemon-console-calling-downstreamApi

Project file:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFrameworks>net7.0</TargetFrameworks>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <Compile Include="WeatherForecast.cs" />
  </ItemGroup>

  <PackageReference Include="Microsoft.Identity.Web.DownstreamApi" Version="2.2.1" />

  <ItemGroup>
    <None Update="appsettings.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
  </ItemGroup>

</Project>

Note that the appsettings.json needs to be copied to the output directory!

Appsettings.json

{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "TenantId": "msidentitysamplestesting.onmicrosoft.com",
    "ClientId": "6af093f3-b445-4b7a-beae-046864468ad6",
    "ClientCredentials": [
      {
        "SourceType": "KeyVault",
        "KeyVaultUrl": "https://webappsapistests.vault.azure.net",
        "KeyVaultCertificateName": "Self-Signed-5-5-22"
      }
    ]
  },

  "MyWebApi": {
    "BaseUrl": "https://localhost:7060/",
    "RelativePath": "/WeatherForecast",
    "RequestAppToken": true,
    "Scopes": [ "api://a4c2469b-cf84-4145-8f5f-cb7bacf814bc/.default" ]
  }
}

Program.cs

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Identity.Abstractions;
using Microsoft.Identity.Web;
using WebApi;

var tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance();
tokenAcquirerFactory.Services.AddDownstreamApi("MyApi", 
    tokenAcquirerFactory.Configuration.GetSection("MyWebApi"));
var sp = tokenAcquirerFactory.Build();

var api = sp.GetRequiredService<IDownstreamApi>();
var result = await api.GetForAppAsync<IEnumerable<WeatherForecast>>("MyApi");
Console.WriteLine($"result = {result?.Count()}");

A similar sample is available to call Microsoft Graph, see tests/DevApps/daemon-app/daemon-console-calling-msgraph

You now control everything when calling MSGraph.

When you use the Graph, you can now augment the request with WithAuthenticationOptions, which provides you with all the control you need. For example, if you want to call Microsoft Graph using the Pop protocol, you would write:

       var users = await graphServiceClient.Users
                .Request()
                .WithAppOnly()
                .WithAuthenticationOptions(options => options.ProtocolScheme = "Pop")
                .GetAsync();

image

Simplified configuration.

In the past, Microsoft.Identity.Web was exposing the MicrosoftIdentityOptions, but this was:

  • only on ASP.NET Core
  • and it inherited from OpenIdConnectOptions, and therefore not suitable for all the platforms and scenarios.

In v2.0, we separated the interfaces from the implmentation. Microsoft.Identity.Web provides the implementation of a new library named Microsoft.Identity.Abstractions, which proposes MicrosoftApplicationAuthenticationOptions (specific to AAD), inheriting from AppplicationAuthenticationOptions (standard compliant), and makes these available in OWIN and SDK/daemon scenarios as well as in ASP.NET Core. To avoid breaking you, the default configuration is still MicrosoftIdentityOptions in ASP.NET Core though.

Performance is greatly improved

The Microsoft.Identity.Web team has worked on performance improvement leading to between x3 and x6 for an end to end call to web API calling graph.

Credentials are generalizing certificates.

In Microsoft.Identity.Web 1.x, you could specify the ClientCertificates, in many different ways.

From Microsoft.Identity.Web 2.x, you can now specify ClientCredentials. In addition to certificates, (and client secrets), you'll be able to specify signed assertions leveraging Workload identity federation. For instance, for instance when the app is hosted in Kubernetes, you can leverage the identity federation for Kubernetes based on Azure workload identity for kubernetes. For details see Kubernetes workload identity and access. You will also setup identity federation wit managed identity.

  // If the app calls downstream APIs
  "ClientCredentials": [
   // When running in the container with Azure workload identity for kubernetes.
   {
    "SourceType": "SignedAssertionFilePath",
    "SignedAssertionFileDiskPath": "optional path to signed assertion"
   },
   {
    // Workload identity federation with MSI
    "SourceType": "SignedAssertionFromManagedIdentity",
    "ManagedIdentityClientId": "optional GUID of user assigned Managed identity"
   },
   // Fallback when running locally to debug
   {
    "SourceType": "KeyVault",
    "KeyVaultUrl": "https://webappsapistests.vault.azure.net",
    "KeyVaultCertificateName": "Self-Signed-5-5-22"
   }
  ],

Microsoft.Identity.Web will try the credentials in the order you specify them, which means you can try things out locally, or deployed, seamlessly.

More credentials will be available as Azure AD supports them.

You can also now provide the UserAssignedManagedIdentityClientId per credential, instead of providing it statically for the application.

Certificate loader now enables you to add your own loaders.

If you have specific ways to load your certificates, not supported by Microsoft.Identity.Web, you can add your own credential loader to the CredentialLoaders property of DefaultCertificateLoader. For this, implement the ICredentialLoader interface, and assign a value other than the enumerations in CredentialSource.

 /// <summary>
    /// Interface to implement loading of credentials.
    /// </summary>
    public interface ICredentialLoader
    {
        /// <summary>
        /// Load the credential from the description, if needed.
        /// </summary>
        /// <param name="credentialDescription">Description of the credential.</param>
        void LoadIfNeeded(CredentialDescription credentialDescription);

        /// <summary>
        /// Loadable CredentialSource.
        /// </summary>
        CredentialSource CredentialSource { get; }
    }

IDownstreamApi

Microsoft.Identity.Web 1.x proposed IDownstreamWebApi, which grew organically, and had a number of feature requests that would provoke breaking changes. We kept IDownstreamWebApi, but propose a new interface IDownstreamApi, which takes into account your feedback

  var httpResponseMessage = await downstreamApi.CallApiForAppAsync("GraphBeta", options => 
  {
   options.BaseUrl = "https://myapiUrl";
   options.Scopes = new string[] { "api://myApi/scope" };
  }).ConfigureAwait(false);

There are also all kind of generic overloads that enable you to pass-in input data and get back output.

See tests/DevApps/WebAppCallsWebApiCallsGraph/Client to understand how to use downstream API.

Also read docs/blog-posts/downstreamwebapi-to-downstreamapi.md to learn how to migrate your code using IDownstreamRestApi to IDownstreamApi

Why is it a v2?

The Microsoft.Identity.Web team has taken a lot of care to avoid breaking changes, and for most of you, moving from Microsoft.Identity.Web 1.x to Microsoft.Identity.Web 2.x shouldn't be difficult: just updating your NuGet reference. You will get warnings advising you to move from IDownstreamWebApi to IDownstreamApi. There are a few things to consider:

Breaking changes

Breaking changes:

Microsoft.Identity.Web 2.x has the following breaking changes. Most of you should not be affected.

  • The Microsoft.Identity.Web.MicrosoftGraph NuGet package used to reference the Microsoft.Identity.Web NuGet package. Now that token acquisition is available on all .NET platforms, Microsoft.Identity.Web.MicrosoftGraph references Microsoft.Identity.Web.TokenAcquisition. If you are using .NET Core 3.1, or .NET >5, and are using the attributes in the controllers (RequiredScopeAttribute, AuthorizeForScopeAttribute), you will need to reference Microsoft.Identity.Web directly.

  • Microsoft.Identity.Web does no longer support the overrides of ConfigureOptions<OpenIdConnectOptions>(delegate) and ConfigureOptions<JwtBearerOptions>(delegate) that don't explicit an authentication scheme. This is to align with ASP.NET Core which always required an authentication scheme. if you were not already doing the right thing with the authentication schemes (that is when you configure options if you were not already specifying an authentication scheme), you will need to change your code:

    services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
            .AddMicrosoftWebApp(Configuration);
    services.ConfigureOptions<OpenIdConnectOptions>(options => /* do something */);

    need to be changed into:

    services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
            .AddMicrosoftWebApp(Configuration);
    
    services.ConfigureOptions<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme,
                                                    options => /* do something */);

    and

     services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
             .AddMicrosoftWebApi(Configuration);
    
    services.ConfigureOptions<JwtBearerDefaults>(options => /* do something */);

    need to be changed into:

     services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
             .AddMicrosoftWebApi(Configuration);
    
    services.ConfigureOptions<JwtBearerDefaults>(JwtBearerDefaults.AuthenticationScheme,
                                                    options => /* do something */);

    If you were already using multiple authentication schemes, you probably were already specifying the right authentication scheme and won't be affected.

  • Microsoft.Identity.Web.CertificatelessOptions was renamed from ManagedIdentityObjectId to ManagedIdentityClientId (request Chris Brooks). This is unlikely to affect you though as this was an experimental feature.

  • Microsoft.Identity.Web.ClaimsPrincipalFactory.FromTenantIdAndObjectId now produces a ClaimsPrincipal from object id and tenant ID, whereas in the past it was using the Home tenant Id and home object ID. There is a new method Microsoft.Identity.Web.ClaimsPrincipalFactory.FromHomeTenantIdAndHomeObjectId which is providing the previous behavior.

  • the methods of MsalDistributedTokenCacheAdapter that used to take a byte[] now take a byte[]. If you had provided your class derived from MsalDistributedTokenCacheAdapter, you'll need to fix the signature (this is to follow changes of signatures in memory and distributed caches in .NET 7). If you didn't provide your own class, you won't be affected.

Binary breaking change

The following are binary breaking changes. If you rebuild your code things will work the same transparently.

  • Microsoft.Identity.Web.CertificateDescription now derives from Microsoft.Identity.Abstractions.CredentialDescription and most of its properties moved into the base class.

  • TokenAcquisitionOptions now inherits from Microsoft.Identity.Abstractions.AcquireTokenOptions (which is now recommended)

  • The code is distributed in different assemblies

    image

Non-breaking changes

  • Microsoft.Identity.Web now supports net6.0, net7.0, net462, net472, netstandard2.0, netcoreapp3.1. It no longer supports net5.0 which is end of life, not not a LTS version.
  • Microsoft.Identity.Web.OWIN bring supports to legacy ASP.NET OWIN web apps and web APIs, hiding most of the authentication complexity
  • New AzureIdentityForKubernetesClientAssertion class in Microsoft.Identity.Web.Certificateless brings supports for Workload identity federation for AKS
  • New extension method WithAuthenticationOptions on Microsoft.Graph.IBaseRequest enables you to customize all the authentication properties related to getting a token for GraphServiceClient (Graph SDK)
  • CertificateDescription has a new override FromBase64Encoded(string base64EncodedValue, string password) taking a password.
  • A new class ClientAssertionProviderBase is a base class for classes that compute client assertions that can be used as client certificates in services calling downstream web API. ManagedIdentityClientAssertion and
  • Microsoft.Identity.Web.DefaultCredentialsLoader provides extensibility to provide your own credential loaders.
  • AddDownstreamApi can be used both on a MicrosoftIdentityAppCallsWebApiAuthenticationBuilder (like AddDownstreamWebApi could be), or directly on a IServiceCollection.
  • DownstreamWebApi is proposed for deprecation in favor of the more powerfull DownstreaApi.
  • MicrosoftIdentityOptions has two new properties ClientCredentials (which is the recommended alternative to ClientSecret, ClientCertificates, and ), and TokenDecryptionCredentials, which is the recommended alternative to DecryptCertificates. These two properties enable certificate-less options removing the need for you to rotate secrets.
  • new classes TokenAcquirerTokenCredential and TokenAcquirerAppTokenCredential can be used to call Azure SDKs (in all scenarios / all .NET platforms)
  • new class TokenAcquirerFactory enables the other scenarios than ASP.NET Core (including OWIN and daemon scenarios with console apps)

Getting started with Microsoft Identity Web

Token cache serialization

Web apps

Web APIs

Daemon scenario

Advanced topics

FAQ

News

Contribute

Other resources

Clone this wiki locally