Skip to content

How ASP.NET Core uses Microsoft.IdentityModel extensions for .NET

Amit⚡ edited this page Aug 5, 2019 · 7 revisions

This article discusses the interactions between ASP.NET Core and Microsoft.IdentityModel extensions for .NET

ASP.NET Core enables you to create:

  • Web applications, signing-in users (and possibly calling Web APIs)
  • Protected Web APIs (possibly calling Web APIs)

ASP.NET Core (like ASP.NET) leverages the notion of Middleware. Depending on the type of app above, the middleware you'll use will be different:

  • OpenIdConnect middleware for Web Apps sign-ing in users
  • JwtBearer for protected Web APIs

In any case, ASP.NET Core will validate token using Microsoft.IdentityModel extensions for .NET, and you can control what aspects of the security tokens you want to validate using the TokenValidationParameters class.

In the cases where ASP.NET Core needs to sign-in users, it will call Azure AD (or another Security Token Service), and for this it will leverage the OpenIdConnectMessage. It will send such a message to the STS, and will receive another message from the STS. Then from this message will progressively be transformed, extracting the identity of the user (into a ClaimsPrincipal object).

Web Apps signing-in users with Azure AD

You can control the way tokens are validated

An example of ASP.NET Core app sign-in users with Azure AD is available from the following sample: active-directory-aspnetcore-webapp-openidconnect-v2. In particular, in the Startup.cs class, you'll notice the ConfigureServices method. This methods, between other things calls services.Configure<OpenIdConnectOptions>() and this method has an argument options (of type OpenIdConnectOptions which is an ASP.NET Core security concept). options has a property named TokenValidationParameters, of type TokenValidationParameters, which is from this library.

 services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>
 {
  . . . 

  // If you want to restrict the users that can sign-in to several organizations
  // Set the tenant value in the appsettings.json file to 'organizations', set
  // ValidateIssuer, above to 'true', and add the issuers you want to accept to the
  // options.TokenValidationParameters.ValidIssuers collection
   options.TokenValidationParameters.ValidateIssuer = false;
 });

You can control and understand how an OpenIdConnectMessage is progressively transformed to an identity and security tokens

options also has a property named Events which surfaces how:

  1. ASP.NET builds-up an OpenIdConnectMessage to send a request to Azure AD

    You have an opportunity to intercept this call and update the OpenIdConnectMessage by adding a handler to the OnRedirectToIdentityProvider(context) event. This can be useful, for instance, if you want to enable single-sign-on, by providing login_hint and domain_hint, which will avoid Azure AD to display the select account dialog. Note that the context parameter of each event has members, some of which are Identity Model Extensions for .NET types (SecurityToken, OpenIdConnectMessage, etc …)

  2. It receives back another OpenIdConnectMessage. You can observe it by subscribing to the OnMessageReceived event.

Depending on the options.ResponseType, the following events might be triggered:

  1. OnTokenValidation, in the case where options.ResponseType contains id_token. Typically the token validation involves the TokenValidationParameters set in the previous paragraph.

  2. OnAuthorizationCodeReceived when options.ResponseType contains code. This gives your application to redeem the code (for instance using MSAL.NET), and keep the access token and refresh token in the cache, so that it can be used later from ASP.NET Core Controllers.

  3. OnTokenResponseReceived when options.ResponseType contains token. This happens when you let ASP.NET Core redeeming the code. You will typically not do that if you handle the code redemption yourself in OnAuthorizationCodeReceived as you will have stopped further processing from happening by calling context.HandleCodeRedemption().

  4. OnAuthenticationFailed will be called any time the authentication has failed (be it because of the token validation or not)

There are another set of events which are about sign-out:

  • OnRedirectToIdentityProviderForSignOut is similar as OnRedirectToIdentityProvider, but for the Sign-out process, whereas this one is for the sign-in process.
  • OnRemoteSignOut is called when Azure AD (or the STS in general) has done the sign-out.

To learn more

If you want to see how this pipeline is used to let a Web App calling a Web API on behalf of the signed-in user, look at the following

If you want to understand how the pipeline works by seing traces, or setting breakpoints in event handlers, look at this PR: https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2/pull/35, which implements a few diagnostics for each event.

Protected Web API

The same kind of customization and pipeline exists in a protected Web API. For more details, see the following sample: https://github.com/Azure-Samples/active-directory-dotnet-native-aspnetcore-v2

This sample contains a progressive tutorial made of two parts:

Sub folder Description
1. Desktop app calls Web API This first part, presents an ASP.NET Core 2.1 Web API protected by Azure Active Directory OAuth Bearer Authentication. This Web API is exercised by a .NET Desktop WPF application. This subfolder contains a Visual Studio solution made of two applications: the desktop application (TodoListClient), and the Web API (TodoListService)
2. Web API now calls Microsoft Graph The second part presents an increment where the Web API now calls Microsoft Graph on-behalf of the user signed-in in the desktop application. In this part, the Web API uses the Microsoft Authentication Library for .NET (MSAL.NET) to acquire a token for Microsoft Graph using the on-behalf-of flow.

The same kind of principles apply for Web APIs, replacing the OpenIdConnectOptions by JwtBearerOptions and OpenIdConnectEvents by JwtBearerEvents:

  services.Configure<JwtBearerOptions>(AzureADDefaults.JwtBearerAuthenticationScheme, options =>
  {
   // This is an Azure AD v2.0 Web API
   options.Authority += "/v2.0";

   // The valid audiences are both the Client ID (options.Audience) and api://{ClientID}
   options.TokenValidationParameters.ValidAudiences = new string[] { options.Audience, $"api://{options.Audience}" };

   // Instead of using the default validation (validating against a single tenant, as we do in line of business apps),
   // we inject our own multitenant validation logic (which even accepts both V1 and V2 tokens)
   options.TokenValidationParameters.IssuerValidator = AadIssuerValidator.ValidateAadIssuer;


   // When an access token for our own Web API is validated, we add it to MSAL.NET's cache so that it can
   // be used from the controllers.
   options.Events = new JwtBearerEvents();
   options.Events.OnTokenValidated = async context =>
   {
    var tokenAcquisition = context.HttpContext.RequestServices.GetRequiredService<ITokenAcquisition>();
    var scopes = new string[] { "user.read" };
    context.Success();

    // Adds the token to the cache, and also handles the incremental consent and claim challenges
    tokenAcquisition.AddAccountToCacheFromJwt(context, scopes);
   };
Clone this wiki locally