Web Apps - mattchenderson/microsoft-identity-web GitHub Wiki

Why use Microsoft.Identity.Web in web apps?

Currently, ASP.NET Core 3.1 web app templates (dot net new mvc -auth) create web apps that sign in users with the Azure AD v1.0 endpoint, allowing users to sign in with their organizational accounts (also called Work or school accounts).

This library adds ServiceCollection and AuthenticationBuilder extension methods for use in the ASP.NET Core web app Startup.cs file. These extension methods enable the web app to sign in users with the Microsoft identity platform and, optionally, enable the web app to call APIs on behalf of the signed-in user.

.NET Core 5.0 creates project templates using directly Microsoft.Identity.Web

Usage

Web apps that sign in users - Startup.cs

With Azure AD

Migrating from previous versions / adding authentication

Assuming you have a similar configuration in appsettings.json to enable the web app:

{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "msidentitysamplestesting.onmicrosoft.com",
    "TenantId": "7f58f645-c190-4ce5-9de4-e2b7acd2a6ab",
    "ClientId": "86699d80-dd21-476a-bcd1-7c1a3d471f75",
    "CallbackPath": "/signin-oidc",
    "SignedOutCallbackPath ": "/signout-callback-oidc",

    // Only if you want to call an API
    "ClientSecret": "[Copy the client secret added to the app from the Azure portal]"
  },
...
}

To enable users to sign in with the Microsoft identity platform:

  1. Add the Microsoft.Identity.Web and Microsoft.Identity.Web.UI NuGet packages (currently in Preview)

  2. Remove the AzureAD.UI and AzureADB2C.UI NuGet packages

  3. Replace this code in your web application's Startup.cs file:

    using Microsoft.Identity.Web;
    
    public class Startup
    {
      ...
      public void ConfigureServices(IServiceCollection services)
      {
       ...
       services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
            .AddAzureAD(options => Configuration.Bind("AzureAd", options));
       ...
      }
      ...
    }
    

    ... by the following code:

    using Microsoft.Identity.Web;
    
    public class Startup
    {
      ...
      public void ConfigureServices(IServiceCollection services)
      {
       ...
          services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
                  .AddMicrosoftIdentityWebApp(Configuration);
       ...
      }
      ...
    }
    
    
Using the sign-in/sign-out UI

This method adds authentication with the Microsoft identity platform. This includes validating the token in all scenarios (single- and multi-tenant applications) in the Azure public and national clouds.

You also need to call AddMicrosoftIdentityUI() if you want to benefit from the sign-in / sign-out.

For instance for Razor pages, you'd want something like the following in Startup.ConfigureServices(IServiceCollection services):

  services.AddRazorPages().AddMvcOptions(options =>
  {
   var policy = new AuthorizationPolicyBuilder()
                    .RequireAuthenticatedUser()
                    .Build();
                options.Filters.Add(new AuthorizeFilter(policy));
  }).AddMicrosoftIdentityUI();

Finally, in Startup.Configure(IApplicationBuilder app, IWebHostEnvironment env) you want to make sure that you map the controllers which are provided by Microsoft.Identity.Web.UI.

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
     // More code

     app.UseAuthentication();
     app.UseAuthorization();

     // More code
     app.UseEndpoints(endpoints =>
     {
      endpoints.MapRazorPages();  // If Razor pages
      endpoints.MapControllers(); // Needs to be added
     });
    }
Enabling sign-up experience

You can enable your web app to allow users to sign up and create a new guest account. First, set up your tenant and the app as described in Add a self-service sign-up user flow to an app. Next, set Prompt property in OpenIdConnectOptions (or in MicrosoftIdentityOptions which inherits from it) to "create" to trigger the sign-up experience. Your app can have a Sign up button linking to an Action which sets the Prompt property, like in the example below. After the user goes through the sign-up process, they will be logged into the app.

[HttpGet("{scheme?}")]
public IActionResult SignUp([FromRoute] string scheme)
{
    scheme ??= OpenIdConnectDefaults.AuthenticationScheme;
    var parameters = new Dictionary<string, object>
    {
        { "prompt", "create" },
    };
    OAuthChallengeProperties oAuthChallengeProperties = new OAuthChallengeProperties(new Dictionary<string, string>(), parameters);
    oAuthChallengeProperties.RedirectUri = Url.Content("~/");

    return Challenge(
        oAuthChallengeProperties,
        scheme);
}

Advanced scenarios

Using delegate events

AddMicrosoftIdentityWebApp (applied to authentication builders) has another override, which takes delegates instead of a configuration section. The override with a configuration section actually calls the override with delegates. See the source code for AddMicrosoftIdentityWebApp with configuration section

