In the previous post we have seen how to build a web application with Blazor to manage a Viva Learning custom provider with its content. The original version of the application, however, has a limitation: it works only in single-tenant mode. This is the scenario in which you're an enterprise who wants to bring their own learning content and make it available to the employees of the company.
What if, instead, you're a learning provider and you want to make your content available to multiple companies? The current architecture of the Blazor application would make it challenging for two reasons:
- You would need to ask to everyone of your customers to create an application on their Azure AD tenant and share the credentials with you.
- You would need to manage multiple instances of the Blazor app, one for each customer you need to support, since each of them will have its own different Azure AD configuration.
In this post, we're going to make a few simple changes to make our Blazor application multi-tenant ready, so that we'll be able to manage from a single app multiple customers. Most of all, we won't need to ask to each customer to create an Azure AD app on their tenant, but they will just need to approve it.
Let's start!
Change the Azure AD app configuration
When we created the Azure AD app in the first post of the series, we configured it to be single tenant. We were able to use it only to perform operations against the tenant where we created the Azure AD app.
To support multi tenant scenarios, we just need to go back to the Azure portal, login with our work account and, in the App registration section under Active Directory, open the app we have previously created. Then click on Authentication and move to the Supported account types section. Change the configuration from Accounts in this organizational directory only (Single tenant) to Accounts in any organizational directory (Any Azure AD directory - Multitenant), as in the following image:
That's it, this is all we must do from the Azure AD side. Now let's move to the Blazor application.
Adapting the Blazor application: the authentication code flow
The first flow we're going to fix is the authentication code one, the one which we use to perform Microsoft Graph operations with delegated permissions and operate on behalf of the logged user. This is the authentication type we need to work with the custom learning providers.
The first operation we must make is to change the Azure AD configuration we have stored in the appsettings.json
file. Inside the file, we have a section called AzureAD
configured like this:
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "M365x68347967.onmicrosoft.com",
"TenantId": "6491d737-d0bd-4f5f-a59d-8bf64f1efc71",
"ClientId": "dd6196e2-bc28-4ad6-b71d-c47049dc58ae",
"CallbackPath": "/signin-oidc",
"ClientSecret": "Client secret from app-registration. Check user secrets/azure portal.",
"ClientCertificates": []
},
With this configuration, we are specifying an explicit Tenant ID. To make our flow multi-tenant, we must change the TenantId
property to common
, as in the following example:
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "M365x68347967.onmicrosoft.com",
"TenantId": "common",
"ClientId": "dd6196e2-bc28-4ad6-b71d-c47049dc58ae",
"CallbackPath": "/signin-oidc",
"ClientSecret": "Client secret from app-registration. Check user secrets/azure portal.",
"ClientCertificates": []
},
That's all we need. Before testing it, however, we need an administrator of the tenant we want to connect to authorize our application, since we're using some Microsoft Graph scopes that require administrator approval. The easiest way to do this is to ask to an admin of the tenant to open in the browser the following URL:
https://login.microsoftonline.com/organizations/v2.0/adminconsent?client_id=<your client id>&scope=https://graph.microsoft.com/.default
You must replace <your client id>
with the Client Id of your Azure AD application, so the same one you have in the appsettings.json
.
The administrator will be asked to login with their work account and then to approve the permissions required for our application. As you can see from the below image, the prompt lists all the permissions we're using to work with Viva Learning:
Once you have done that, you can now press F5 in Visual Studio and launch the debugger. You should be able to open your browser on the URL https://localhost:7073/
and login with any work account, not just the one from your tenant, but from any tenant.
Adapting the Blazor application: the client credentials flow
You'll remember that in our Blazor application we're using two different implementations of the Microsoft Graph client: one authenticated with delegated permissions, to work with learning providers, and one with applications permissions, to work with the learning content. The second authentication type requires a few changes. The first one to be made is in the GraphServiceClient
class we have built over the course of the previous post. We need to change the AcquireAccessTokenAsync()
method to accept the tenant id as external parameter. We can't use any more the tenant id of our own tenant and we can't use the new one we have set in the appsettings.json
file, common
, because it isn't supported in a client credentials flow scenario. Since we don't have a user logging in, we must specify which is the tenant we want to operate on. This is the new definition of the method:
public async Task AcquireAccessTokenAsync(string tenantId)
{
var scopes = new string[] { ".default" };
var aadConfig = configuration.GetSection("AzureAd");
var client = ConfidentialClientApplicationBuilder.Create(aadConfig["ClientId"])
.WithTenantId(tenantId)
.WithClientSecret(aadConfig["ClientSecret"]).Build();
var token = await client.AcquireTokenForClient(scopes).ExecuteAsync();
accessToken = token.AccessToken;
var authProvider = new DelegateAuthenticationProvider(async (request) =>
{
request.Headers.Authorization =
new AuthenticationHeaderValue("Bearer", accessToken);
});
applicationClient = new GraphServiceClient(authProvider);
}
As you can see, we're accepting tenantId
as parameter of the method and we're using it when we set up the authentication client with the ConfidentialClientApplicationBuilder
class. Remember also to update the IGraphServiceClient
interface to reflect the new parameter.
We have completed the changes on the authentication side. Now we need to make a few ones on the UI side.
Adapting the Blazor application: the user interface
This new multi-tenant application requires a bit of a change in the UI of our Razor component which displays the custom learning provider with its content. In the previous implementation, we were using the OnInitializedAsync()
method of the component to automatically load, at startup, our custom learning provider with its articles. In this new scenario, however, we don't have any more a fixed tenant id, so we can't load data automatically. We need to ask first the user the tenant id he wants to work with, and then load the data.
First, let's go to our LearningProviders
Razor component and let's add at the top, before the two tables, the following snippet:
<p>
<strong>Tenant Id of the customer</strong>
<input type="text" @bind="tenantId" />
<button @onclick="LoadProvider">Load provider</button>
</p>
We have added a text input, which will collect the tenant id, and a button to trigger the action.
Now let's move to the @code
section and let's make a few changes. First, we need to add a new property called tenantId
:
@code {
string tenantId;
}
As you can see, this property is connected to the input
control we have added to the component through the @bind
property. This means that, every time the user types something in the box, we'll get the text in the property. Now let's implement the LoadProvider()
method, which is invoked when the user presses the Load provider button. We're going to move the whole content of the OnInitializedAsync()
inside this method:
public async Task LoadProvider()
{
if (!string.IsNullOrEmpty(tenantId))
{
await graphService.AcquireAccessTokenAsync(tenantId);
providers = await graphService.GetLearningProvidersAsync();
if (providers != null && providers.Count > 0)
{
contents = await graphService.GetLearningContentAsync(providers?.FirstOrDefault().Id);
}
}
}
When the user presses the button, we're going to perform all the operations that previously we did at startup: we acquire the access token for the tenant specified in the text input and, once we have an authenticated client with this token, we use it get the custom learning provider and its contents.
That's it! Now you can verify that everything is working by pressing F5, launching the browser and:
- Login with an account from the tenant you want to manage (remember that an administrator must approve the application first by invoking the consent URL).
- Go to the page which hosts the learning providers component.
- Paste the id of the tenant you're targeting and press Load provider.
- You should see showing up in the tables the custom learning provider with its content.
The solution you can find on GitHub has an additional feature: in case the customer's tenant doesn't have a learning provider yet, it gives you the option to create one, by redirecting to a component called NewLearningProvider
. We won't discuss its implementation in details in this post, since there's nothing special to highlight: it uses the same APIs that we have discussed in the first post of the series.
Wrapping up
In this article, we have evolved our Blazor application to support multi-tenant scenarios, so that you can manage your custom learning content for multiple customers from a single application. We have learned the changes we must make in our AAD application and in the Blazor app to turn the single tenant implementation into a multi-tenant one. The biggest advantage of this approach is that we won't need to ask every customer we have to manually create an app registration on their tenant, but they can simply approve the one we have on our tenant.
You can find the full source code on GitHub.
Happy coding!