Hi FB,
You have hit a classic architectural hurdle with the Microsoft Entra ID On-Behalf-Of (OBO) flow. The behavior you are seeing is exactly how the protocol is designed to work, and the root cause lies in the Audience (aud claim) of your initial token.
Why the assertion fails in your OBO attempt: When you perform the initial Auth Code flow using MS Graph scopes, Entra ID mints an Access Token specifically for MS Graph. The audience (aud) of that token is [https://graph.microsoft.com](https://graph.microsoft.com).
The fundamental rule of the OBO flow in Entra ID is that the middle-tier API (App 1) can only exchange a token where App 1 itself is the audience. You cannot use a token minted for MS Graph as an assertion to get a token for App 2. Entra ID rejects this because App 1 is effectively trying to trade a token that doesn't belong to it.
Why it works when you pass the custom API scope in the first call: When you pass App 2's scope in the initial frontend call, you are bypassing the OBO flow entirely. You are simply doing a direct delegation from the Frontend to App 2. However, in a true middle-tier architecture, this defeats the purpose of having App 1 broker the requests.
The Architectural Fix (The Correct OBO Pattern)
To make this work deterministically, you need to restructure your token requests so App 1 acts as a true middle-tier broker.
Step 1: Expose an API on App 1 In Entra ID, go to App 1 -> Expose an API and create a scope (e.g., api://<App1-Client-ID>/access_as_user).
Step 2: Configure API Permissions
- In App 1's App Registration, grant Delegated permissions to App 2's Custom API.
In App 1's App Registration, grant Delegated permissions to MS Graph (for your user groups).
Ensure admin consent is granted for both if required.
Step 3: The Initial Call (Frontend to App 1) Your client application must authenticate the user and request a token using App 1's scope (the one created in Step 1).
Result: You receive a token where aud = App 1.
Step 4: The OBO Calls (App 1 to Downstream APIs) Now that App 1 holds a token where it is the intended audience, it can use this token as the assertion in the OBO flow. App 1 will actually perform two separate OBO requests:
OBO Request A: Use the App 1 token to request a token for MS Graph scopes (to retrieve user groups).
OBO Request B: Use the exact same App 1 token to request a token for App 2's custom API scopes.
This pattern ensures strict boundary control, correct audiences, and allows App 1 to orchestrate multiple downstream APIs using a single frontend authentication event.
Hope this clarifies the mechanics of the flow!Hi FB,
You have hit a classic architectural hurdle with the Microsoft Entra ID On-Behalf-Of (OBO) flow. The behavior you are seeing is exactly how the protocol is designed to work, and the root cause lies in the Audience (aud claim) of your initial token.
Why the assertion fails in your OBO attempt: When you perform the initial Auth Code flow using MS Graph scopes, Entra ID mints an Access Token specifically for MS Graph. The audience (aud) of that token is [https://graph.microsoft.com](https://graph.microsoft.com).
The fundamental rule of the OBO flow in Entra ID is that the middle-tier API (App 1) can only exchange a token where App 1 itself is the audience. You cannot use a token minted for MS Graph as an assertion to get a token for App 2. Entra ID rejects this because App 1 is effectively trying to trade a token that doesn't belong to it.
Why it works when you pass the custom API scope in the first call: When you pass App 2's scope in the initial frontend call, you are bypassing the OBO flow entirely. You are simply doing a direct delegation from the Frontend to App 2. However, in a true middle-tier architecture, this defeats the purpose of having App 1 broker the requests.
The Architectural Fix (The Correct OBO Pattern)
To make this work deterministically, you need to restructure your token requests so App 1 acts as a true middle-tier broker.
Step 1: Expose an API on App 1 In Entra ID, go to App 1 -> Expose an API and create a scope (e.g., api://<App1-Client-ID>/access_as_user).
Step 2: Configure API Permissions
In App 1's App Registration, grant Delegated permissions to App 2's Custom API.
In App 1's App Registration, grant Delegated permissions to MS Graph (for your user groups).
Ensure admin consent is granted for both if required.
Step 3: The Initial Call (Frontend to App 1) Your client application must authenticate the user and request a token using App 1's scope (the one created in Step 1).
Result: You receive a token where aud = App 1.
Step 4: The OBO Calls (App 1 to Downstream APIs) Now that App 1 holds a token where it is the intended audience, it can use this token as the assertion in the OBO flow. App 1 will actually perform two separate OBO requests:
OBO Request A: Use the App 1 token to request a token for MS Graph scopes (to retrieve user groups).
OBO Request B: Use the exact same App 1 token to request a token for App 2's custom API scopes.
This pattern ensures strict boundary control, correct audiences, and allows App 1 to orchestrate multiple downstream APIs using a single frontend authentication event.
Hope this clarifies the mechanics of the flow!