In advanced scenarios you might want to add configuration by code, or if you want to subscribe to OpenIdConnect events. For instance if you want to provide a custom processing when the token is validated, you could use code like the following:

services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
        .AddMicrosoftIdentityWebApp(options =>
{
    Configuration.Bind("AzureAD", options);
    options.Events ??= new OpenIdConnectEvents();
    options.Events.OnTokenValidated += OnTokenValidatedFunc;
});

with OnTokenValidatedFunc like the following:

private async Task OnTokenValidatedFunc(TokenValidatedContext context)
{
    // Custom code here
    await Task.CompletedTask.ConfigureAwait(false);
}

In the above code, your handler will be executed after any existing handlers. In the below code, your code will be executed before any other existing handlers.

services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
        .AddMicrosoftIdentityWebApp(options =>
{
    Configuration.Bind("AzureAD", options);
    options.Events ??= new OpenIdConnectEvents();
    var existingHandlers = options.Events.OnTokenValidated;
    options.Events.OnTokenValidated = OnTokenValidatedFunc;
    options.Events.OnTokenValidated += existingHandlers;
});

This override also allows you to set an optional delegate to initialize the CookiesAuthenticationOptions.

Specify the authentication scheme

In more advanced scenarios, for instance if you use several IdPs, you might want to specify an authentication scheme (here using the default authentication scheme, which makes this code snippet equivalent to the previous one)

using Microsoft.Identity.Web;

public class Startup
{
  ...
  public void ConfigureServices(IServiceCollection services)
  {
   ...
      services.AddAuthentication("MyAuthenticationScheme")
              .AddMicrosoftIdentityWebApp(Configuration, 
                 openIdConnectAuthenticationScheme: "MyAuthenticationScheme");
   ...
  }
  ...
}

Additional resources to sign-in user to a web app with Microsoft identity platform

See also:

With Azure AD B2C

