Secure a backend web API in a multitenant application

 Sample code The Tailspin Surveys application uses a backend web API to manage CRUD operations on surveys. 

Source: Secure a backend web API in a multitenant application

For example, when a user clicks “My Surveys”, the web application sends an HTTP request to the web API:

GET /users/{userId}/surveys

The web API returns a JSON object:

{
  "Published":[],
  "Own":[
    {"Id":1,"Title":"Survey 1"},
    {"Id":3,"Title":"Survey 3"},
    ],
  "Contribute": [{"Id":8,"Title":"My survey"}]
}

The web API does not allow anonymous requests, so the web app must authenticate itself using OAuth 2 bearer tokens.

Note

This is a server-to-server scenario. The application does not make any AJAX calls to the API from the browser client.

There are two main approaches you can take:

  • Delegated user identity. The web application authenticates with the user's identity.
  • Application identity. The web application authenticates with its client ID, using OAuth 2 client credential flow.

The Tailspin application implements delegated user identity. Here are the main differences:

Delegated user identity:

  • The bearer token sent to the web API contains the user identity.
  • The web API makes authorization decisions based on the user identity.
  • The web application needs to handle 403 (Forbidden) errors from the web API, if the user is not authorized to perform an action.
  • Typically, the web application still makes some authorization decisions that affect UI, such as showing or hiding UI elements).
  • The web API can potentially be used by untrusted clients, such as a JavaScript application or a native client application.

Application identity:

  • The web API does not get information about the user.
  • The web API cannot perform any authorization based on the user identity. All authorization decisions are made by the web application.
  • The web API cannot be used by an untrusted client (JavaScript or native client application).
  • This approach may be somewhat simpler to implement, because there is no authorization logic in the Web API.

