Supporting tenant selection for B2B users in a multi-tenant Azure Active Directory application

This is a developer-oriented post, so a basic understanding of OAuth2 and Azure Active Directory authentication is required.

Yeah, that title sure is a mouthful. So, what's it all about?

Let's say that you have an awesome app that does extra cool stuff.

When performing authentication to Azure AD you use the https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize or https://login.microsoftonline.com/{tenant}/oauth2/authorize endpoint, depending on the version you wish to use.

The {tenant} value can be a:

  • Tenant id in the form of a guid;
  • A domain name from the tenant from which you wish to connect; or
  • A special value of common when you do not care about the tenant.
    • The V2 endpoint also has the special values of organizations and consumers.

How to authenticate B2B users in a multi-tenant app

So, armed with this information, how do you authorize your users in a multi-tenant app without knowing from which tenant they came?

Easy peasy, simply target the /common or /organizations (for the V2 endpoint) authentication endpoint. Simple enough. The users will be authenticated to their tenant and you can enjoy all of the APIs available to you – Exchange, CSOM, Graph API, etc.

OK, but one day somebody comes along and says: this is all well and good, but I keep getting data from my own tenant; I want to see data from tenant Y and I use the same account – they added me to the tenant as a guest. 

Well, again the answer is – it’s simple. Don’t use the /common (/organizations) endpoint, use the /{tenantGuid} or /{domain} endpoints for these login scenarios. Just enter the information for the tenant the user wants to use.

Great, how are you supposed to do that?  Isn’t this all a bit paradoxical?

This is a multi-tenant application – we really don’t know where the user came from. This is precisely the reason that the /common and /organizations endpoints exist. We can only find out about the tenant after the user is logged in.

For guest users, we could ask the user for his input in identifying the domain of the tenant he wants to connect to. Or maybe not – that would not be a great UX.

Ideally, we would present him with a list of tenants in which he has an account and let him choose from these.

I'm sure that the Microsoft Graph API, the API that rules them all, has what we’re looking for. It always does. Or does it? Yeah, nothing to be found there. 

After a bit of fumbling around with constructing the correct Google query we come across a StackOverflow question (https://stackoverflow.com/questions/45235572/getting-all-b2b-directories-user-is-member-of) in which the following link can be found: https://docs.microsoft.com/en-us/rest/api/resources/tenants/list.

The endpoint mentioned in the answer is at https://management.azure.com/tenants?api-version=2016-06-01After exploring a bit and trying it by using the handy Try It button, there is disappointment in the air.

 tenant guid list

 

OK, great. We can get a list of tenants but the only details returned are tenant guid and some kind of an id. How is this usable exactly? Who thought that this would be enough information to do something useful with it? Should we show the guids to the user and let him choose? Ah yes, 825fd5d7-ffd8-43f0-8ffb-f644cba8c46c, they provide us with IT support. 539ee038-456a-4043-b6c6-92acfd0534da? Yeah, they are in charge of our employee training.

But we do have the guids, so could we not find some endpoint from which we can retrieve the tenant name?

The id field looks a bit like a URL that can be queried, so maybe the tenant name is available there. There’s nothing about this kind of an endpoint mentioned in the docs but I guess we could try to query it. Of course, there’s no handy Try It button, so a little bit of coding is required (or just use postman with the same access token displayed on the documentation page). After firing the request, we get the following error: The request did not have a provided subscription. All requests must have an associated subscription Id.”

Something tells me that this is not actually a case of a missing subscription ID, since the /tenants/list endpoint works correctly without one.

Is there maybe some other endpoint? We do have the https://graph.microsoft.com/v1.0/organization  endpoint where we can get the default and initial domain of the tenant, but the kicker is that, of course, we have to be authenticated on that tenant. While a silent authentication to a different tenant is possible once we are authenticated through the /organizations endpoint, there is a strong possibility that we were not given consent to use the Azure AD application in question. So even before choosing a tenant, we need to get a series of consent prompts for each and every one of the tenants just so we can discover the tenant names. No, this does not sound like a solution that anybody would be happy with.

It may already have occurred to the more astute readers among you that the Azure API endpoint mentioned above has a version query parameter that is a date. Really, why use normal version numbers that you can easily increment when “date” is such an appealing option (date is actually an understandable choice for a complex system like Azure, which has multiple systems versioned independently, and one level of indirection less when talking about versions is welcome).

A quick bit of Googling for versions of this API does not reveal much. What would happen if I enter a newer date? Will it work? I believe we can assume the answer to that question. Well, that was a long shot anyway – the response was 400 Bad Request.  Since I was using C# to send the request, I didn’t bother to check the content of the response (foreshadowing occurs) once I saw that the request failed.

Back to the Stack Overflow question, there is a very good blog post linked and written by Jan Hajek, the author of the question: https://hajekj.net/2017/07/24/creating-a-multi-tenant-application-which-supports-b2b-users/. This link contains a lot of useful information, so be sure to check it out. It mentions the https://portal.azure.com/AzureHubs/api/tenants/List endpoint, which contains a lot more info about the tenants.

OK, cool, seems a bit hackish but I'll take what I can get, and all my problems are now solved. Or are they? No, the endpoint does not work anymore. It's seems that this is defunct. Strange, I was positive that an undocumented API endpoint would still be working a year later. It's not like Microsoft keeps changing the admin centers every single day. 

Speaking of admin centers, I guess I could check out the Azure admin portal since this is where the undocumented endpoint was used.

 Azure API tenants list endpoint

 

THIS is exactly what we need – a list of tenants.

By doing a bit of network observation in the Chrome developer tools we can see that there are a couple of batched requests going out to https://managment.azure.com. Seems to be the same Azure application that is mentioned in the docs. By going a bit deeper and looking at the individual responses from the batched requests we can find the data used to populate the tenant selector.

The corresponding request is … https://management.azure.com/tenants?api-version=2017-08-01This looks awfully familiar – it is the same URL as in the Azure API docs. It seems that 2017-08-01 is a valid version and we get the tenant display names; as a bonus we get some domain information. Microsoft changed the data-source from an undocumented to a documented API. I find this a very welcome change. Unfortunately, the documentation could be a little better by mentioning the various versions and changes.

Going back to my little test application and changing the API version parameter works like a charm. But, for one reason or another I change it back to a failing date. This time I also check the content of the response.

valid API versions returned in the error

Yeah, the valid API versions were right there in the error returned. It’s a little bit sad that I did not catch this fact sooner, and I move on. 

We finally get some enjoyment in building a tenant selector component. 

Now that the user can select a tenant, we simply use the selected tenant’s id to fire an authorization request on the /{tenantGuid} endpoint. Since we are already logged in and authorized on the tenant that serves as an identity provider for the other tenant, the authorization will be silent when using any of the authentication libraries (MSAL, ADAL).  The only gotcha is that if we have not received consent in the Azure Active Directory application for the selected tenant, the user will then be presented with a consent screen. A little note about ADAL (and possibly MSAL) –please use a different AuthenticationContext for each tenant. Using a single instance will result in weird errors. The TokenCache can be shared through.

And for the final and easiest step, there is the “small” issue of changing the application code to handle the fact that usernames and tenants can be in a 1:N relationship. 

Should be simple and fast (just to be clear, it was NOT simple and fast 😊).