The principle is the same, except that the appsettings.json has generally a section named "AzureAdB2C" (but you can choose the name you want, provided it's consistent with what you use in the AddMicrosoftIdentityWebApp method, and you need to declare policies.

{
  "AzureAdB2C": {
    "Instance": "https://fabrikamb2c.b2clogin.com",
    "ClientId": "fdb91ff5-5ce6-41f3-bdbd-8267c817015d",
    "Domain": "fabrikamb2c.onmicrosoft.com",
    "SignedOutCallbackPath": "/signout/B2C_1_susi",
    "SignUpSignInPolicyId": "b2c_1_susi",
    "ResetPasswordPolicyId": "b2c_1_reset",
    "EditProfilePolicyId": "b2c_1_edit_profile", // Optional profile editing policy

The startup.cs file is then:

using Microsoft.Identity.Web;

public class Startup
{
  ...
  public void ConfigureServices(IServiceCollection services)
  {
   ...
   services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
      .AddMicrosoftIdentityWebApp(Configuration, "AzureAdB2C");
   ...
  }
  ...
}

See also:

Programmatically invoke a B2C journey

This example, demonstrates how to programmatically trigger a B2C user flow from a controller action or razor. In the AuthorizeForScopes attribute, you will define the scopes and B2C user flow, and then include the same in GetAccessTokenForUserAsync. Microsoft Identity Web will handle the challenge for you.

  [AuthorizeForScopes(Scopes = new string[] { Scope }, UserFlow = EditProfile)] // Must be the same user flow as used in `GetAccessTokenForUserAsync()`
  public async Task<ActionResult> ClaimsEditProfile()
  {
   // We get a token, but we don't use it. It's only to trigger the user flow
   await _tokenAcquisition.GetAccessTokenForUserAsync(
                new string[] { Scope },
                userFlow: EditProfile);
   return View(Claims, null);
  }

Web apps that sign in users and call web APIs on behalf of the signed-in user - Startup.cs

If you want your web app to call web APIs, add the .EnableTokenAcquisitionToCallDownstreamApi() line, and then choose a token cache implementation, for example .AddInMemoryTokenCaches():

WebAppBuilderExtensionsMethods

WebAppServiceExtensionsMethods

using Microsoft.Identity.Web;

public class Startup
{
 const string scopesToRequest = "user.read";
  ...
  public void ConfigureServices(IServiceCollection services)
  {
   ...
   services.AddMicrosoftIdentityWebAppAuthentication(Configuration)
                .EnableTokenAcquisitionToCallDownstreamApi(new string[] { scopesToRequest })
                     .AddInMemoryTokenCaches();
   ...
  }
  ...
}

By default, AddMicrosoftIdentityWebAppAuthentication and the override of AddMicrosoftIdentityWebApp taking a configuration object get the configuration from the "AzureAD" section of the configuration files. It has several parameters you can change.

The proposed token cache serialization is in memory. You can also use the session cache, or various distributed caches.

Optimization

Note that you don't need to pass-in the scopes to request when calling EnableTokenAcquisitionToCallDownstreamApi. You can do that just in time in the controller (see Web app controller below)

using Microsoft.Identity.Web;

public class Startup
{
  ...
  public void ConfigureServices(IServiceCollection services)
  {
   ...
   services.AddMicrosoftIdentityWebAppAuthentication(Configuration)
                .EnableTokenAcquisitionToCallDownstreamApi()
                  .AddInMemoryTokenCaches();
   ...
  }
  ...
}

Web app controller

For your web app to call web APIs on behalf of the signed-in user, add a parameter of type ITokenAcquisition to the constructor of your controller (the ITokenAcquisition service will be injected by dependency injection by ASP.NET Core).

ITokenAcquisition

using Microsoft.Identity.Web;

[Authorize]
public class HomeController : Controller
{
  readonly ITokenAcquisition tokenAcquisition;

  public HomeController(ITokenAcquisition tokenAcquisition)
  {
   this.tokenAcquisition = tokenAcquisition;
  }
  ...

Then, in your controller actions, call ITokenAcquisition.GetAccessTokenForUserAsync, passing the scopes for which to request a token. The other methods of ITokenAcquisition are used from the EnableTokenAcquisitionToCallDownstreamApi() method and similar methods for web APIs (see below).

[Authorize]
public class HomeController : Controller
{
  readonly ITokenAcquisition tokenAcquisition;
  ...
  [AuthorizeForScopes(Scopes = new[] { "user.read" })]
  public async Task<IActionResult> Action()
  {
   string[] scopes = new []{"user.read"};
   string token = await tokenAcquisition.GetAccessTokenForUserAsync(scopes);
   ...
   // call the downstream API with the bearer token in the Authorize header
  }

The controller action is decorated with the AuthorizeForScopesAttribute which enables it to process the MsalUiRequiredException that could be thrown by the service implementing ITokenAcquisition.GetAccessTokenForUserAsync. The web app can then interact with the user and ask them to consent to the scopes, or re-sign in if needed.

AuthorizeForScopes

Web apps that acquire tokens on their own behalf (daemon scenarios, client credential flow)

If your application wants to call a web API on behalf of itself (not of behalf of a user), you can use ITokenAcquisition.GetAccessTokenForAppAsync in the controller. The code in the startup.cs file is the same as when you call an API on behalf of a user, and the constructor of your controller or Razor page injects an ITokenAcquisition service.

[Authorize]
public class HomeController : Controller
{
  readonly ITokenAcquisition tokenAcquisition;
  ...
  public async Task<IActionResult> Action()
  {
   string[] scopes = new []{"users.read.all"};
   string token = await tokenAcquisition.GetAccessTokenForAppAsync(scopes);
   ...
   // call the downstream API with the bearer token in the Authorize header
  }

More information about the scenarios

For more details on the end to end scenario, see:

Microsoft Identity Web and Protocols

OAuth 2.0 protocols used in web apps.

In web apps, Microsoft.Identity.Web leverages the following OAuth 2.0 protocols:

AADB2C90088 invalid_grant error

If you have an AAD B2C web app, which just signs in users (does not call a web API), and you include a valid client secret for the web app in the appsettings.json file, you will get this error when switching between policies:

Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler: Error: Message contains error: 'invalid_grant', error_description: 'AADB2C90088: The provided grant has not been issued for this endpoint. Actual Value : B2C_1_susi_v3 and Expected Value : B2C_1_edit_profile

How do I resolve the error?

Do one of the following:

  1. Remove the client secret in appsettings.json. If the web app is only signing in users and NOT calling a downstream API, it does not need to have the client secret included in the appsettings.json file.

  2. Enable MSAL .NET to correctly handle the auth code redemntion by including the following in Startup.cs:

services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
                    .AddMicrosoftIdentityWebApp(Configuration, "AzureAdB2C")
                        .EnableTokenAcquisitionToCallDownstreamApi()
                        .AddInMemoryTokenCaches();

Why?

This occurs because each policy, or user flow, is a separate authorization server in AAD B2C, meaning each user flow issues their own tokens. When only signing-in users, your client app only needs the ID token, which contains information about the signed in user. However, if the client secret is included in the appsettings.json, Microsoft Identity Web assumes the web app will eventually call a downstream API, so it requests a code and ID token. ASP .NET then receives the code, but cannot complete the authorization code flow, so MSAL .NET is invoked to redeem the code for tokens, but the ID token and the code were not provided by the same authority (for example, one for su_si policy and one for edit_profile policy), so an invalid_grant error is thrown.