Repository: AzureAD/microsoft-authentication-library-for-python

Issue:
Cannot put client id into scopes, therefore cannot get access_token for Azure B2C
According to [Azure B2C documentation](https://learn.microsoft.com/en-us/azure/active-directory-b2c/access-tokens#openid-connect-scopes), to get an access_token, client id must be added to scopes, i.e. `scopes=['openid', 'offline_access', '<CLIENT_ID>']`.
> The OpenID Connect standard specifies several special scope values. The following scopes represent the permission to access the user's profile:
> * openid - Requests an ID token.
> * offline_access - Requests a refresh token using [Auth Code flows](https://learn.microsoft.com/en-us/azure/active-directory-b2c/authorization-code-flow).
> * 00000000-0000-0000-0000-000000000000 - Using the client ID as the scope indicates that your app needs an access token that can be used against your own service or web API, represented by the same client ID.

However, `_decorate_scope` will replace client id with `['openid', 'profile', 'offline_access']`, which does not generate access_token.

https://github.com/AzureAD/microsoft-authentication-library-for-python/blob/545e856124985da4758530ab811d2c137fa8e333/msal/application.py#L591

Discussion:

Patch:
diff --git a/msal/application.py b/msal/application.py
index 7e3ec16..62ba4b5 100644
--- a/msal/application.py
+++ b/msal/application.py
@@ -588,18 +588,9 @@ class ClientApplication(object):
             raise ValueError(
                 "API does not accept {} value as user-provided scopes".format(
                     reserved_scope))
-        if self.client_id in scope_set:
-            if len(scope_set) > 1:
-                # We make developers pass their client id, so that they can express
-                # the intent that they want the token for themselves (their own
-                # app).
-                # If we do not restrict them to passing only client id then they
-                # could write code where they expect an id token but end up getting
-                # access_token.
-                raise ValueError("Client Id can only be provided as a single scope")
-            decorated = set(reserved_scope)  # Make a writable copy
-        else:
-            decorated = scope_set | reserved_scope
+
+        # client_id can also be used as a scope in B2C
+        decorated = scope_set | reserved_scope
         decorated -= self._exclude_scopes
         return list(decorated)
 


Test patch:
diff --git a/tests/test_application.py b/tests/test_application.py
index 804ccb8..b62f41d 100644
--- a/tests/test_application.py
+++ b/tests/test_application.py
@@ -625,3 +625,18 @@ class TestClientCredentialGrant(unittest.TestCase):
         self._test_certain_authority_should_emit_warnning(
             authority="https://login.microsoftonline.com/organizations")
 
+
+class TestScopeDecoration(unittest.TestCase):
+    def _test_client_id_should_be_a_valid_scope(self, client_id, other_scopes):
+        # B2C needs this https://learn.microsoft.com/en-us/azure/active-directory-b2c/access-tokens#openid-connect-scopes
+        reserved_scope = ['openid', 'profile', 'offline_access']
+        scopes_to_use = [client_id] + other_scopes
+        self.assertEqual(
+            set(ClientApplication(client_id)._decorate_scope(scopes_to_use)),
+            set(scopes_to_use + reserved_scope),
+            "Scope decoration should return input scopes plus reserved scopes")
+
+    def test_client_id_should_be_a_valid_scope(self):
+        self._test_client_id_should_be_a_valid_scope("client_id", [])
+        self._test_client_id_should_be_a_valid_scope("client_id", ["foo"])
+
diff --git a/tests/test_e2e.py b/tests/test_e2e.py
index 5c43f4f..48ffe47 100644
--- a/tests/test_e2e.py
+++ b/tests/test_e2e.py
@@ -884,6 +884,18 @@ class WorldWideTestCase(LabBasedTestCase):
             scope=config["scopes"],
             )
 
+    def test_b2c_allows_using_client_id_as_scope(self):
+        # See also https://learn.microsoft.com/en-us/azure/active-directory-b2c/access-tokens#openid-connect-scopes
+        config = self.get_lab_app_object(azureenvironment="azureb2ccloud")
+        config["scopes"] = [config["appId"]]
+        self._test_username_password(
+            authority=self._build_b2c_authority("B2C_1_ROPC_Auth"),
+            client_id=config["appId"],
+            username="b2clocal@msidlabb2c.onmicrosoft.com",
+            password=self.get_lab_user_secret("msidlabb2c"),
+            scope=config["scopes"],
+            )
+
 
 class WorldWideRegionalEndpointTestCase(LabBasedTestCase):
     region = "westus"