Skip to content

Latest commit

 

History

History
850 lines (619 loc) · 53.5 KB

File metadata and controls

850 lines (619 loc) · 53.5 KB
page_type name description languages products urlFragment extensions
sample
Integrate a web app and Web Api that authenticates users and calls a custom Web API and Microsoft Graph using the multi-tenant integration pattern (SaaS)
Integrate a web app and Web Api that authenticates users and calls a protected Web API and Microsoft Graph using the multi-tenant integration pattern (SaaS)
csharp
azure
dotnet
microsoft-entra-id
ms-graph
microsoft-identity-platform-aspnetcore-webapp-tutorial
services
ms-identity
platform
AspNetCore
endpoint
Microsoft Entra ID v2.0
level
400
client
ASP.NET Core Web App
service
ASP.NET Core Web API

Integrate a web app and Web Api that authenticates users and calls a custom Web API and Microsoft Graph using the multi-tenant integration pattern (SaaS)

Build status

Overview

This sample demonstrates a ASP.NET Core Web App calling a ASP.NET Core Web API that is secured using Microsoft Entra ID.

ℹ️ To learn how to integrate an application with Microsoft Entra ID as a multi-tenant app, consider going through the recorded session:Develop multi-tenant applications with the Microsoft identity platform.

Scenario

This sample demonstrates how to secure a multi-tenant ASP.NET Core MVC web application (TodoListClient) which calls another protected multi-tenant ASP.NET Core Web API (ToDoListService) with the Microsoft Identity Platform. This sample builds on the concepts introduced in the Integrate an app that authenticates users and calls Microsoft Graph using the multi-tenant integration pattern (SaaS) sample. We advise you go through that sample once before trying this sample.

In this sample, we would protect an ASP.Net Core Web API using the Microsoft Identity Platform. The Web API will be protected using Microsoft Entra ID OAuth 2.0 Bearer Authorization. The API will support authenticated users with Work and School accounts. Further on the API will also call a downstream API (Microsoft Graph) on behalf of the signed-in user using the OAuth 2.0 on-behalf-of flow to provide additional value to its client apps.

The Web API is marked as a multi-tenant app, so that it can be provisioned into Microsoft Entra tenants where the registered client applications in that tenant can then obtain Access Tokens for this web API and make calls to it.

Note that the client applications that want to call this web API do not need to be multi-tenant themselves to be able to do so.

Overview

This sample presents a client Web application that signs-in users and obtains an Access Token for this protected Web API.

Both applications use the Microsoft.Identity.Web and Microsoft Authentication Library MSAL.NET to sign-in user and obtain a JWT access token through the OAuth 2.0 protocol.

The client Web App:

  1. Signs-in users using the MSAL.NET and Microsoft.Identity.Web libraries.
  2. Acquires an Access Token for the protected Web API.
  3. Calls the ASP.NET Core Web API by using the access token as a bearer token in the authentication header of the Http request.

The Web API:

  1. Authorizes the caller (user) using the Microsoft.Identity.Web.
  2. Acquires another access token on-behalf-of the signed-in user using the on-behalf of flow.
  3. The Web API then uses this new Access token to call Microsoft Graph.

You can run the sample by using either Visual Studio or command line interface as shown below:

Running the sample using Visual Studio

Clean the solution, rebuild the solution, and run it. You might want to go into the solution properties and set both projects as startup projects, with the service project starting first.

When you start the Web API from Visual Studio, depending on the browser you use, you'll get:

  • an empty web page (with Microsoft Edge)
  • or an error HTTP 401 (with Chrome)

This behavior is expected as the browser is not authenticated. The Web application will be authenticated, so it will be able to access the Web API.

A recording of a Microsoft Identity Platform developer session that covered this topic of developing a multi-tenant app with Microsoft Entra ID is available at Develop multi-tenant applications with Microsoft identity platform. Scenario Image

Prerequisites

This sample will not work with a personal Microsoft account. If you're signed in to the Microsoft Entra admin center with a personal Microsoft account and have not created a user account in your directory before, you will need to create one before proceeding.

Setup the sample

Step 1: Clone or download this repository

From your shell or command line:

git clone https://github.com/Azure-Samples/microsoft-identity-platform-aspnetcore-webapp-tutorial.git

or download and extract the repository .zip file.

⚠️ To avoid path length limitations on Windows, we recommend cloning into a directory near the root of your drive.

Step 2: Navigate to project folder

cd 4-WebApp-your-API\4-3-AnyOrg\TodoListService

Step 3: Register the sample application(s) in your tenant

