11 minute read

Since Microsoft's Azure AD got the Business-to-Business (B2B) functionality, it has enabled a broad variety of new scenarios to be developed. It for example makes sharing various resources and information within applications much more easier. Today we are going to investigate the way to build an application which is not only a multi-tenant one, but also supports the user to be member of multiple directories.

Say we have a following scenario: Company A and Company B are both provisioned for the Application (just a sample application, a practical use case will be published soon as a commercial solution) which is multi-tenant. The Application allows users to collaborate on projects and share some sort of information within a team which is represented by an Office 365 Group. After some time, company A requires an employee from company B to work on the same project, so they are added to the Office 365 Group (provisioned as B2B guest), which allows them to access data stored on SharePoint, conversation in e-mail and also Microsoft Teams in future. However, the guest user can only see the data of his home organization - company B.

Obtaining list of organizations the guest user is member of

The first example of how this should work which came to my mind was the Azure Portal and Microsoft's My Apps portal. All these portals allow the user to switch the tenant once they get signed in. Both these portals call their own API in order to obtain these information. You can find out more at this Stack Overflow thread.

Philippe found an API available and supported by Microsoft - https://management.azure.com/tenants, which returns the tenantIds of those, which you are member of. Unfortunately, you are unable to obtain the tenant's name like from the endpoint below, so in order to get the name you would probably have to query graph on behalf of each directory and get its name which is too complicated in my opinion.

We are going to go with calling the Azure Portal's endpoint: https://portal.azure.com/AzureHubs/api/tenants/List - please note, like I already mentioned in the Stack Overflow's thread, it is an internal endpoint which isn't really documented or supported by Microsoft publicly, so you shouldn't use it in production applications. In order to call it, we have to authenticate (using ADAL) with Azure Service Management API (in preview) and then send HTTP POST request to the endpoint. It is then going to return data in following format:

{
    "failure": false,
    "tenants": [
        {
            "id": "67266d43-8de7-494d-9ed8-3d1bd3b3a764",
            "domainName": "thenetw.org",
            "displayName": "TheNetw.org s.r.o.",
            "isSignedInTenant": false
        },
        {
            "id": "40c29545-8bca-4f51-8689-48e6819200d2",
            "domainName": "hajekj.net",
            "displayName": "hajekj",
            "isSignedInTenant": true
        },
        {
            "id": "8026f5b9-c636-4dc9-9ad1-bb30a080c651",
            "domainName": "b2c.hajekj.net",
            "displayName": "hajekj-b2c",
            "isSignedInTenant": false
        }
    ]
}

Now once we are able to list all the tenants the user is member of, we can now easily integrate this into our application. We now would go and create a similar selector to the one in Azure Portal.

Switching between tenants

Since the application is multi-tenant by nature, each organization should perform a sign-up (to grant the admin consent if needed, create the application's instance etc.). Each information which we store organization-wide should contain a reference to the organization's tenant, preferably the organization tenant's ID (a GUID).

In order to distinguish, which user belongs to which organization, we then use the tid claim from the id_token issued by AAD (it is accessible in the ASP.NET Identity). Please don't use the organization's domain name (and if so, use the *.onmicrosoft.com one, since domains can change over the time, but the tenant id will stay).

A standard multi-tenant application redirects the user to the common endpoint, signs the user in and then validates the id_token's issuer, in this case, the tenant's id will always be the user's home tenant.

So how do we force the AAD to switch the tenant? We instruct the AAD to sign the user in with the correct tenant instead of the common endpoint! After that, the token will be issued by the correct tenant and we will be able to access the application (and also the directory, Microsoft Graph, etc.) of company A as a user from company B.

Code sample

So in order to show you a working sample, instead of just writing about it, I made a sample which is available on GitHub. I will walk you through some of the code parts.

Registering the application

The very first thing which you need to do is register the application with Azure Active Directory. After that, you need to assign it permissions - for this specific sample, you need to assign permissions for Windows Azure Service Management API and also Microsoft Graph (sign in and view user's profile, read all user's basic profiles).

Little bit more detailed information about registering the application is available in README of the project.

Listing user's tenants

After we sign the user in, we need to obtain a token for the Azure Service Management API and query it for the tenant information, we parse the JSON returned into a model and then list all the available tenants. A note here - you should only display the tenants which have provisioned your application and granted the consent, otherwise the user will face errors during the sign-in, since the application is not going to be in the directory.

Code for obtaining the tenant list is here, I don't think it requires any comments.

Redirecting to the correct tenant

By default, we sign in the user through the common endpoint. Later, if the user decides to switch the tenant, we call the SignInAsync again, but we pass in the tenant's id.

public async Task TenantSelect(Guid id)
{
    var state = new Dictionary<string, string> { { "tenantId", id.ToString() } };
    await HttpContext.Authentication.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties(state)
    {
        RedirectUri = Url.Action("TenantSelection", "Home")
    }, ChallengeBehavior.Unauthorized);
}

After that, we catch the tenant id in the OnRedirectToIdentityProvider event and adjust the redirect URL accordingly. We also save the tenant id into the state which is passed along in the request, so that when the user is returned into the application. The state is encrypted so it cannot be tampered with.

OnRedirectToIdentityProvider = (context) =>
{
    string tenantId;
context.Properties.Items.TryGetValue("tenantId", out tenantId);
    if(tenantId != null)
    {
        context.ProtocolMessage.IssuerAddress = context.ProtocolMessage.IssuerAddress.Replace("common", tenantId);
    }
    return Task.FromResult(0);
},

