Not surprising at all. The configuration and docs for Oauth2 on Entra is an absolute cluster-f. Evidently, it’s so confusing that not even Microsoft themselves can get it right.
Their solution to this will be to add even more documentation, as if anyone had the stomach to read through the spaghetti that exist today.
Ran into this just a few weeks ago. According to the documentation it should be impossible to perform the authorization code flow with a scope that targets multiple resource servers. But if I request "openid $clientid/.default" it works. Kinda. At the end of the flow I get back an ID token and and access token. The ID token indicates that Azure has acknowledged the OIDC scope. But when I check the access token I can see that the scope has been adjusted to not include "openid". And indeed I'm unable to call Microsoft Graph which serves as the UserInfo endpoint. I was unable to find any good explanation for this behavior.
(I work on Entra) The OpenID Connect standard says that when you make a request using the OpenID Connect scopes (openid, profile, email, address, phone, offline_access), you get an access token that can be used to call the UserInfo endpoint. The OpenID Connect standard *does not say* what happens when you combine OpenID Connect scopes with OAuth scopes (like $clientid/.default).
Entra treats such requests as an OpenID Connect OAuth hybrid. The ID token is as specified under OpenID Connect, but the access token is as expected from OAuth. In practice, these are the tokens most people want. The UserInfo endpoint is stupid - you can get all that information in the ID token without an extra round trip.
> According to the documentation it should be impossible to perform the authorization code flow with a scope that targets multiple resource servers.
(I work on Entra) Can you point me to the documentation for this? This statement is not correct. The WithExtraScopesToConsent method (https://learn.microsoft.com/en-us/dotnet/api/microsoft.ident...) exists for this purpose. An Entra client can call the interactive endpoint (/authorize) with scope=openid $clientid/.default $client2/.default $client3/.default - multiple resource servers as long as it specifies exactly one of those resource servers on the non-interactive endpoint (/token) - i.e. scope=openid $clientid/.default. In the language of Microsoft.Identity.Client (MSAL), that's .WithScopes("$clientid/.default").WithExtraScopesToConsent("$client2/.default $client3.default"). This pattern is useful when your app needs to access multiple resources and you want the user to resolve all relevant permission or MFA prompts up front.
It is true that an access token can only target a single resource server - but it should be possible to go through the first leg of the authorization code flow for many resources, and then the second leg of the authorization code flow for a single resource, followed by refresh token flows for the remaining resources.
You are confusing the purpose of the openid scope. That scope is used to "enable" OIDC in an otherwise pure-OAuth server. By itself, the openid scope never gives you access to anything itself, so it should not impact the Access Token at all - which should not include that scope (as it would be useless anyway). The UserInfo endpoint should only return claims that were requested in the authorization request via scopes like `profile` and `email`. The ID token is only returned if your response_type includes `id_token` and usually means you want the claims directly returned as a JWT ID Token, and won't be making Userinfo requests.
For me, the "openid" scope gives me access to the UserInfo endpoint (which is provided by the Microsoft Graph API). So probably this is something where the implementation in Azure differs from the general protocol spec?
You can see it that way, but you need to understand that if what you want from the Userinfo endpoint is to obtain claims about the subject... and to do that, you need to require scopes that map to claims (the openid scope does not map any claim) or you need to explicitly request the claims directly. An authorization request that only requests the `openid` scope should result in a Userinfo response containing only the user's `sub` (because that's a mandatory claim to return) but the OIDC server may chose to just fail the request.