Skip to content

Cannot authenticate against a Microsoft Entra ID protected backend: Entra never advertises custom scopes in scopes_supported #930

Description

@emmanuelmathot

The problem

Microsoft Entra ID cannot advertise an application's custom API scopes in its OIDC discovery document. Its scopes_supported is tenant-wide and permanently fixed at ["openid", "profile", "email", "offline_access"]. This is by design, per Microsoft in AzureAD/microsoft-identity-web#1689:

The OIDC metadata endpoint just returns Azure AD's STS metadata. There's no concept of custom resources or custom scopes here.

OidcProviderInfo.__init__ intersects the scopes a backend declares at /credentials/oidc against that list (openeo/rest/auth/oidc.py, 0.48.0):

self._scopes = {"openid"}.union(scopes or []).intersection(self._supported_scopes)

A sensible default against a well-behaved provider but with Entra it drops the one scope that matters:

provider = OidcProviderInfo(
    issuer=f"https://login.microsoftonline.com/{TENANT}/v2.0",
    scopes=["openid", "profile", "email", f"api://{CLIENT_ID}/openeo"],
)
provider.get_scopes_string()
# -> 'email openid profile'      the api:// scope never reaches Entra

With no resource scope requested, Entra issues an access token unusable and the backend (here titiler-openeo) rejects the token, and the user sees a signature error with no obvious connection to a dropped scope.

The net effect is that no openEO backend protected by Entra can be authenticated against with this client, and nothing the backend or the app registration can configure will change that.

Possible accommodations

Any of these would be enough:

  1. Warn instead of dropping keep the declared scopes, log.warning when one is not advertised. Preserves the diagnostic value without the breakage.
  2. Opt out e.g. OidcProviderInfo(..., strict_scopes=False), plumbed through Connection.authenticate_oidc*.
  3. Do not intersect at all RFC 8414 makes scopes_supported RECOMMENDED and descriptive, and RFC 6749 §3.3 puts the grant decision on the authorization server, which reports the result in the scope response parameter.

Options 1 and 3 also help any other provider with incomplete metadata; option 2 leaves each user to discover the problem first.

Workaround

from openeo.rest.auth.oidc import OidcProviderInfo

_orig_init = OidcProviderInfo.__init__

def _patched_init(self, issuer=None, discovery_url=None, scopes=None, **kwargs):
    _orig_init(self, issuer=issuer, discovery_url=discovery_url, scopes=scopes, **kwargs)
    self._scopes = {"openid"} | set(scopes or [])

OidcProviderInfo.__init__ = _patched_init

Environment

openeo 0.48.0 · Python 3.13 · Entra ID v2.0 endpoint, single tenant · backend: titiler-openeo

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions