Skip to content

Web Apps

Jean-Marc Prieur edited this page Jan 12, 2024 · 50 revisions

Using Microsoft.Identity.Web to protect ASP.NET Core web apps

Microsoft identity web supports ASP.NET Core web apps that sign-in users in Microsoft Entra ID, Azure AD B2C, and Microsoft Entra External IDs. Optionally these apps can call downstream web APIs. Web apps typically run on a server and serve HTML pages.

Create a new ASP.NET core web app

To create a new ASP.NET Core web app you can either use the command line, or the Visual Studio wizard.

Create a new ASP.NET Core web app using the command line

You can create a web app that sign-in users. You can also add the capability of calling Microsoft Graph, or any downstream API. Based on what you want to have, use the following commands, from a developer prompt.

Add web app calls graph call downstream API
Entra ID or Entra External IDs dotnet new webapp --auth SingleOrg --calls-graph --called-api-scopes <scopes> --called-api-url <url>
Azure AD B2C dotnet new webapp --auth IndividualB2C N/A --called-api-scopes <scopes> --called-api-url <url>

ex:

dotnet new webapp --auth SingleOrg --calls-graph

You can also replace webapp by:

  • mvc to have controllers and views instead of razor pages
  • razor (which is the same as webapp)
  • blazorserver to have a web app with blazor pages. From the authentication point of view things will be the same.

AFter the command line runs, you 'll have the code for your web app. You now need to map the configuration of this code (in the appsettings.json file) to a app registration in Azure AD (new or existing). For this, check out the msidentity-app-sync tool

Create a new ASP.NET Core web app using the Visual Studio wizard

  1. In Visual Studio, choose Create a new project

  2. In the Create a new project dialog, choose ASP.NET Core web app, and press Next

  3. Provide a project name, a location, and a solution name, and press next

  4. in the next dialog, in the Authentication type drop down, choose "Microsoft identity platform"

    then click Create

  5. Once the code is generated, the 'Connected services' page automatically opens in Visual Studio, and proposes you to install the donet msidentity tool, that will handle the app registration for you. Click Next

  6. Choose a tenant where to create an application. Depending on the tenant type (AAD or B2C), your code will be updated to be an AAD or an Azure AD B2C application.

  7. Create a new app, or pick an existing app in the tenant.

  8. Choose if you want to call Microsoft Graph or not. The code for your application will be updated accordingly

  9. If you chose to call an API, the wizard will also provide you with options to store the application secret.

Add authentication to an existing web app

  1. Add the nuget Microsoft.Identity.Web NuGet package

  2. Add the following usings at the top of the file

    using Microsoft.AspNetCore.Authentication.OpenIdConnect;
    using Microsoft.Identity.Web;
  3. In the Program.cs file, after var app = builder.Build();, add:

    builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
           .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"));
  4. Replace services.AddRazorPages() by:

      services.AddRazorPages().AddMvcOptions(options =>
      {
       var policy = new AuthorizationPolicyBuilder()
                     .RequireAuthenticatedUser()
                     .Build();
                 options.Filters.Add(new AuthorizeFilter(policy));
      }).AddMicrosoftIdentityUI();
     }
  5. After app.UseAuthentication(), use:

      app.UseAuthentication();
      app.UseAuthorization();
    
      // More code
      app.UseEndpoints(endpoints =>
      {
       endpoints.MapRazorPages();  // If Razor pages
       endpoints.MapControllers(); // Needs to be added
      });
     }

Migrate an ASP.NET Core 3.1 web app to use Microsoft Identity Web

See Migrate an ASP.NET Core 3.1 web app to use Microsoft Identity Web

Advanced scenarios

Enabling a 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);
}

Using delegate events instead of the configuration section

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)

      services.AddAuthentication("MyAuthenticationScheme")
              .AddMicrosoftIdentityWebApp(Configuration, 
                 openIdConnectAuthenticationScheme: "MyAuthenticationScheme");

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

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.

See also

Authenticate using Azure AD and OpenID Connect

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