In either approach, the web application must get an access token, which is the credential needed to call the web API.

  • For delegated user identity, the token has to come from an identity provider (IDP), such as Azure Active Directory, which can issue a token on behalf of the user.
  • For client credentials, an application might get the token from the IDP or host its own token server. (But don't write a token server from scratch; use a well-tested framework like IdentityServer4.) If you authenticate with Azure AD, it's strongly recommended to get the access token from Azure AD, even with client credential flow.

The rest of this article assumes the application is authenticating with Azure AD.

Getting the access token

A diagram that shows the web application requesting an access token from Azure AD and sending the token to the web API.

Register the web API in Azure AD

In order for Azure AD to issue a bearer token for the web API, you need to configure some things in Azure AD.

  1. Register the web API in Azure AD.
  2. Add the client ID of the web app to the web API application manifest, in the knownClientApplications property. See the GitHub readme for more information.
  3. Give the web application permission to call the web API. In the Azure portal, you can set two types of permissions: “Application Permissions” for application identity (client credential flow), or “Delegated Permissions” for delegated user identity.Delegated permissionsA screenshot of the Azure portal that shows the application permissions and delegated permissions.

Getting an access token

Note

This set of articles and the accompanying reference implementation include & use non-current tools, libraries, and technologies; but the concepts described are still relevant.

Before calling the web API, the web application gets an access token from Azure AD. In a .NET application, use the Azure AD Authentication Library (ADAL) for .NET.

In the OAuth 2 authorization code flow, the application exchanges an authorization code for an access token. The following code uses ADAL to get the access token. This code is called during the AuthorizationCodeReceived event.

// The OpenID Connect middleware sends this event when it gets the authorization code.
public override async Task AuthorizationCodeReceived(AuthorizationCodeReceivedContext context)
{
    string authorizationCode = context.ProtocolMessage.Code;
    string authority = "https://login.microsoftonline.com/" + tenantID
    string resourceID = "https://tailspin.onmicrosoft.com/surveys.webapi" // App ID URI
    ClientCredential credential = new ClientCredential(clientId, clientSecret);

    AuthenticationContext authContext = new AuthenticationContext(authority, tokenCache);
    AuthenticationResult authResult = await authContext.AcquireTokenByAuthorizationCodeAsync(
        authorizationCode, new Uri(redirectUri), credential, resourceID);

    // If successful, the token is in authResult.AccessToken
}

Here are the various parameters that are needed:

  • authority. Derived from the tenant ID of the signed in user. (Not the tenant ID of the SaaS provider)
  • authorizationCode. the auth code that you got back from the IDP.
  • clientId. The web application's client ID.
  • clientSecret. The web application's client secret.
  • redirectUri. The redirect URI that you set for OpenID Connect. This is where the IDP calls back with the token.
  • resourceID. The App ID URI of the web API, which you created when you registered the web API in Azure AD
  • tokenCache. An object that caches the access tokens. See Token caching.

If AcquireTokenByAuthorizationCodeAsync succeeds, ADAL caches the token. Later, you can get the token from the cache by calling AcquireTokenSilentAsync:

AuthenticationContext authContext = new AuthenticationContext(authority, tokenCache);
var result = await authContext.AcquireTokenSilentAsync(resourceID, credential, new UserIdentifier(userId, UserIdentifierType.UniqueId));

where userId is the user's object ID, which is found in the Original Post>

Using the access token to call the web API

Once you have the token, send it in the Authorization header of the HTTP requests to the web API.

Authorization: Bearer xxxxxxxxxx

The following extension method from the Surveys application sets the Authorization header on an HTTP request, using the HttpClient class.

public static async Task<HttpResponseMessage> SendRequestWithBearerTokenAsync(this HttpClient httpClient, HttpMethod method, string path, object requestBody, string accessToken, CancellationToken ct)
{
    var request = new HttpRequestMessage(method, path);
    if (requestBody != null)
    {
        var json = JsonConvert.SerializeObject(requestBody, Formatting.None);
        var content = new StringContent(json, Encoding.UTF8, "application/json");
        request.Content = content;
    }

    request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
    request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

    var response = await httpClient.SendAsync(request, ct);
    return response;
}

Authenticating in the web API

The web API has to authenticate the bearer token. In ASP.NET Core, you can use the Microsoft.AspNet.Authentication.JwtBearer package. This package provides middleware that enables the application to receive OpenID Connect bearer tokens.

Bestseller No. 1
Mybaby Automatic Soap Dispenser, 1.1 Pound
  • Works With All Liquid Hand Soaps
  • Plays 20 Second Song That Teaches Children How To...
  • Motion Sensor Technology Provides Sanitary,...
  • Magnetically Attached Drip Tray Removes Easily For...
  • Self-cleaning Function Prevents Clogs And Mess
SaleBestseller No. 2
Asterom Walking Cane - Handmade Wolf Cane - Cool Walking Canes for Men and Women - Wooden, Carved, Unique - Walking Sticks for Men & Seniors (36 Inch)
  • STYLISH CANE FROM THE RED CARPET TO YOUR HOME! Our...
  • 100% SATISFACTION GUARANTEED! Buy with confidence,...
  • EXCEPTIONAL COMFORT DUE TO ERGONOMIC HANDLE. The...
  • CHOOSE THE CORRECT LENGTH TO AVOID SPINE PAIN....
  • WIDE QUAD CANE TIP COMPATIBLE. Need extra...

Register the middleware in your web API Startup class.

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ApplicationDbContext dbContext, ILoggerFactory loggerFactory)
{
    // ...

    app.UseJwtBearerAuthentication(new JwtBearerOptions {
        Audience = configOptions.AzureAd.WebApiResourceId,
        Authority = Constants.AuthEndpointPrefix,
        TokenValidationParameters = new TokenValidationParameters {
            ValidateIssuer = false
        },
        Events= new SurveysJwtBearerEvents(loggerFactory.CreateLogger<SurveysJwtBearerEvents>())
    });

    // ...
}
  • Audience. Set this to the App ID URL for the web API, which you created when you registered the web API with Azure AD.
  • Authority. For a multitenant application, set this to https://login.microsoftonline.com/common/.
  • TokenValidationParameters. For a multitenant application, set ValidateIssuer to false. That means the application will validate the issuer.
  • Events is a class that derives from JwtBearerEvents.

Issuer validation

Validate the token issuer in the JwtBearerEvents.TokenValidated event. The issuer is sent in the “iss” claim.

In the Surveys application, the web API doesn't handle tenant sign-up. Therefore, it just checks if the issuer is already in the application database. If not, it throws an exception, which causes authentication to fail.