There are two projects in this sample. Each needs to be separately registered in your Microsoft Entra tenant. To register these projects, you can:

  • follow the steps below for manually register your apps
  • or use PowerShell scripts that:
    • automatically creates the Microsoft Entra applications and related objects (passwords, permissions, dependencies) for you.
    • modify the projects' configuration files.
Expand this section if you want to use this automation:
> :warning: If you have never used **Microsoft Graph PowerShell** before, we recommend you go through the [App Creation Scripts Guide](./AppCreationScripts/AppCreationScripts.md) once to ensure that your environment is prepared correctly for this step.

1. On Windows, run PowerShell as **Administrator** and navigate to the root of the cloned directory
1. In PowerShell run:

   ```PowerShell
   Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process -Force
   ```

1. Run the script to create your Microsoft Entra application and configure the code of the sample application accordingly.
1. For interactive process -in PowerShell, run:

   ```PowerShell
   cd .\AppCreationScripts\
   .\Configure.ps1 -TenantId "[Optional] - your tenant id" -AzureEnvironmentName "[Optional] - Azure environment, defaults to 'Global'"
   ```

> Other ways of running the scripts are described in [App Creation Scripts guide](./AppCreationScripts/AppCreationScripts.md). The scripts also provide a guide to automated application registration, configuration and removal which can help in your CI/CD scenarios.

Choose the Microsoft Entra tenant where you want to create your applications

To manually register the apps, as a first step you'll need to:

  1. Sign in to the Microsoft Entra admin center.
  2. If your account is present in more than one Microsoft Entra tenant, select your profile at the top right corner in the menu on top of the page, and then switch directory to change your portal session to the desired Microsoft Entra tenant.