Accepting the response from AAD

Like already mentioned above, we then have to handle the token validation on our own since the application is multi-tenant. We are going to pull the tenant id from the authentication token (or tid from the id_token, upon validating its signature) and compare it the desired issuer which we saved into the state. If the state is empty, we treat it as if the user has signed in through the common endpoint, so we only validate whether the organization exists.

OnTokenValidated = (context) =>
{
    string tenantId;
context.Properties.Items.TryGetValue("tenantId", out tenantId);
    if(tenantId != null)
    {
        string userTenantId = context.Ticket.Principal.FindFirst(AzureAdClaimTypes.TenantId)?.Value;
        if (userTenantId != tenantId)
        {
            throw new Exception($"You signed in with wrong tenant, expected: {tenantId} but got {userTenantId}");
        }
    }
    // You would validate whether the organization exists in the system etc.
    return Task.FromResult(0);
}

Beware of Token Cache

When you use ADAL for authentication, you need to have some sort of Token Cache to persist the tokens and be able to request tokens for other services. The token cache samples usually use user's object id as cache identifier, which won't do any good when you switch tenants. You either need to change the identifier to also contain the tenant (eg. tenantId_userId) or for example in this example's case, when storing tokens in the Session, you simply separate them by tenantId.

Summary

And we are done. The user can now use the application as B2B guest with having the option to choose between the tenants which they want to use the application as.

Yet there is another scenario which comes to my mind - what if organization B didn't have the multi-tenant application provisioned? You would then need to sign the user in from the organization B and prompt him for tenant selection. It could require the need for the admin consent to be granted on behalf of organization B if you require some special permissions for Graph or other APIs (and if user permissions are not enough), however you would just bounce all the other users away with a tenant selection prompt which would be empty in their case since they wouldn't have access to any of the organization's instances.

For single tenant applications, B2B is going to work just fine since all the users are signed in directly with the correct Azure AD instance which then gives you correct token.

Alternative approach to listing user's tenants would be to give the user an option to specify the tenant which they want to authenticate to. All the user would have to know is the domain of one user who they are collaborating with, because then, you can get the tenant id from that domain. After that, you just verify whether the tenant has signed up for your application, optionally verify whether the user is present in the directory before redirecting them (by using client credentials call to Graph) and if everything goes fine, you just redirect the user to proper AAD instance. However, it will probably be rather confusing for the user than just choosing from a simple dropdown.

I hope you will now have a better understanding of B2B usage in your multi-tenant applications. If there's something unclear about this approach, please let me know in comments or contact me directly.

Lastly, you can expect one commercial application enabled with this functionality very soon! Stay tuned!

Comments

Vitalii

Thank you for the nice article. You saved my day. I have only one question. How can I fix a problem of getting tenants when user logged in directory where he is as Guest not a Member. The exception is looked like:

Exception: {“error”:”invalid_grant”,”error_description”:”AADSTS50034: To sign into this application the account must be added to the GUID directory.\r\nTrace ID: GUID\r\nCorrelation ID: GUID\r\nTimestamp: 2017-08-03 15:58:15Z”,”error_codes”:[50034],”timestamp”:”2017-08-03 15:58:15Z”,”trace_id”:”GUID”,”correlation_id”:”GUID”}

Jan Hajek

Right, I just tried with a clean directory and I am hitting the same issue. The documentation states that Guests are synonymous to Members (https://docs.microsoft.com/en-us/azure/active-directory/active-directory-b2b-user-properties#can-azure-ad-b2b-users-be-added-as-members-instead-of-guests) which makes it more confusing. I noticed you already wrote to Stack Overflow (https://stackoverflow.com/questions/45487492/aadsts50034-to-sign-into-this-application-the-account-must-be-added-to-the-gu) so I voted it up. All we can do is wait for Microsoft’s response in this case.

Vitalii

HI Jan, We have found out the problem. The problem in authorization context of getting tenants. We have to use ID of logged active directory instead of “common”.


public AzureServiceManagement(IHttpContextAccessor httpContextAccessor, IConfigurationRoot configuration)
{
    _httpContextAccessor = httpContextAccessor;
    _userObjectId = _httpContextAccessor.HttpContext.User.FindFirst(AzureAdClaimTypes.ObjectId)?.Value;
    _tenantId = _httpContextAccessor.HttpContext.User.FindFirst(AzureAdClaimTypes.TenantId)?.Value;
    _clientId = configuration["Authentication:AzureAd:ClientId"];
    //_authContext = new AuthenticationContext("https://login.microsoftonline.com/common", new NaiveSessionCache(_tenantId, _httpContextAccessor.HttpContext.Session));
    _authContext = new AuthenticationContext("https://login.microsoftonline.com" + _tenantId, new NaiveSessionCache(_tenantId, _httpContextAccessor.HttpContext.Session));

    _clientCredential = new ClientCredential(_clientId, configuration["Authentication:AzureAd:ClientSecret"]);
}
Jan Hajek

Yes, I have fixed the issue in the GitHub sample repo and also added it as an aswer to your Stack Overflow question. Thanks a lot for pointing this out!

Steve Ralston

Can all flavors of Office 365 access a third-party multi-tenant app, or is that feature only available for certain kinds of accounts?

Jan Hajek

This feature is leveraging Azure Active Directory, which is in the heart of Office 365 (providing all identity, authentication, authorization and directory services), so yes, any Azure AD backed service can leverage of B2B access.

To submit comments, go to GitHub Discussions.