Skip to content

Document tenant-specific authority selection for on-behalf-of token redemption #980

Open

Description

@hidingmyname

MSAL client type

Confidential

Problem Statement

Location (AzureAD/microsoft-authentication-library-for-python @ default branch c53d5ea):

    def acquire_token_on_behalf_of(self, user_assertion, scopes, claims_challenge=None, forwarded_client_claims=None, **kwargs):
        """Acquires token using on-behalf-of (OBO) flow.

        The current app is a middle-tier service which was called with a token
        representing an end user.
        The current app can use such token (a.k.a. a user assertion) to request
        another token to access downstream web API, on behalf of that user.
        See `detail docs here <https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow>`_ .
        ...
        if self.authority.tenant.lower() in ["common", "organizations"]:
            warnings.warn(
                "Using /common or /organizations authority "
                "in acquire_token_for_client() is unreliable. "
                "Please use a specific tenant instead.", DeprecationWarning)

Problem

A middle-tier web API commonly reuses a ConfidentialClientApplication that was configured with https://login.microsoftonline.com/common or /organizations (for example, copied from a multi-tenant sign-in setup) and then calls acquire_token_on_behalf_of(). Nothing at the Python entry point tells the developer which tenant the OBO redemption should target, which is where guest-user and Conditional Access scenarios become hard to diagnose.

The acquire_token_on_behalf_of() docstring links to the generic protocol page and the consent section, but says nothing about which authority/tenant to use. By contrast, acquire_token_for_client() already warns at runtime that /common or /organizations "is unreliable. Please use a specific tenant instead." There is no similar hint, in code or docs, for OBO.

The MSAL.NET documentation has an explicit section, "Important Note on On-Behalf-Of (OBO) Flow with Guest Users":

When performing the On-Behalf-Of (OBO) flow, especially with guest users, it is important to target the specific tenant, denoted by the tid claim from the client token. Do not use /common or /organizations in OBO, because the token will be for the user's home tenant.

MSAL Python currently has no equivalent guidance.

This is a documentation/usability request. I am not reporting a failing request or a security problem in MSAL Python itself.

Reproduction

There is no failure to reproduce; the gap is in the documentation. Reading help(msal.ConfidentialClientApplication.acquire_token_on_behalf_of) on c53d5ea shows no guidance on authority or tenant selection, and calling it on an application configured with /common or /organizations emits no hint, unlike acquire_token_for_client().

Environment: checked on dev at c53d5ea47f77.

Proposed solution

Suggested fix

Add guidance to the acquire_token_on_behalf_of() docstring (and, if appropriate, the MSAL Python docs on learn.microsoft.com or a sample) that:

  1. Recommends a tenant-specific authority for OBO redemption rather than /common or /organizations, mirroring the MSAL.NET note.
  2. Explains that, for guest users, the tenant to target is the one in the incoming token's tid claim (the resource tenant), not the user's home tenant.
  3. Shows a short Python example that takes tid from the already validated incoming access token (that is, after the web API's own JWT validation, not from an unverified decode) and builds a per-tenant ConfidentialClientApplication, e.g.:
# `claims` = claims of the incoming access token, AFTER your API has validated it
tenant_id = claims["tid"]
cca = msal.ConfidentialClientApplication(
    client_id, client_credential=secret,
    authority=f"https://login.microsoftonline.com/{tenant_id}",
    token_cache=cache,
)
result = cca.acquire_token_on_behalf_of(incoming_token, scopes=["https://graph.microsoft.com/.default"])

Out of scope: MSAL automatically trusting or decoding tid from an unvalidated assertion. A runtime DeprecationWarning, like the one in acquire_token_for_client(), could be considered separately, but documentation is the main request here, since a warning alone would not explain which tenant to use.

This docstring change covers points 1–3 above, adding a note with the MSAL.NET guidance and the example to acquire_token_on_behalf_of():

diff --git a/msal/application.py b/msal/application.py
--- a/msal/application.py
+++ b/msal/application.py
@@ -2731,6 +2731,24 @@ class ConfidentialClientApplication(ClientApplication):  # server-side web app
         See how to gain consent upfront for your middle-tier app from this article.
         https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow#gaining-consent-for-the-middle-tier-application
 
+        .. note::
+            Use a tenant-specific authority for OBO rather than ``/common`` or
+            ``/organizations``, which would get a token for the user's home tenant.
+            This matters especially for guest users: target the tenant in the
+            ``tid`` claim of the incoming token (the resource tenant).
+            Read ``tid`` only after your web API has validated the incoming token,
+            for example::
+
+                # `claims` = claims of the incoming access token, AFTER your API has validated it
+                tenant_id = claims["tid"]
+                cca = msal.ConfidentialClientApplication(
+                    client_id, client_credential=secret,
+                    authority=f"https://login.microsoftonline.com/{tenant_id}",
+                    token_cache=cache,
+                    )
+                result = cca.acquire_token_on_behalf_of(
+                    incoming_token, scopes=["https://graph.microsoft.com/.default"])
+
         :param str user_assertion: The incoming token already received by this app
         :param list[str] scopes: Scopes required by downstream API (a resource).
         :param claims_challenge:

This is a documentation-only change, so there is no test for it; on d73407a the updated acquire_token_on_behalf_of() docstring parses as reStructuredText without warnings (docutils 0.23, Python 3.13.18).

I can submit a PR for this.

Related: checked #53 (original OBO support request, implemented; does not cover tenant selection), #909 (closed unmerged; switched two OBO integration tests from /organizations to a tenant-specific authority because of Conditional Access failures in CI, a practical example of the same pitfall), and #147 (web API token-validation example, not OBO authority selection).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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