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

Issue:
Add public convenience string constants for endpoints of each cloud (sovereign and public)
**Context**
Customers have had difficulty finding the right documentation pages:
https://docs.microsoft.com/en-us/azure/azure-government/documentation-government-developer-guide#endpoint-mapping
https://docs.microsoft.com/en-us/azure/china/resources-developer-guide#check-endpoints-in-azure

**See also other libraries exposing these:**
- [obj-c](https://azuread.github.io/microsoft-authentication-library-for-objc/Enums/MSALAzureCloudInstance.html) 
- [.net](https://docs.microsoft.com/en-us/dotnet/api/microsoft.identity.client.azurecloudinstance?view=azure-dotnet)

**Reference:**
https://identitydivision.visualstudio.com/Engineering/_workitems/edit/1063014

**Other:**
https://github.com/AzureAD/microsoft-authentication-library-for-java/issues/258

Discussion:

Patch:
diff --git a/msal/application.py b/msal/application.py
index 05b77fc..04ad5fd 100644
--- a/msal/application.py
+++ b/msal/application.py
@@ -231,8 +231,23 @@ class ClientApplication(object):
 
         :param str authority:
             A URL that identifies a token authority. It should be of the format
-            https://login.microsoftonline.com/your_tenant
-            By default, we will use https://login.microsoftonline.com/common
+            ``https://login.microsoftonline.com/your_tenant``
+            By default, we will use ``https://login.microsoftonline.com/common``
+
+            *Changed in version 1.17*: you can also use predefined constant
+            and a builder like this::
+
+                from msal.authority import (
+                    AuthorityBuilder,
+                    AZURE_US_GOVERNMENT, AZURE_CHINA, AZURE_PUBLIC)
+                my_authority = AuthorityBuilder(AZURE_PUBLIC, "contoso.onmicrosoft.com")
+                # Now you get an equivalent of
+                # "https://login.microsoftonline.com/contoso.onmicrosoft.com"
+
+                # You can feed such an authority to msal's ClientApplication
+                from msal import PublicClientApplication
+                app = PublicClientApplication("my_client_id", authority=my_authority, ...)
+
         :param bool validate_authority: (optional) Turns authority validation
             on or off. This parameter default to true.
         :param TokenCache cache:
diff --git a/msal/authority.py b/msal/authority.py
index 0656011..14a6ad1 100644
--- a/msal/authority.py
+++ b/msal/authority.py
@@ -14,12 +14,19 @@ from .exceptions import MsalServiceError
 
 
 logger = logging.getLogger(__name__)
+
+# Endpoints were copied from here
+# https://docs.microsoft.com/en-us/azure/active-directory/develop/authentication-national-cloud#azure-ad-authentication-endpoints
+AZURE_US_GOVERNMENT = "login.microsoftonline.us"
+AZURE_CHINA = "login.chinacloudapi.cn"
+AZURE_PUBLIC = "login.microsoftonline.com"
+
 WORLD_WIDE = 'login.microsoftonline.com'  # There was an alias login.windows.net
 WELL_KNOWN_AUTHORITY_HOSTS = set([
     WORLD_WIDE,
-    'login.chinacloudapi.cn',
+    AZURE_CHINA,
     'login-us.microsoftonline.com',
-    'login.microsoftonline.us',
+    AZURE_US_GOVERNMENT,
     'login.microsoftonline.de',
     ])
 WELL_KNOWN_B2C_HOSTS = [
@@ -30,6 +37,19 @@ WELL_KNOWN_B2C_HOSTS = [
     ]
 
 
+class AuthorityBuilder(object):
+    def __init__(self, instance, tenant):
+        """A helper to save caller from doing string concatenation.
+
+        Usage is documented in :func:`application.ClientApplication.__init__`.
+        """
+        self._instance = instance.rstrip("/")
+        self._tenant = tenant.strip("/")
+
+    def __str__(self):
+        return "https://{}/{}".format(self._instance, self._tenant)
+
+
 class Authority(object):
     """This class represents an (already-validated) authority.
 
@@ -53,6 +73,8 @@ class Authority(object):
             performed.
         """
         self._http_client = http_client
+        if isinstance(authority_url, AuthorityBuilder):
+            authority_url = str(authority_url)
         authority, self.instance, tenant = canonicalize(authority_url)
         parts = authority.path.split('/')
         is_b2c = any(self.instance.endswith("." + d) for d in WELL_KNOWN_B2C_HOSTS) or (


Test patch:
diff --git a/tests/http_client.py b/tests/http_client.py
index a5587b7..5adbbde 100644
--- a/tests/http_client.py
+++ b/tests/http_client.py
@@ -20,6 +20,9 @@ class MinimalHttpClient:
         return MinimalResponse(requests_resp=self.session.get(
             url, params=params, headers=headers, timeout=self.timeout))
 
+    def close(self):  # Not required, but we use it to avoid a warning in unit test
+        self.session.close()
+
 
 class MinimalResponse(object):  # Not for production use
     def __init__(self, requests_resp=None, status_code=None, text=None):
diff --git a/tests/test_authority.py b/tests/test_authority.py
index cd6db78..9fdc83c 100644
--- a/tests/test_authority.py
+++ b/tests/test_authority.py
@@ -8,16 +8,37 @@ from tests.http_client import MinimalHttpClient
 @unittest.skipIf(os.getenv("TRAVIS_TAG"), "Skip network io during tagged release")
 class TestAuthority(unittest.TestCase):
 
+    def _test_given_host_and_tenant(self, host, tenant):
+        c = MinimalHttpClient()
+        a = Authority('https://{}/{}'.format(host, tenant), c)
+        self.assertEqual(
+            a.authorization_endpoint,
+            'https://{}/{}/oauth2/v2.0/authorize'.format(host, tenant))
+        self.assertEqual(
+            a.token_endpoint,
+            'https://{}/{}/oauth2/v2.0/token'.format(host, tenant))
+        c.close()
+
+    def _test_authority_builder(self, host, tenant):
+        c = MinimalHttpClient()
+        a = Authority(AuthorityBuilder(host, tenant), c)
+        self.assertEqual(
+            a.authorization_endpoint,
+            'https://{}/{}/oauth2/v2.0/authorize'.format(host, tenant))
+        self.assertEqual(
+            a.token_endpoint,
+            'https://{}/{}/oauth2/v2.0/token'.format(host, tenant))
+        c.close()
+
     def test_wellknown_host_and_tenant(self):
         # Assert all well known authority hosts are using their own "common" tenant
         for host in WELL_KNOWN_AUTHORITY_HOSTS:
-            a = Authority(
-                'https://{}/common'.format(host), MinimalHttpClient())
-            self.assertEqual(
-                a.authorization_endpoint,
-                'https://%s/common/oauth2/v2.0/authorize' % host)
-            self.assertEqual(
-                a.token_endpoint, 'https://%s/common/oauth2/v2.0/token' % host)
+            self._test_given_host_and_tenant(host, "common")
+
+    def test_wellknown_host_and_tenant_using_new_authority_builder(self):
+        self._test_authority_builder(AZURE_PUBLIC, "consumers")
+        self._test_authority_builder(AZURE_CHINA, "organizations")
+        self._test_authority_builder(AZURE_US_GOVERNMENT, "common")
 
     @unittest.skip("As of Jan 2017, the server no longer returns V1 endpoint")
     def test_lessknown_host_will_return_a_set_of_v1_endpoints(self):