Multi-Tenant Daemons with Microsoft Identity platform

Background

I have recently speaking to a customer about the following scenario. Customer has an API that supposed to be consumed by some **trusted** daemon applications built and managed by their partners. It’s not the interactive users (no signed in users via an web application) who will be using the API, rather some background process that would periodically consume the API (as a system account). Most of the partners have their own Identity platform (mostly on Azure AD).

First thought – Single tenant API

The first attempt was made with a confidential client flow (aka Client Credentials flow). The idea was, API publisher will:

  • publish their API
  • Register an application in the publisher Azure AD to represent the API (as OpenID resource)
  • Register another app to represent consumers (as OpenID Audience).
  • Create one or more client secrets for the audience app-registration and distribute them to the consumer daemon applications.

Soon it turned out, it’s quite a hassle to generate number of secrets (or client certificates) and share them in a secure way to the consumers (partner organizations) and rotate them frequently. Also, the tokens that are used in this method, represent the same identity for all the partners, therefore, custom metadata needed to be introduced (or interpret certificate’s data) to identify the consumer identity.

That led us thinking of a solution where consumers are in control of creating their application identity in their tenant (consumer tenant) and API maintains a whitelist of consumer tenants (read Valid Issuers) and allows JWT (Json Web Tokens) issued by those trusted STS (valid issuers).

let me explain how we did that.

Multi-Tenant API

Let’s take a look into a diagram that shows the flows.

I will be using .net to demonstrate the API implementation, however, I will not use the MSAL or other higher-order libraries that hides the authentication requests, so we can indeed see the actual requests are being made. Thus, also can be implemented the same way in other languages.

We will start by registering an application on Publisher Azure AD. This app will represent our API. From Azure portal you can create an application, make sure you select “Multi-tenant” for supported account types.

Redirect URIs are options in our case, as we will be using Daemon applications. So keep it like below:

After creating the application, navigate to the Authentication tab and check the Access Token grant.

Finally take note of the application ID URI of the application.

Let’s now build a simplest API (single file top-level program) in .net 5 that exposes a single endpoint and accepts only tokens issued by a list (I will be using 2 separate Azure AD) of trusted Azure AD from partners.

Here’s how the API looks like:

const string azureAdCommonDiscoveryEndpoint = 
         "https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration";
OpenIdConnectConfiguration config = default(OpenIdConnectConfiguration);

List<string> validIssuers = new List<string>
{
    "https://sts.windows.net/11111111-1111-1111-1111-1111111111111/",  // Partner 1 Azure AD tenant ID
    "https://sts.windows.net/22222222-2222-2222-2222-2222222222222/" // Partner 2 Azure AD tenant
};
const string audience = "api://090fc198-1111-1111-1111-1111111111111"; // We got this in earlier step in App registration

Next we will setup a simple API endpoint like below:

Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.Configure(app =>
            {
                app.UseRouting();
                app.UseEndpoints(route =>
                {
                    route.MapGet("/", async context => 
                    {   
                          // API Code
                    });
                });
            });
        })
        .Build().Run();

Now we have the endpoint (/) configured, let’s fill that implementation with the following code:

try
{
    var authHeader = $"{context.Request.Headers["Authorization"]}";
    if (!string.IsNullOrWhiteSpace(authHeader))
    {
        string accessToken = authHeader.Substring(7);
        if (config == null)
        {
            var configManager = new ConfigurationManager<OpenIdConnectConfiguration>(azureAdCommonDiscoveryEndpoint, new OpenIdConnectConfigurationRetriever());
            config = await configManager.GetConfigurationAsync();
        }
        var claims = new JwtSecurityTokenHandler()
            .ValidateToken(accessToken,
            new TokenValidationParameters
            {
                ValidIssuers = validIssuers,
                ValidAudience = audience,
                ValidateAudience = true,
                ValidateIssuer = true,
                IssuerSigningKey = config.SigningKeys.FirstOrDefault(),
                ValidateLifetime = true
            }, out var jwt);
        await context.Response.WriteAsJsonAsync<JwtSecurityToken>(jwt as JwtSecurityToken, options);
        return;
    }
}
catch { }
context.Response.StatusCode = 401;