Register the service app (WebApi_MultiTenant_v2)

  1. Navigate to the Microsoft Entra admin center and select the Microsoft Entra ID service.
  2. Select the App Registrations blade on the left, then select New registration.
  3. In the Register an application page that appears, enter your application's registration information:
    1. In the Name section, enter a meaningful application name that will be displayed to users of the app, for example WebApi_MultiTenant_v2.
    2. Under Supported account types, select Accounts in any organizational directory
    3. Select Register to create the application.
  4. In the Overview blade, find and note the Application (client) ID. You use this value in your app's configuration file(s) later in your code.
  5. In the app's registration screen, select the Authentication blade to the left.
  6. If you don't have a platform added, select Add a platform and select the Web option.
    1. In the Redirect URI section enter the following redirect URI:
      1. https://localhost:44351/api/Home
    2. Click Save to save your changes.
  7. In the app's registration screen, select the Certificates & secrets blade in the left to open the page where you can generate secrets and upload certificates.
  8. In the Client secrets section, select New client secret:
    1. Type a key description (for instance app secret).
    2. Select one of the available key durations (6 months, 12 months or Custom) as per your security posture.
    3. The generated key value will be displayed when you select the Add button. Copy and save the generated value for use in later steps.
    4. You'll need this key later in your code's configuration files. This key value will not be displayed again, and is not retrievable by any other means, so make sure to note it from the Microsoft Entra admin center before navigating to any other screen or blade.

    💡 For enhanced security, instead of using client secrets, consider using certificates and Azure KeyVault.

    1. Since this app signs-in users, we will now proceed to select delegated permissions, which is is required by apps signing-in users.
    2. In the app's registration screen, select the API permissions blade in the left to open the page where we add access to the APIs that your application needs:
    3. Select the Add a permission button and then:
    4. Ensure that the Microsoft APIs tab is selected.
    5. In the Commonly used Microsoft APIs section, select Microsoft Graph
    6. In the Delegated permissions section, select User.Read.All in the list. Use the search box if necessary. This permission requires Admin Consent, so please select Grant admin consent.
    7. Select the Add permissions button at the bottom.
  9. In the app's registration screen, select the Expose an API blade to the left to open the page where you can publish the permission as an API for which client applications can obtain access tokens for. The first thing that we need to do is to declare the unique resource URI that the clients will be using to obtain access tokens for this API. To declare an resource URI(Application ID URI), follow the following steps:
    1. Select Set next to the Application ID URI to generate a URI that is unique for this app.
    2. For this sample, accept the proposed Application ID URI (api://{clientId}) by selecting Save.

      ℹ️ Read more about Application ID URI at Validation differences by supported account types (signInAudience).

Publish Delegated Permissions
  1. All APIs must publish a minimum of one scope, also called Delegated Permission, for the client apps to obtain an access token for a user successfully. To publish a scope, follow these steps:
  2. Select Add a scope button open the Add a scope screen and Enter the values as indicated below:
    1. For Scope name, use ToDoList.Read.
    2. Select Admins and users options for Who can consent?.
    3. For Admin consent display name type in Read users ToDo list using the 'WebApi_MultiTenant_v2'.
    4. For Admin consent description type in Allow the app to read the user's ToDo list using the 'WebApi_MultiTenant_v2'.
    5. For User consent display name type in Read your ToDo list items via the 'WebApi_MultiTenant_v2'.
    6. For User consent description type in Allow the app to read your ToDo list items via the 'WebApi_MultiTenant_v2'.
    7. Keep State as Enabled.
    8. Select the Add scope button on the bottom to save this scope.

    Repeat the steps above for another scope named ToDoList.ReadWrite

  3. Select the Manifest blade on the left.
    1. Set accessTokenAcceptedVersion property to 2.
    2. Select on Save.

ℹ️ Follow the principle of least privilege when publishing permissions for a web API.

Publish Application Permissions
  1. All APIs should publish a minimum of one App role for applications, also called Application Permission, for the client apps to obtain an access token as themselves, i.e. when they are not signing-in a user. Application permissions are the type of permissions that APIs should publish when they want to enable client applications to successfully authenticate as themselves and not need to sign-in users. To publish an application permission, follow these steps:

  2. Still on the same app registration, select the App roles blade to the left.

  3. Select Create app role:

    1. For Display name, enter a suitable name for your application permission, for instance ToDoList.Read.All.
    2. For Allowed member types, choose Application to ensure other applications can be granted this permission.
    3. For Value, enter ToDoList.Read.All.
    4. For Description, enter Allow the app to read every user's ToDo list using the 'WebApi_MultiTenant_v2'.
    5. Select Apply to save your changes.

    Repeat the steps above for another app permission named ToDoList.ReadWrite.All

Configure Optional Claims
  1. Still on the same app registration, select the Token configuration blade to the left.
  2. Select Add optional claim:
    1. Select optional claim type, then choose Access.
    2. Select the optional claim idtyp.

    Indicates token type. This claim is the most accurate way for an API to determine if a token is an app token or an app+user token. This is not issued in tokens issued to users.

    1. Select Add to save your changes.
Configure the service app (WebApi_MultiTenant_v2) to use your app registration

Open the project in your IDE (like Visual Studio or Visual Studio Code) to configure the code.

In the steps below, "ClientID" is the same as "Application ID" or "AppId".

  1. Open the ToDoListService\appsettings.json file.
  2. Find the key Domain and replace the existing value with your Microsoft Entra tenant domain, ex. contoso.onmicrosoft.com.
  3. Find the key TenantId and replace the existing value with 'common'.
  4. Find the key ClientId and replace the existing value with the application ID (clientId) of WebApi_MultiTenant_v2 app copied from the Microsoft Entra admin center.
  5. Find the key ClientSecret and replace the existing value with the generated secret that you saved during the creation of WebApi_MultiTenant_v2 copied from the Microsoft Entra admin center.

Register the client app (WebApp_MultiTenant_v2)

  1. Navigate to the Microsoft Entra admin center and select the Microsoft Entra ID service.
  2. Select the App Registrations blade on the left, then select New registration.
  3. In the Register an application page that appears, enter your application's registration information:
    1. In the Name section, enter a meaningful application name that will be displayed to users of the app, for example WebApp_MultiTenant_v2.
    2. Under Supported account types, select Accounts in any organizational directory
    3. Select Register to create the application.
  4. In the Overview blade, find and note the Application (client) ID. You use this value in your app's configuration file(s) later in your code.
  5. In the app's registration screen, select the Authentication blade to the left.
  6. If you don't have a platform added, select Add a platform and select the Web option.
    1. In the Redirect URI section enter the following redirect URIs:
      1. https://localhost:44321/
      2. https://localhost:44321/signin-oidc
    2. In the Front-channel logout URL section, set it to https://localhost:44321/signout-callback-oidc.
    3. Click Save to save your changes.
  7. In the app's registration screen, select the Certificates & secrets blade in the left to open the page where you can generate secrets and upload certificates.
  8. In the Client secrets section, select New client secret:
    1. Type a key description (for instance app secret).
    2. Select one of the available key durations (6 months, 12 months or Custom) as per your security posture.
    3. The generated key value will be displayed when you select the Add button. Copy and save the generated value for use in later steps.
    4. You'll need this key later in your code's configuration files. This key value will not be displayed again, and is not retrievable by any other means, so make sure to note it from the Microsoft Entra admin center before navigating to any other screen or blade.

    💡 For enhanced security, instead of using client secrets, consider using certificates and Azure KeyVault.

    1. Since this app signs-in users, we will now proceed to select delegated permissions, which is is required by apps signing-in users.
    2. In the app's registration screen, select the API permissions blade in the left to open the page where we add access to the APIs that your application needs:
    3. Select the Add a permission button and then:
    4. Ensure that the My APIs tab is selected.
    5. In the list of APIs, select the API WebApi_MultiTenant_v2.
    6. In the Delegated permissions section, select ToDoList.Read, ToDoList.ReadWrite in the list. Use the search box if necessary.
    7. Select the Add permissions button at the bottom.
Configure Optional Claims
  1. Still on the same app registration, select the Token configuration blade to the left.
  2. Select Add optional claim:
    1. Select optional claim type, then choose ID.
    2. Select the optional claim acct.

    Provides user's account status in tenant. If the user is a member of the tenant, the value is 0. If they're a guest, the value is 1.

    1. Select Add to save your changes.
Configure the client app (WebApp_MultiTenant_v2) to use your app registration

Open the project in your IDE (like Visual Studio or Visual Studio Code) to configure the code.

In the steps below, "ClientID" is the same as "Application ID" or "AppId".

  1. Open the ToDoListClient\appsettings.json file.
  2. Find the key ClientId and replace the existing value with the application ID (clientId) of WebApp_MultiTenant_v2 app copied from the Microsoft Entra admin center.
  3. Find the key TenantId and replace the existing value with 'common'.
  4. Find the key Domain and replace the existing value with your Microsoft Entra tenant domain, ex. contoso.onmicrosoft.com.
  5. Find the key ClientSecret and replace the existing value with the generated secret that you saved during the creation of WebApp_MultiTenant_v2 copied from the Microsoft Entra admin center.
  6. Find the key RedirectUri and replace the existing value with the base address of WebApp_MultiTenant_v2 (by default https://localhost:44321/).
  7. Find the key TodoListServiceScope and replace the existing value with ScopeDefault.
  8. Find the key TodoListServiceAppId and replace the existing value with the application ID (clientId) of WebApi_MultiTenant_v2 app copied from the Microsoft Entra admin center.
  9. Find the key TodoListBaseAddress and replace the existing value with the base address of WebApi_MultiTenant_v2 (by default https://localhost:44351/).
  10. Find the key AdminConsentRedirectApi and replace the existing value with the Redirect URI for WebApi_MultiTenant_v2. (by default https://localhost:44351/).
  11. Find the app key ClientCertificates and add the keys as displayed below:
    "ClientCertificates": [
        {
        "SourceType": "",
        "CertificateDiskPath": "",
        "CertificatePassword": ""
        }
    ]
  1. Update values of the keys: 1 .SourceType to Path.
    1. CertificateDiskPath to the path where certificate exported with private key (the name will be assigned automatically by PowerShell script and it will be equal to the Application name.pfx) is stored. For example, C:\\AppCreationScripts\the name will be assigned automatically by PowerShell script and it will be equal to the Application name.pfx
    2. CertificatePassword add the password used while exporting the certificate.
  2. If you had set ClientSecret previously, set its value to empty string, "".

Configure Known Client Applications for service (WebApi_MultiTenant_v2)

For a middle-tier web API (WebApi_MultiTenant_v2) to be able to call a downstream web API, the middle tier app needs to be granted the required permissions as well. However, since the middle-tier cannot interact with the signed-in user, it needs to be explicitly bound to the client app in its Microsoft Entra ID registration. This binding merges the permissions required by both the client and the middle-tier web API and presents it to the end user in a single consent dialog. The user then consent to this combined set of permissions. To achieve this, you need to add the Application Id of the client app to the knownClientApplications property in the manifest of the web API. Here's how:

  1. In the Microsoft Entra admin center, navigate to your WebApi_MultiTenant_v2 app registration, and select the Manifest blade.
  2. In the manifest editor, change the knownClientApplications: [] line so that the array contains the Client ID of the client application (WebApp_MultiTenant_v2) as an element of the array.

For instance:

    "knownClientApplications": ["ca8dca8d-f828-4f08-82f5-325e1a1c6428"],
  1. Save the changes to the manifest.

Variation: Using certificates instead of client secrets

Follow README-use-certificate.md to know how to use this option.

Step 4: Running the sample

From your shell or command line, execute the following commands:

    cd 4-WebApp-your-API\4-3-AnyOrg\TodoListService\TodoListService
    dotnet run

Then, open a separate command terminal and run:

    cd 4-WebApp-your-API\4-3-AnyOrg\ToDoListClient
    dotnet run

Explore the sample

Expand the section

Open your browser and navigate to https://localhost:44321.

NOTE: Remember, the To-Do list is stored in memory in this ToDoListService app. Each time you run the projects, your To-Do list will get emptied.

Testing the Application

To properly test this application, you need at least two tenants, and on each tenant, at least one administrator and one non-administrator account.

The different ways of obtaining admin consent

A service principal of your multi-tenant app and API is provisioned after the tenant admin manually or programmatically consents. The consent can be obtained from a tenant admin by using one of the following methods:

  1. By using the /adminconsent endpoint.
  2. By Using the PowerShell command New-AzADServicePrincipal.

Obtain Consent using the /adminconsent endpoint

You can try the /adminconsent endpoint on the home page of the sample by clicking on the Consent as Admin link. Web API is provisioned first because the Web App is dependent on the Web API. The admin consent endpoint allows developers to programmatically build links to obtain consent.

admin consent endpoint

The .default scope

Did you notice the scope here is set to .default, as opposed to User.Read.All for Microsoft Graph and access_as_user for Web API? This is a built-in scope for every application that refers to the static list of permissions configured on the application registration. Basically, it bundles all the permissions in one scope. The /.default scope can be used in any OAuth 2.0 flow, but is necessary when using the v2 admin consent endpoint to request application permissions. Read about scopes usage at Scopes and permissions in the Microsoft Identity Platform.

Since both the web app and API needs to be consented by the tenant admin, the admin will need to consent twice.

  1. First, the tenant admin will consent for the Web API. The Web API is consented first as the client Web app depends on the Web API and not the other way around.
  2. Then, the code will redirect the tenant admin to consent for the client web app.

When redirected to the /adminconsent endpoint, the tenant admin will see the sign-in or the coose account screen:

redirect

After you choose an admin account, it will lead to the following prompt to consent for the Web API :

consent

When you click Accept, it will redirects to /adminconsent endpoint again to obtain consent for the Web App:

redirect

After you choose an admin account, it will lead to the Web App consent as below:

consent

Once it finishes, your applications service principals will be provisioned in the tenant admin's tenant.

Consent using PowerShell

The tenant administrators of a tenant can provision service principals for the applications in their tenant using the Azure AD Powershell Module. After installing the Azure AD Powershell Module v2, you can run the following cmdlet:

Connect-AzureAD -TenantId "[The tenant Id]"
New-AzureADServicePrincipal -AppId '<client/app id>'

If you get errors during admin consent, consider deleting the service principal of your apps in the tenant(s) you are about to test, in order to remove any previously granted consent and to be able to run the provisioning process from the beginning.

How to delete Service Principals of your apps in a tenant

Steps for deleting a service principal differs with respect to whether the principal is in the home tenant of the application or in another tenant. If it is in the home tenant, you will find the entry for the application under the App Registrations blade. If it is another tenant, you will find the entry under the Enterprise Applications blade. Read more about these blades in the How and why applications are added to Microsoft Entra ID.The screenshot below shows how to access the service principal from a home tenant:

principal1

The rest of the process is the same for both cases. In the next screen, click on Properties and then the Delete button on the upper side.

principal1

You have now deleted the service principal of Web App for that tenant. Similarly, you can delete the service principal for Web API. Next time, admin needs to provision service principal for both the applications in the tenant from which that admin belongs.

  1. Open your browser and navigate to https://localhost:44321 and sign-in using the link on top-right.
  2. Click on To-Do List, you can click on Create New link. It will redirect to create task screen where you can add a new task and assign it to any user from the list.
  3. The To-Do List screen also displays tasks that are assigned to and created by signed-in user. The user can edit and delete the created tasks but can only view the assigned tasks.

Did the sample not work for you as expected? Did you encounter issues trying this sample? Then please reach out to us using the GitHub Issues page.

Consider taking a moment to share your experience with us.

Troubleshooting

Expand for troubleshooting info

ASP.NET core applications create session cookies that represent the identity of the caller. Some Safari users using iOS 12 had issues which are described in ASP.NET Core #4467 and the Web kit bugs database Bug 188165 - iOS 12 Safari breaks ASP.NET Core 2.1 OIDC authentication.

If your web site needs to be accessed from users using iOS 12, you probably want to disable the SameSite protection, but also ensure that state changes are protected with CSRF anti-forgery mechanism. See the how to fix section of Microsoft Security Advisory: iOS12 breaks social, WSFed and OIDC logins #4647

To provide feedback on or suggest features for Microsoft Entra ID, visit User Voice page.

About the code

Expand the section ### Provisioning your Multi-tenant Apps in another Microsoft Entra tenant programmatically

Often the user-based consent will be disabled in a Microsoft Entra tenant or your application will be requesting permissions that requires a tenant-admin consent. In these scenarios, your application will need to utilize the /adminconsent endpoint to provision both the ToDoListClient and the ToDoListService before the users from that tenant are able to sign-in to your app.

When provisioning, you have to take care of the dependency in the topology where the ToDoListClient is dependent on ToDoListService. So in such a case, you would provision the ToDoListService before the ToDoListClient.

Code for the Web App (TodoListClient)

In Startup.cs, below lines of code enables Microsoft identity platform endpoint. This endpoint is capable of signing-in users both with their Work and School Accounts.

services.AddMicrosoftWebAppAuthentication(Configuration)
    .AddMicrosoftWebAppCallsWebApi(Configuration, new string[] { Configuration["TodoList:TodoListServiceScope"] })
   .AddInMemoryTokenCaches();
  1. AddMicrosoftWebAppAuthentication : This enables your application to use the Microsoft identity platform endpoint. This endpoint is capable of signing-in users both with their Work and School and Microsoft Personal accounts.
  2. AddMicrosoftWebAppCallsWebApi : Enables the web app to call the protected API ToDoList Api.
  3. AddInMemoryTokenCaches: Adds an in memory token cache provider, which will cache the Access Tokens acquired for the Web API.

The following code enables to add client service to use the HttpClient by dependency injection.

services.AddTodoListService(Configuration);

Admin Consent Endpoint

In HomeController.cs, the method AdminConsentApi has the code to redirect the user to the admin consent endpoint for the admin to consent for the Web API. The state parameter in the URI contains a link for AdminConsentClient method.

public IActionResult AdminConsentApi()
{
    string adminConsent1 = "https://login.microsoftonline.com/organizations/v2.0/adminconsent?client_id="+ _ApiClientId 
        + "&redirect_uri=" + _ApiRedirectUri
        + "&state=" + _RedirectUri + "Home/AdminConsentClient" + "&scope=" + _ApiScope;

    return Redirect(adminConsent1);
}

The method AdminConsentClient has the code to redirect the user to the admin consent endpoint for the admin to consent for the Web App.

public IActionResult AdminConsentClient()
{
    string adminConsent2 = "https://login.microsoftonline.com/organizations/v2.0/adminconsent?client_id=" + _ClientId
        + "&redirect_uri=" + _RedirectUri
        + "&state=123&scope=" + _TodoListServiceScope;

    return Redirect(adminConsent2);
}

Handle the MsalUiRequiredException from Web API

If signed-in user does not have consent for a permission on the Web API, for instance "user.read.all" in this sample, then Web API will throw MsalUiRequiredException. The response contains the details about consent Uri and proposed action.

The Web App contains a method HandleChallengeFromWebApi in ToDoListService.cs that handles the exception thrown by API. It creates a consent URI and throws a custom exception i.e., WebApiMsalUiRequiredException.

private void HandleChallengeFromWebApi(HttpResponseMessage response)
{
    //proposedAction="consent"
    List<string> result = new List<string>();
    AuthenticationHeaderValue bearer = response.Headers.WwwAuthenticate.First(v => v.Scheme == "Bearer");
    IEnumerable<string> parameters = bearer.Parameter.Split(',').Select(v => v.Trim()).ToList();
    string proposedAction = GetParameter(parameters, "proposedAction");

    if (proposedAction == "consent")
    {
        string consentUri = GetParameter(parameters, "consentUri");

        var uri = new Uri(consentUri);

        var queryString = System.Web.HttpUtility.ParseQueryString(uri.Query);
        queryString.Set("redirect_uri", _ApiRedirectUri);
        queryString.Add("prompt", "consent");
        queryString.Add("state", _RedirectUri);

        var uriBuilder = new UriBuilder(uri);
        uriBuilder.Query = queryString.ToString();
        var updateConsentUri = uriBuilder.Uri.ToString();
        result.Add("consentUri");
        result.Add(updateConsentUri);

        throw new WebApiMsalUiRequiredException(updateConsentUri);
    }
}

The following code in ToDoListController.cs catches the WebApiMsalUiRequiredException exception thrown by HandleChallengeFromWebApi method as explained above. Further it Redirects to consentUri that is retrieved from exception message. Admin needs to consent as user.read.all permission requires admin approval.

public async Task<IActionResult> Create()
{
    ToDoItem todo = new ToDoItem();
    try
    {
        ...
    }
    catch (WebApiMsalUiRequiredException ex)
    {
        return Redirect(ex.Message);
    }
}

Code for the Web API (ToDoListService)

Admin consent Client Redirect

In HomeController.cs, the method AdminConsent redirects to the URI passed in the state parameter by Web App. If admin consent is cancelled from API consent screen then it redirects to base address of Web App.

public IActionResult AdminConsent()
{
    var decodeUrl = System.Web.HttpUtility.UrlDecode(HttpContext.Request.QueryString.ToString());
    var queryString = System.Web.HttpUtility.ParseQueryString(decodeUrl);
    var clientRedirect = queryString["state"];
    if (!string.IsNullOrEmpty(clientRedirect))
    {
        if (queryString["error"] == "access_denied" && queryString["error_subcode"] == "cancel")
        {
            var clientRedirectUri = new Uri(clientRedirect);
            return Redirect(clientRedirectUri.GetLeftPart(System.UriPartial.Authority));
        }
        else
        {
            return Redirect(clientRedirect);
        }
    }
    else
    {
        return RedirectToAction("GetTodoItems", "TodoList");
    }
}

Choosing which scopes to expose

This sample exposes a delegated permission (access_as_user) that will be presented in the access token claim. The method AddMicrosoftWebApi does not validate the scope, but Microsoft.Identity.Web has a HttpContext extension method, VerifyUserHasAnyAcceptedScope, where you can validate the scope as below:

HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi);

For delegated permissions how to access scopes

If a token has delegated permission scopes, they will be in the scp or http://schemas.microsoft.com/identity/claims/scope claim.

Custom Token Validation Allowing only Registered Tenants

By marking your application as multi-tenant, your application will be able to sign-in users from any Microsoft Entra tenant out there. Now you would want to restrict the tenants you want to work with. For this, we will now extend token validation to only those Microsoft Entra tenants registered in the application database. Below, the event handler OnTokenValidated was configured to grab the tenantId from the token claims and check if it has an entry on the records. If it doesn't, an exception is thrown, canceling the authentication.

Another way to control who is allowed into API is to use Policies. This is configured as part of services.AddAuthorization call. See the code below.

//get list of allowed tenants from configuration
  var allowedTenants = Configuration.GetSection("AzureAd:AllowedTenants").Get<string[]>();

  //configure OnTokenValidated event to filter the tenants
  //you can use either this approach or the one below through policies
  services.Configure<JwtBearerOptions>(
      JwtBearerDefaults.AuthenticationScheme, options =>
      {
          var existingOnTokenValidatedHandler = options.Events.OnTokenValidated;
          options.Events.OnTokenValidated = async context =>
          {
              await existingOnTokenValidatedHandler(context);
              if (!allowedTenants.Contains(context.Principal.GetTenantId()))
              {
                  throw new UnauthorizedAccessException("This tenant is not authorized");
              }
          };
      });


  // Creating policies that wraps the authorization requirements
  services.AddAuthorization(

      //uncomment this part if you need to filter the tenants by a policy
      //refer to https://github.com/AzureAD/microsoft-identity-web/wiki/authorization-policies#filtering-tenants

      //builder =>
      //{
      //    string policyName = "User belongs to a specific tenant";
      //    builder.AddPolicy(policyName, b =>
      //    {
      //        b.RequireClaim(ClaimConstants.TenantId, allowedTenants);
      //    });
      //    builder.DefaultPolicy = builder.GetPolicy(policyName);
      //}

  );

Controlling access to API actions with scopes

During startup of Web API Application, four permissions were created:

  • 2 for user scopes: ToDoList.Read and ToDoList.ReadWrite.
  • 2 for app permissions: ToDoList.Read.All and ToDoList.ReadWrite.All It's important to note that because current sample is a multi-tenant sample, app permissions won't take effect, but are left here as an example for a single tenant samples

For enhanced and secure access we can decide what scope can access what operation. For example Read and Write scopes and permissions are required for GET:

    // GET: api/TodoItems
    [HttpGet]
    [RequiredScopeOrAppPermission(
        AcceptedScope = new string[] { _todoListReadScope, _todoListReadWriteScope },
        AcceptedAppPermission = new string[] { _todoListReadAllPermission, _todoListReadWriteAllPermission }
        )]
    public async Task<ActionResult<IEnumerable<TodoItem>>> GetTodoItems()
    {
         try
            {
                // this is a request for all ToDo list items of a certain user.
                if (!IsAppOnlyToken())
                {
                    return await _context.TodoItems.Where(x => x.TenantId == _userTenantId && (x.AssignedTo == _signedInUser || x.Assignedby == _signedInUser)).ToArrayAsync();
                }

                // Its an app calling with app permissions, so return all items across all users
                return await _context.TodoItems.Where(x => x.TenantId == _userTenantId).ToArrayAsync();
            }
            catch (Exception)
            {
                throw;
            }
    }

Write scopes and permissions will let user access POST:

    [HttpPost]
        [RequiredScopeOrAppPermission(
            AcceptedScope = new string[] { _todoListReadWriteScope },
            AcceptedAppPermission = new string[] { _todoListReadWriteAllPermission })]
        public async Task<ActionResult<TodoItem>> CreateTodoItem(TodoItem todoItem)
        {
            var random = new Random();
            todoItem.Id = random.Next();


            _context.TodoItems.Add(todoItem);
            await _context.SaveChangesAsync();

            return Ok(todoItem);
        }

How the code was created

Expand the section

The sample is based on ASP.NET CORE API template

Because there are two parts - Client and Service, you will have to create 2 separate projects under same solution.

During the project configuration, specify Microsoft Identity Platform inside Authentication Type dropdown box. As IDE installs the solution, it might require to install an additional components.

After the initial project was created, we have to continue with further configuration and tweaking. The most of configuration changes are inside Setup.cs files, so please follow with Client Setup.cs and Service Setup.cs for further details.

You will have to delete the default controllers and all relevant data from the projects and create Home and TodoList controller for bot Client and Service projects. Refer to the controller sections accordingly.

How to deploy this sample to Azure

Expand the section

Deploying web API to Azure App Services

There is one web API in this sample. To deploy it to Azure App Services, you'll need to:

  • create an Azure App Service
  • publish the projects to the App Services

⚠️ Please make sure that you have not switched on the Automatic authentication provided by App Service. It interferes the authentication code used in this code example.

Publish your files (WebApi_MultiTenant_v2)

Publish using Visual Studio

Follow the link to Publish with Visual Studio.

Publish using Visual Studio Code
  1. Install the Visual Studio Code extension Azure App Service.
  2. Follow the link to Publish with Visual Studio Code

ℹ️ When calling the web API, your app may receive an error similar to the following:

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://some-url-here. (Reason: additional information here).

If that's the case, you'll need enable cross-origin resource sharing (CORS) for you web API. Follow the steps below to do this:

  • Go to Microsoft Entra admin center, and locate the web API project that you've deployed to App Service.
  • On the API blade, select CORS. Check the box Enable Access-Control-Allow-Credentials.
  • Under Allowed origins, add the URL of your published web app that will call this web API.

Update the Microsoft Entra app registration (WebApi_MultiTenant_v2)

  1. Navigate back to to the Microsoft Entra admin center. In the left-hand navigation pane, select the Microsoft Entra ID service, and then select App registrations (Preview).
  2. In the resulting screen, select the WebApi_MultiTenant_v2 application.
  3. In the app's registration screen, select Authentication in the menu.
    • In the Redirect URIs section, update any absolute reply URLs to match the site URL of your Azure deployment. Relative URL should be left as-is. For example:
      • https://WebApi_MultiTenant_v2.azurewebsites.net/api/Home

Deploying Web app to Azure App Service

There is one web app in this sample. To deploy it to Azure App Services, you'll need to:

  • create an Azure App Service
  • publish the projects to the App Services, and
  • update its client(s) to call the website instead of the local environment.

Publish your files (WebApp_MultiTenant_v2)

Publish using Visual Studio

Follow the link to Publish with Visual Studio.

Publish using Visual Studio Code
  1. Install the Visual Studio Code extension Azure App Service.
  2. Follow the link to Publish with Visual Studio Code

Update the Microsoft Entra app registration (WebApp_MultiTenant_v2)

  1. Navigate back to to the Microsoft Entra admin center. In the left-hand navigation pane, select the Microsoft Entra ID service, and then select App registrations (Preview).
  2. In the resulting screen, select the WebApp_MultiTenant_v2 application.
  3. In the app's registration screen, select Authentication in the menu.
    1. In the Redirect URIs section, update the reply URLs to match the site URL of your Azure deployment. For example:
      1. https://WebApp_MultiTenant_v2.azurewebsites.net/
      2. https://WebApp_MultiTenant_v2.azurewebsites.net/signin-oidc
    2. Update the Front-channel logout URL fields with the address of your service, for example https://WebApp_MultiTenant_v2.azurewebsites.net

Update authentication configuration parameters (WebApp_MultiTenant_v2)

  1. In your IDE, locate the WebApp_MultiTenant_v2 project. Then, open ToDoListClient\appsettings.json.
  2. Find the key for redirect URI and replace its value with the address of the web app you published, for example, https://WebApp_MultiTenant_v2.azurewebsites.net/redirect.
  3. Find the key for web API endpoint and replace its value with the address of the web API you published, for example, https://WebApi_MultiTenant_v2.azurewebsites.net/api.

⚠️ If your app is using an in-memory storage, Azure App Services will spin down your web site if it is inactive, and any records that your app was keeping will be empty. In addition, if you increase the instance count of your website, requests will be distributed among the instances. Your app's records, therefore, will not be the same on each instance.

Next Steps

Learn how to:

Contributing

If you'd like to contribute to this sample, see CONTRIBUTING.MD.

This project has adopted the Microsoft Open Source Code of Conduct. For more information, see the Code of Conduct FAQ or contact opencode@microsoft.com with any additional questions or comments.

Learn More