Repository: Azure/azure-cli

Issue:
login: expose ADFS flow for azure stack env

Discussion:
codecov-io: # [Codecov](https://codecov.io/gh/Azure/azure-cli/pull/3415?src=pr&el=h1) Report
> Merging [#3415](https://codecov.io/gh/Azure/azure-cli/pull/3415?src=pr&el=desc) into [master](https://codecov.io/gh/Azure/azure-cli/commit/46d5382995c2740df260704f270df0e5cac965a5?src=pr&el=desc) will **increase** coverage by `<.01%`.
> The diff coverage is `72.72%`.

[![Impacted file tree graph](https://codecov.io/gh/Azure/azure-cli/pull/3415/graphs/tree.svg?src=pr&width=650&token=2pog0TKvF8&height=150)](https://codecov.io/gh/Azure/azure-cli/pull/3415?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master    #3415      +/-   ##
==========================================
+ Coverage   70.81%   70.81%   +<.01%     
==========================================
  Files         394      394              
  Lines       25533    25532       -1     
  Branches     3889     3890       +1     
==========================================
  Hits        18081    18081              
+ Misses       6310     6308       -2     
- Partials     1142     1143       +1
```


| [Impacted Files](https://codecov.io/gh/Azure/azure-cli/pull/3415?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/azure-cli-core/azure/cli/core/\_profile.py](https://codecov.io/gh/Azure/azure-cli/pull/3415?src=pr&el=tree#diff-c3JjL2F6dXJlLWNsaS1jb3JlL2F6dXJlL2NsaS9jb3JlL19wcm9maWxlLnB5) | `87.85% <72.72%> (+0.2%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/Azure/azure-cli/pull/3415?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/Azure/azure-cli/pull/3415?src=pr&el=footer). Last update [46d5382...287b9e0](https://codecov.io/gh/Azure/azure-cli/pull/3415?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/src/azure-cli-core/HISTORY.rst b/src/azure-cli-core/HISTORY.rst
index 103f25a5c..867cd36a3 100644
--- a/src/azure-cli-core/HISTORY.rst
+++ b/src/azure-cli-core/HISTORY.rst
@@ -7,6 +7,7 @@ unreleased
 ^^^^^^^^^^^^^^^^^^
 * Command paths are no longer case sensitive.
 * Certain boolean-type parameters are no longer case sensitive.
+* Support login to ADFS on prem server like Azure Stack
 
 2.0.6 (2017-05-09)
 ^^^^^^^^^^^^^^^^^^
diff --git a/src/azure-cli-core/azure/cli/core/_profile.py b/src/azure-cli-core/azure/cli/core/_profile.py
index 79191e18a..43fbe0d3b 100644
--- a/src/azure-cli-core/azure/cli/core/_profile.py
+++ b/src/azure-cli-core/azure/cli/core/_profile.py
@@ -56,9 +56,14 @@ _CLIENT_ID = '04b07795-8ddb-461a-bbee-02f9e1bf7b46'
 _COMMON_TENANT = 'common'
 
 
-def _authentication_context_factory(authority, cache):
+def _authentication_context_factory(tenant, cache):
     import adal
-    return adal.AuthenticationContext(authority, cache=cache, api_version=None)
+    authority_url = CLOUD.endpoints.active_directory
+    is_adfs = authority_url.lower().endswith('/adfs')
+    if not is_adfs:
+        authority_url = authority_url + '/' + (tenant or _COMMON_TENANT)
+    return adal.AuthenticationContext(authority_url, cache=cache, api_version=None,
+                                      validate_authority=(not is_adfs))
 
 
 _AUTH_CTX_FACTORY = _authentication_context_factory
@@ -69,10 +74,6 @@ CLOUD = get_active_cloud()
 logger.debug('Current cloud config:\n%s', str(CLOUD))
 
 
-def get_authority_url(tenant=None):
-    return CLOUD.endpoints.active_directory + '/' + (tenant or _COMMON_TENANT)
-
-
 def _load_tokens_from_file(file_path):
     all_entries = []
     if os.path.isfile(file_path):
@@ -393,7 +394,7 @@ class SubscriptionFinder(object):
         self.tenants = []
 
     def find_from_user_account(self, username, password, tenant, resource):
-        context = self._create_auth_context(tenant or _COMMON_TENANT)
+        context = self._create_auth_context(tenant)
         token_entry = context.acquire_token_with_username_password(
             resource,
             username,
@@ -408,8 +409,7 @@ class SubscriptionFinder(object):
         return result
 
     def find_through_interactive_flow(self, tenant, resource):
-
-        context = self._create_auth_context(tenant or _COMMON_TENANT)
+        context = self._create_auth_context(tenant)
         code = context.acquire_user_code(resource, _CLIENT_ID)
         logger.warning(code['message'])
         token_entry = context.acquire_token_with_device_code(resource, code, _CLIENT_ID)
@@ -430,8 +430,7 @@ class SubscriptionFinder(object):
 
     def _create_auth_context(self, tenant, use_token_cache=True):
         token_cache = self._adal_token_cache if use_token_cache else None
-        authority = get_authority_url(tenant)
-        return self._auth_context_factory(authority, token_cache)
+        return self._auth_context_factory(tenant, token_cache)
 
     def _find_using_common_tenant(self, access_token, resource):
         import adal
@@ -514,8 +513,7 @@ class CredsCache(object):
                 cred_file.write(json.dumps(all_creds))
 
     def retrieve_token_for_user(self, username, tenant, resource):
-        authority = get_authority_url(tenant)
-        context = self._auth_ctx_factory(authority, cache=self.adal_token_cache)
+        context = self._auth_ctx_factory(tenant, cache=self.adal_token_cache)
         token_entry = context.acquire_token(resource, username, _CLIENT_ID)
         if not token_entry:
             raise CLIError("Could not retrieve token from local cache, please run 'az login'.")
@@ -530,8 +528,7 @@ class CredsCache(object):
         if not matched:
             raise CLIError("Please run 'az account set' to select active account.")
         cred = matched[0]
-        authority_url = get_authority_url(cred[_SERVICE_PRINCIPAL_TENANT])
-        context = self._auth_ctx_factory(authority_url, None)
+        context = self._auth_ctx_factory(cred[_SERVICE_PRINCIPAL_TENANT], None)
         sp_auth = ServicePrincipalAuth(cred.get(_ACCESS_TOKEN, None) or
                                        cred.get(_SERVICE_PRINCIPAL_CERT_FILE, None))
         token_entry = sp_auth.acquire_token(context, resource, sp_id)


Test patch:
diff --git a/src/azure-cli-core/tests/test_profile.py b/src/azure-cli-core/tests/test_profile.py
index ea3648494..90603c558 100644
--- a/src/azure-cli-core/tests/test_profile.py
+++ b/src/azure-cli-core/tests/test_profile.py
@@ -13,7 +13,7 @@ from adal import AdalError
 from azure.mgmt.resource.subscriptions.models import (SubscriptionState, Subscription,
                                                       SubscriptionPolicies, SpendingLimit)
 from azure.cli.core._profile import (Profile, CredsCache, SubscriptionFinder,
-                                     ServicePrincipalAuth, CLOUD)
+                                     ServicePrincipalAuth, CLOUD, _AUTH_CTX_FACTORY)
 from azure.cli.core.util import CLIError
 
 
@@ -451,6 +451,38 @@ class Test_Profile(unittest.TestCase):  # pylint: disable=too-many-public-method
         mock_auth_context.acquire_token.assert_called_once_with(
             mgmt_resource, self.user1, mock.ANY)
 
+    @mock.patch('adal.AuthenticationContext.acquire_token_with_username_password', autospec=True)
+    @mock.patch('adal.AuthenticationContext.acquire_token', autospec=True)
+    @mock.patch('azure.cli.core._profile.CLOUD', autospec=True)
+    def test_find_subscriptions_thru_username_password_adfs(self, mock_get_cloud, mock_acquire_token,
+                                                            mock_acquire_token_username_password):
+        TEST_ADFS_AUTH_URL = 'https://adfs.local.azurestack.external/adfs'
+
+        def test_acquire_token(self, resource, username, password, client_id):
+            global acquire_token_invoked
+            acquire_token_invoked = True
+            if (self.authority.url == TEST_ADFS_AUTH_URL and self.authority.is_adfs_authority):
+                return Test_Profile.token_entry1
+            else:
+                raise ValueError('AuthContext was not initialized correctly for ADFS')
+
+        mock_acquire_token_username_password.side_effect = test_acquire_token
+        mock_acquire_token.return_value = self.token_entry1
+        mock_arm_client = mock.MagicMock()
+        mock_arm_client.tenants.list.return_value = [TenantStub(self.tenant_id)]
+        mock_arm_client.subscriptions.list.return_value = [self.subscription1]
+        mock_get_cloud.endpoints.active_directory = TEST_ADFS_AUTH_URL
+        finder = SubscriptionFinder(_AUTH_CTX_FACTORY,
+                                    None,
+                                    lambda _: mock_arm_client)
+        mgmt_resource = 'https://management.core.windows.net/'
+        # action
+        subs = finder.find_from_user_account(self.user1, 'bar', None, mgmt_resource)
+
+        # assert
+        self.assertEqual([self.subscription1], subs)
+        self.assertTrue(acquire_token_invoked)
+
     @mock.patch('adal.AuthenticationContext', autospec=True)
     @mock.patch('azure.cli.core._profile.logger', autospec=True)
     def test_find_subscriptions_thru_username_password_with_account_disabled(self, mock_logger,