As you can see, we are simply validating the signature of provided JWT with the selected number of issuers (white-listed issuers), the target audience of the JWT and the token lifetime.

We can run this application now either locally or host it on Azure (App service, Functions whatever you like). And finally, we will share the App ID URI (e.g. api://090fc198-1111-1111-1111-1111111111111) to our consumer developers.

Build Daemon applications – API consumer

We will be assuming that the partners have their own Azure AD. (Other Identity provider would also work with a bit of code change, as the design is based on OAuth 2.0/Open ID.)

We will have to create an application (app registration blade from Azure AD) in consumer AD (a separate Azure AD tenant than the API publisher). This could be single-tenant application. Once created, take note the Application (client) ID, create a secret (or certificate – but I will be demonstrating the secret-based flow only).

Now we can collect a token with these client credentials. I am using VSCode Rest client to collect tokens and here’s the request look like:

# Defining the variables
@host = login.microsoftonline.com
@tenant = 2222222-2222-2222-2222-22222222222222
@contentType = application/x-www-form-urlencoded
@clientID = 7ce5b2d8-0000-0000-0000-0000000000000
@targetScopeAppID = 090fc198-1111-1111-1111-1111111111111  # This is the App ID URI from the publisher tenant
@encodedScope = api%3A%2F%2F{{targetScopeAppID}}%2F.default
@clientSecret = ***************

@apihost = your-azure-app.azurewebsites.net # Or use localhost if you like, @apihost = localhost:5001

With these variables, we can issue the first request to grab a token from Azure AD (Consumer tenant):

# @name getToken
POST https://{{host}}/{{tenant}}/oauth2/v2.0/token HTTP/1.1
content-type: {{contentType}}

client_id={{clientID}}&scope={{encodedScope}}&client_secret={{clientSecret}}&grant_type=client_credentials

The above request will return us a token (issued by consumer AD) and with that, we can now issue API call to target API server:

GET https://{{apihost}}/
Authorization: {{getToken.response.body.token_type}} {{getToken.response.body.access_token}}

This would be authenticated and our publisher API will reply back with the validated JWT to demonstrate that the token was validated successfully.

Azure API management

In above example, the API was written with .net/C#, a bunch of codes we have written to manually validate the JWT. Which is great for exploring and learning what’s going on under the hood. However, you want to minimize these code (separation of concerns) when you are about to go live. Azure API management provides a handful access-restriction policies, one of them is JWT validation – that we can leverage here to get rid of our token validation codes. In this section we will see how we can achieve that.

Let’s assume we have an API hosted on Azure Function, Azure app service or even a Logic App. The API guarded with an Azure API management (as a gateway) and token validations are taken care of at gateway level.

In this model, you need to ensure all traffics flows through the API gateway only, either via network level protections or at least IP whitelisting to prevent traffic directly reach to the API endpoint, hence bypass the token validation completely. We will not explain how to do that securely, as that goes beyond this article.

We will add the following policy to our API management for token validation:

<policies>
    <inbound>
        <base />
        <validate-jwt header-name="Authorization" failed-validation-httpcode="401" failed-validation-error-message="No valid bearer token provided" require-expiration-time="true" require-scheme="Bearer" require-signed-tokens="true" clock-skew="10" output-token-variable-name="jwt">
            <openid-config url="https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration" />
            <audiences>
                <audience>api://090fc198-1111-1111-1111-1111111111111</audience>
            </audiences>
            <issuers>
                <issuer>https://sts.windows.net/9d22bb6f-1111-1111-1111-1111111111111/</issuer>
                <issuer>https://sts.windows.net/cac2cc32-2222-2222-2222-2222222222222/</issuer>
            </issuers>
        </validate-jwt>
    </inbound>
    <backend>
        <base />
    </backend>
    <outbound>
        <base />
    </outbound>
    <on-error>
        <base />
    </on-error>
</policies>

That’s it, the above policy will do exactly what we have done before in .net code. You can see that we have added our whitelisted (trusted partner) Azure AD as valid issuers of tokens.

Conclusion

If you are interested, you can see the code samples (snippets above) in this GitHub repo. If you have thoughts/alternative ideas, I am glad to hear them.

Thanks for reading.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s