public override async Task TokenValidated(TokenValidatedContext context)
{
    var principal = context.Ticket.Principal;
    var tenantManager = context.HttpContext.RequestServices.GetService<TenantManager>();
    var userManager = context.HttpContext.RequestServices.GetService<UserManager>();
    var issuerValue = principal.GetIssuerValue();
    var tenant = await tenantManager.FindByIssuerValueAsync(issuerValue);

    if (tenant == null)
    {
        // The caller was not from a trusted issuer. Throw to block the authentication flow.
        throw new SecurityTokenValidationException();
    }

    var identity = principal.Identities.First();

    // Add new claim for survey_userid
    var registeredUser = await userManager.FindByObjectIdentifier(principal.GetObjectIdentifierValue());
    identity.AddClaim(new Claim(SurveyClaimTypes.SurveyUserIdClaimType, registeredUser.Id.ToString()));
    identity.AddClaim(new Claim(SurveyClaimTypes.SurveyTenantIdClaimType, registeredUser.TenantId.ToString()));

    // Add new claim for Email
    var email = principal.FindFirst(ClaimTypes.Upn)?.Value;
    if (!string.IsNullOrWhiteSpace(email))
    {
        identity.AddClaim(new Claim(ClaimTypes.Email, email));
    }
}

As this example shows, you can also use the TokenValidated event to modify the claims. Remember that the claims come directly from Azure AD. If the web application modifies the claims that it gets, those changes won't show up in the bearer token that the web API receives. For more information, see Claims transformations.

Authorization

For a general discussion of authorization, see Role-based and resource-based authorization.

The JwtBearer middleware handles the authorization responses. For example, to restrict a controller action to authenticated users, use the [Authorize] attribute and specify JwtBearerDefaults.AuthenticationScheme as the authentication scheme:

[Authorize(ActiveAuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]

This returns a 401 status code if the user is not authenticated.

To restrict a controller action by authorization policy, specify the policy name in the [Authorize] attribute:

[Authorize(Policy = PolicyNames.RequireSurveyCreator)]

This returns a 401 status code if the user is not authenticated, and 403 if the user is authenticated but not authorized. Register the policy on startup:

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthorization(options =>
    {
        options.AddPolicy(PolicyNames.RequireSurveyCreator,
            policy =>
            {
                policy.AddRequirements(new SurveyCreatorRequirement());
                policy.RequireAuthenticatedUser(); // Adds DenyAnonymousAuthorizationRequirement
                policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme);
            });
        options.AddPolicy(PolicyNames.RequireSurveyAdmin,
            policy =>
            {
                policy.AddRequirements(new SurveyAdminRequirement());
                policy.RequireAuthenticatedUser(); // Adds DenyAnonymousAuthorizationRequirement
                policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme);
            });
    });

    // ...
}

Protecting application secrets

It's common to have application settings that are sensitive and must be protected, such as:

  • Database connection strings
  • Passwords
  • Cryptographic keys

As a security best practice, you should never store these secrets in source control. It's too easy for them to leak — even if your source code repository is private. And it's not just about keeping secrets from the general public. On larger projects, you might want to restrict which developers and operators can access the production secrets. (Settings for test or development environments are different.)

A more secure option is to store these secrets in Azure Key Vault. Key Vault is a cloud-hosted service for managing cryptographic keys and other secrets. This article shows how to use Key Vault to store configuration settings for your app.

In the Tailspin Surveys application, the following settings are secret:

  • The database connection string.
  • The Redis connection string.
  • The client secret for the web application.
New
Rose Quartz Agate | Serving Tray with Brass Handles | Circular (Gold - Finish), Diameter(12 inch)
  • The natural look of agate stone creates a unique...
  • The edges of each piece are electroplated (NOT...
  • Handles are solid brass.
  • Available in 12", 14" and 16" diameter
  • Can do silver, rose gold, or gold plating for...
New
AANTHROPOLOGY By Rhea White Crystal Agate Cheese Platter/Tray | Circular (Gold - Finish), Diameter(12 inches)
  • The natural look of agate stone creates a unique...
  • The edges of each piece are electroplated (NOT...
  • Handles are solid brass.
  • Available in 12", 14" and 16" diameter
  • Can do silver, rose gold, or gold plating for...
New
Magenta Agate Aventurine Set of 4 Large Coasters/Sign Boards (Rose Gold - Finish)
  • Sold as a set of 4 pieces
  • 4" x 4"
  • 10 - 12 mm thick
  • Natural stone will vary in size, color, and...
  • Each piece has 4 rubber pads to avoid scratching...

The Surveys application loads configuration settings from the following places:

  • The appsettings.json file
  • The user secrets store (development environment only; for testing)
  • The hosting environment (app settings in Azure web apps)
  • Key Vault (when enabled)

Each of these overrides the previous one, so any settings stored in Key Vault take precedence.

Note

By default, the Key Vault configuration provider is disabled. It's not needed for running the application locally. You would enable it in a production deployment.

At startup, the application reads settings from every registered configuration provider, and uses them to populate a strongly typed options object. For more information, see Using Options and configuration objects.