This was something that was asked from me a while back.
Is it possible to have a multi-tenant app, which calls API A, which then calls API B? What would I need to do to make that work?
So this is what I might call an API Chain.
Another question was:
Can I have app A which calls the API of app B, and then also have app B call the API of app A?
In this case there is a cyclic dependency between apps A and B. The question is: can this be done in a multi-tenant app?
I was really curious if either of these scenarios was possible. And if they were, what would it take?
Scenario Setups
For both scenarios, I will register the applications in one of my test Azure AD tenants. I will authenticate to the applications using a user in another Azure AD tenant. All applications will be marked as multi-tenant, and of the Web app/API type (publicClient = false).
For the API chain scenario, I wanted to go a step further than the question. Four applications would be registered in the tenant:
- Client
- API A
- API B
- API C
They formed a permission requirement chain such that:
- Client requires access to API A
- API A requires access to API B
- API B requires access to API C
So we have applications in the following arrangement:
For the second scenario, cyclic dependency, two applications were registered:
- Client/API A
- Client/API B
Both required access to each other. So we have two applications like this:
Known Client Applications
In both of the scenarios, we will be authenticating with a user from another Azure AD tenant. For the scenario to succeed, a service principal must be created for each of the Application objects which compose the app in the target tenant. A service principal is created when the user consents to the permissions the Application requires.
A problem that arises in multi-tenant scenarios is that if an application requires access to another application/API, that application/API's service principal must be present in the tenant. We could get around this by having the user first go through consent for the other application/API, but that is the wrong way to do it.
We can instead define that the application requiring access (the client), is a known client application of the API. We do this by modifying the application manifest for the API in question:
{
"knownClientApplications": [
"client-app-client-id"
]
}
By putting the client id/application id of the client in here, Azure AD allows consent to be done for both the client and the API at the same time.
API Chain Scenario
My theory at first was that the APIs should be in the following arrangement:
So each Application would require access to the next Application in the chain, and also be a known client application of the next one. Seems reasonable to me.
However, trying to authenticate results in an error:
AADSTS65005: The app needs access to a service (https://joonasapps.onmicrosoft.com/47ea1420-ad51-4e85-b682-79631f001d0e) that your organization "joonasw.net" has not subscribed to or enabled. Contact your IT Admin to review the configuration of your service subscriptions.
The error is talking about API B. So next, I decided to try making the Client a known client of API B.
Result: Failure. The error is the same.
I then tried to make Client and API A known clients of API C.
Result: Failure. This did not change change the error either.
The next experiment was to require access to API B from the Client.
Result: Failure. But, the error changed:
AADSTS65005: The app needs access to a service (https://joonasapps.onmicrosoft.com/2131f619-f44d-4b99-b9cd-6a3253dd0c26) that your organization c5e5d73b-e74c-48b3-a1ad-b0af0cf7f751 has not subscribed to or enabled. Contact your IT Admin to review the configuration of your service subscriptions.
It is now complaining about API C. Interestingly, it also uses the tenant id in the error instead of the domain as previously. Anyway, this is progress!
Since we made progress by adding a permission requirement to the client, next we will also require access to API C from the Client.
Result: Success. We got an authorization code in the response, and I was able to confirm with AAD Graph Explorer that the Service Principals were created along with the OAuth2PermissionGrants giving them access to the resources they required.
At this point I had a theory on what was going on, so I removed the known client chain. The Client would be a known client application for all APIs, and nothing else. The applications looked like this:
Result: Success.
Now we know what Azure AD requires. Here is how I see Azure AD's logic:
- User tries to log in to Client
- AAD checks if a Service Principal exists for Client
- Seeing one does not exist, it checks if Client is multi-tenanted
- AAD then checks if all the Service Principals exist for the APIs Client wants access to
- For those that do not exist, AAD checks if Client is a known client application to each API
- If all do not specify Client as a known client app, authentication fails
- AAD then proceeds to check the required access of the APIs for which a Service Principal does not exist
- If any required permission is found targeting an API for which no Service Principal exists in the tenant, fail authentication
So, it would seem that Azure AD does not traverse the known client app chain. It only looks at the direct dependencies of the client being signed in to. Which means that Client must require access to all APIs in the chain, no matter if it uses them directly or not.
The nice thing is that certain Service Principals exist by default. Such as Microsoft Graph API. So in this scenario, API B can require access to user's files, and Client won't have to change anything to enable that.
Cyclic Dependency Scenario
This scenario was a little similar to the above. There are two applications. App A and App B. Both of them have an API as well, and they both use the other one's API. I know this scenario might seem a bit weird, but this kind of thing can happen. I started with the assumption that the following could work:
Result: Failure.
I tried to authenticate to App A, but was met with an error:
AADSTS65005: The app needs access to a service (https://joonasapps.onmicrosoft.com/d4b1d1cc-da7b-4be2-bf2f-84d45f4256da) that your organization joonasw.net has not subscribed to or enabled. Contact your IT Admin to review the configuration of your service subscriptions.
That identifier is for App A. I am trying to sign in to it, and create the Service Principals. I spent some time thinking about it, and realized the problem. We can also think of our app arrangement like this:
If we use the logic I theorised Azure AD is using from the first scenario, we can see why this will not work. Azure AD requires all dependencies of App B to already have a Service Principal in the tenant, or be direct dependencies of App A. But App A cannot depend on itself.
Here are a few solutions to the problem:
- Use one Application for both App A and App B
- Simplest
- Does mean any permissions given to one are given to the other
- Use a separate Application for the API of App A or App B
- Requires a little bit of changes to one of the apps
- Use a different client id for the API part
- For example, App A then depends on the API app for App B, and App B depends on App A, so no cycle exists
- Re-architect the system so that the pieces interacting together are inside one API, which App A and App B call
- Requires most work
- Separate Applications for each piece, which is a good thing
- Make the whole thing one app
- They are already dependent on each other, which is kinda funny
Conclusions
Based on my experiments, Azure AD does not traverse API chains to gather the permissions required for consent. It only checks the direct dependencies of the client. So make sure that your client requires access to all the APIs in the chain. And then also ensure the client is a known client application of all the APIs. The APIs in the chain should still require access to the APIs they need to use. But they do not need to be known clients of them.
Cyclic dependencies do not work. The problem is actually very similar to the API chain scenario, except this cannot be fixed since the app cannot depend on itself. I presented some possible solutions to this above.
If you noticed any mistakes that I may have done in the experiments, or you found a way to do something which did not work for me, don't hesitate to leave a comment. I really appreciate all good and constructive feedback.
Until next time!