def _test_device_flow( self, client_id=None, authority=None, scope=None, **ignored): assert client_id and authority and scope self.app = msal.PublicClientApplication( client_id, authority=authority, http_client=MinimalHttpClient()) flow = self.app.initiate_device_flow(scopes=scope) assert "user_code" in flow, "DF does not seem to be provisioned: %s".format( json.dumps(flow, indent=4)) logger.info(flow["message"]) duration = 60 logger.info("We will wait up to %d seconds for you to sign in" % duration) flow["expires_at"] = min( # Shorten the time for quick test flow["expires_at"], time.time() + duration) result = self.app.acquire_token_by_device_flow(flow) self.assertLoosely( # It will skip this test if there is no user interaction result, assertion=lambda: self.assertIn('access_token', result), skippable_errors=self.app.client.DEVICE_FLOW_RETRIABLE_ERRORS) if "access_token" not in result: self.skip("End user did not complete Device Flow in time") self.assertCacheWorksForUser(result, scope, username=None) result["access_token"] = result["refresh_token"] = "************" logger.info( "%s obtained tokens: %s", self.id(), json.dumps(result, indent=4))
def test_memorize(self): # We use a real authority so the constructor can finish tenant discovery authority = "https://login.microsoftonline.com/common" self.assertNotIn(authority, Authority._domains_without_user_realm_discovery) a = Authority(authority, MinimalHttpClient(), validate_authority=False) try: # We now pretend this authority supports no User Realm Discovery class MockResponse(object): status_code = 404 a.user_realm_discovery("*****@*****.**", response=MockResponse()) self.assertIn( "login.microsoftonline.com", Authority._domains_without_user_realm_discovery, "user_realm_discovery() should memorize domains not supporting URD" ) a.user_realm_discovery( "*****@*****.**", response= "This would cause exception if memorization did not work") finally: # MUST NOT let the previous test changes affect other test cases Authority._domains_without_user_realm_discovery = set([])
def get_lab_app( env_client_id="LAB_APP_CLIENT_ID", env_client_secret="LAB_APP_CLIENT_SECRET", ): """Returns the lab app as an MSAL confidential client. Get it from environment variables if defined, otherwise fall back to use MSI. """ if os.getenv(env_client_id) and os.getenv(env_client_secret): # A shortcut mainly for running tests on developer's local development machine # or it could be setup on Travis CI # https://docs.travis-ci.com/user/environment-variables/#defining-variables-in-repository-settings # Data came from here # https://docs.msidlab.com/accounts/confidentialclient.html logger.info("Using lab app defined by ENV variables %s and %s", env_client_id, env_client_secret) client_id = os.getenv(env_client_id) client_secret = os.getenv(env_client_secret) else: logger.info("ENV variables %s and/or %s are not defined. Fall back to MSI.", env_client_id, env_client_secret) # See also https://microsoft.sharepoint-df.com/teams/MSIDLABSExtended/SitePages/Programmatically-accessing-LAB-API's.aspx raise unittest.SkipTest("MSI-based mechanism has not been implemented yet") return msal.ConfidentialClientApplication(client_id, client_secret, authority="https://login.microsoftonline.com/" "72f988bf-86f1-41af-91ab-2d7cd011db47", # Microsoft tenant ID http_client=MinimalHttpClient())
def setUpClass(cls): http_client = MinimalHttpClient() if "client_certificate" in CONFIG: private_key_path = CONFIG["client_certificate"]["private_key_path"] with open(os.path.join(THIS_FOLDER, private_key_path)) as f: private_key = f.read() # Expecting PEM format cls.client = Client( CONFIG["openid_configuration"], CONFIG['client_id'], http_client=http_client, client_assertion=JwtSigner( private_key, algorithm="RS256", sha1_thumbprint=CONFIG["client_certificate"]["thumbprint"] ).sign_assertion( audience=CONFIG["openid_configuration"]["token_endpoint"], issuer=CONFIG["client_id"], ), client_assertion_type=Client.CLIENT_ASSERTION_TYPE_JWT, ) else: cls.client = Client(CONFIG["openid_configuration"], CONFIG['client_id'], http_client=http_client, client_secret=CONFIG.get('client_secret'))
def test_subject_name_issuer_authentication(self): self.skipUnlessWithConfig(["client_id", "client_certificate"]) client_cert = self.config["client_certificate"] assert "private_key_path" in client_cert and "thumbprint" in client_cert if not "public_certificate" in client_cert: self.skipTest( "Skipping SNI test due to lack of public_certificate") with open(os.path.join(THIS_FOLDER, client_cert['private_key_path'])) as f: private_key = f.read() # Should be in PEM format with open(os.path.join(THIS_FOLDER, client_cert['public_certificate'])) as f: public_certificate = f.read() self.app = msal.ConfidentialClientApplication( self.config['client_id'], authority=self.config["authority"], client_credential={ "private_key": private_key, "thumbprint": self.config["thumbprint"], "public_certificate": public_certificate, }, http_client=MinimalHttpClient()) scope = self.config.get("scope", []) result = self.app.acquire_token_for_client(scope) self.assertIn('access_token', result) self.assertCacheWorksForApp(result, scope)
def test_acquire_token_silent_with_an_empty_cache_should_return_none(self): config = self.get_lab_user( usertype="cloud", azureenvironment=self.environment, publicClient="no") app = msal.ConfidentialClientApplication( config['client_id'], authority=config['authority'], http_client=MinimalHttpClient()) result = app.acquire_token_silent(scopes=config['scope'], account=None) self.assertEqual(result, None)
def _test_acquire_token_by_client_secret( self, client_id=None, client_secret=None, authority=None, scope=None, **ignored): assert client_id and client_secret and authority and scope app = msal.ConfidentialClientApplication( client_id, client_credential=client_secret, authority=authority, http_client=MinimalHttpClient()) result = app.acquire_token_for_client(scope) self.assertIsNotNone(result.get("access_token"), "Got %s instead" % result)
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)
def test_lessknown_host_will_return_a_set_of_v1_endpoints(self): # This is an observation for current (2016-10) server-side behavior. # It is probably not a strict API contract. I simply mention it here. less_known = 'login.windows.net' # less.known.host/ v1_token_endpoint = 'https://{}/common/oauth2/token'.format(less_known) a = Authority('https://{}/common'.format(less_known), MinimalHttpClient()) self.assertEqual(a.token_endpoint, v1_token_endpoint) self.assertNotIn('v2.0', a.token_endpoint)
def _test_acquire_token_obo(self, config_pca, config_cca): # 1. An app obtains a token representing a user, for our mid-tier service pca = msal.PublicClientApplication(config_pca["client_id"], authority=config_pca["authority"], http_client=MinimalHttpClient()) pca_result = pca.acquire_token_by_username_password( config_pca["username"], config_pca["password"], scopes=config_pca["scope"], ) self.assertIsNotNone( pca_result.get("access_token"), "PCA failed to get AT because %s" % json.dumps(pca_result, indent=2)) # 2. Our mid-tier service uses OBO to obtain a token for downstream service cca = msal.ConfidentialClientApplication( config_cca["client_id"], client_credential=config_cca["client_secret"], authority=config_cca["authority"], http_client=MinimalHttpClient(), # token_cache= ..., # Default token cache is all-tokens-store-in-memory. # That's fine if OBO app uses short-lived msal instance per session. # Otherwise, the OBO app need to implement a one-cache-per-user setup. ) cca_result = cca.acquire_token_on_behalf_of(pca_result['access_token'], config_cca["scope"]) self.assertNotEqual(None, cca_result.get("access_token"), str(cca_result)) # 3. Now the OBO app can simply store downstream token(s) in same session. # Alternatively, if you want to persist the downstream AT, and possibly # the RT (if any) for prolonged access even after your own AT expires, # now it is the time to persist current cache state for current user. # Assuming you already did that (which is not shown in this test case), # the following part shows one of the ways to obtain an AT from cache. username = cca_result.get("id_token_claims", {}).get("preferred_username") self.assertEqual(config_cca["username"], username) if username: # A precaution so that we won't use other user's token account = cca.get_accounts(username=username)[0] result = cca.acquire_token_silent(config_cca["scope"], account) self.assertEqual(cca_result["access_token"], result["access_token"])
def test_authority_honors_a_patched_requests(self): # First, we test that the original, unmodified authority is working a = msal.authority.Authority( "https://login.microsoftonline.com/common", MinimalHttpClient()) self.assertEqual( a.authorization_endpoint, 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize') original = msal.authority.requests try: # Now we mimic a (discouraged) practice of patching authority.requests msal.authority.requests = DummyHttpClient() # msal.authority is expected to honor that patch. with self.assertRaises(RuntimeError): a = msal.authority.Authority( "https://login.microsoftonline.com/common", MinimalHttpClient()) finally: # Tricky: # Unpatch is necessary otherwise other test cases would be affected msal.authority.requests = original
def test_rt_being_added(self): client = Client( {"token_endpoint": "http://example.com/token"}, "client_id", http_client=MinimalHttpClient(), on_obtaining_tokens=lambda event: self.assertEqual( "new", event["response"].get("refresh_token")), on_updating_rt=lambda rt_item, new_rt: self.fail( "This should not be called here"), ) client.obtain_token_by_authorization_code("code", post=self._dummy)
def test_invalid_host_skipping_validation_can_be_turned_off(self): try: Authority('https://example.com/invalid', MinimalHttpClient(), validate_authority=False) except ValueError as e: if "invalid_instance" in str(e): # Imprecise but good enough self.fail( "validate_authority=False should turn off validation") except: # Could be requests...RequestException, json...JSONDecodeError, etc. pass # Those are expected for this unittest case
def test_client_secret(self): self.skipUnlessWithConfig(["client_id", "client_secret"]) self.app = msal.ConfidentialClientApplication( self.config["client_id"], client_credential=self.config.get("client_secret"), authority=self.config.get("authority"), http_client=MinimalHttpClient()) scope = self.config.get("scope", []) result = self.app.acquire_token_for_client(scope) self.assertIn('access_token', result) self.assertCacheWorksForApp(result, scope)
def test_rt_being_updated(self): old_rt = {"refresh_token": "old"} client = Client( {"token_endpoint": "http://example.com/token"}, "client_id", http_client=MinimalHttpClient(), on_obtaining_tokens=lambda event: self.assertNotIn( "refresh_token", event["response"]), on_updating_rt=lambda old, new: # TODO: ensure it being called (self.assertEqual(old_rt, old), self.assertEqual("new", new)), ) client.obtain_token_by_refresh_token({"refresh_token": "old"}, post=self._dummy)
def _test_username_password(self, authority=None, client_id=None, username=None, password=None, scope=None, **ignored): assert authority and client_id and username and password and scope self.app = msal.PublicClientApplication( client_id, authority=authority, http_client=MinimalHttpClient()) result = self.app.acquire_token_by_username_password( username, password, scopes=scope) self.assertLoosely(result) # self.assertEqual(None, result.get("error"), str(result)) self.assertCacheWorksForUser( result, scope, username=username if ".b2clogin.com" not in authority else None, )
def test_rt_being_migrated(self): old_rt = {"refresh_token": "old"} client = Client( {"token_endpoint": "http://example.com/token"}, "client_id", http_client=MinimalHttpClient(), on_obtaining_tokens=lambda event: self.assertEqual( "new", event["response"].get("refresh_token")), on_updating_rt=lambda rt_item, new_rt: self.fail( "This should not be called here"), ) client.obtain_token_by_refresh_token({"refresh_token": "old"}, on_updating_rt=False, post=self._dummy)
def test_client_certificate(self): self.skipUnlessWithConfig(["client_id", "client_certificate"]) client_cert = self.config["client_certificate"] assert "private_key_path" in client_cert and "thumbprint" in client_cert with open(os.path.join(THIS_FOLDER, client_cert['private_key_path'])) as f: private_key = f.read() # Should be in PEM format self.app = msal.ConfidentialClientApplication( self.config['client_id'], {"private_key": private_key, "thumbprint": client_cert["thumbprint"]}, http_client=MinimalHttpClient()) scope = self.config.get("scope", []) result = self.app.acquire_token_for_client(scope) self.assertIn('access_token', result) self.assertCacheWorksForApp(result, scope)
def _get_app_and_auth_code( client_id, client_secret=None, authority="https://login.microsoftonline.com/common", port=44331, scopes=["https://graph.microsoft.com/.default"], # Microsoft Graph **kwargs): from msal.oauth2cli.authcode import obtain_auth_code app = msal.ClientApplication( client_id, client_secret, authority=authority, http_client=MinimalHttpClient()) redirect_uri = "http://localhost:%d" % port ac = obtain_auth_code(port, auth_uri=app.get_authorization_request_url( scopes, redirect_uri=redirect_uri, **kwargs)) assert ac is not None return (app, ac, redirect_uri)
def setUp(self): self.authority_url = "https://login.microsoftonline.com/common" self.authority = msal.authority.Authority(self.authority_url, MinimalHttpClient()) self.scopes = ["s1", "s2"] self.uid = "my_uid" self.utid = "my_utid" self.account = {"home_account_id": "{}.{}".format(self.uid, self.utid)} self.frt = "what the frt" self.cache = msal.SerializableTokenCache() self.preexisting_family_app_id = "preexisting_family_app" self.cache.add({ # Pre-populate a FRT "client_id": self.preexisting_family_app_id, "scope": self.scopes, "token_endpoint": "{}/oauth2/v2.0/token".format(self.authority_url), "response": TokenCacheTestCase.build_response( access_token="Siblings won't share AT. test_remove_account() will.", id_token=TokenCacheTestCase.build_id_token(aud=self.preexisting_family_app_id), uid=self.uid, utid=self.utid, refresh_token=self.frt, foci="1"), }) # The add(...) helper populates correct home_account_id for future searching
def setUp(self): self.authority_url = "https://login.microsoftonline.com/common" self.authority = msal.authority.Authority( self.authority_url, MinimalHttpClient()) self.scopes = ["s1", "s2"] self.uid = "my_uid" self.utid = "my_utid" self.account = {"home_account_id": "{}.{}".format(self.uid, self.utid)} self.rt = "this is a rt" self.cache = msal.SerializableTokenCache() self.client_id = "my_app" self.cache.add({ # Pre-populate the cache "client_id": self.client_id, "scope": self.scopes, "token_endpoint": "{}/oauth2/v2.0/token".format(self.authority_url), "response": TokenCacheTestCase.build_response( access_token="an expired AT to trigger refresh", expires_in=-99, uid=self.uid, utid=self.utid, refresh_token=self.rt), }) # The add(...) helper populates correct home_account_id for future searching self.app = ClientApplication( self.client_id, authority=self.authority_url, token_cache=self.cache)
def test_unknown_host_wont_pass_instance_discovery(self): _assert = getattr(self, "assertRaisesRegex", self.assertRaisesRegexp) # Hack with _assert(ValueError, "invalid_instance"): Authority('https://example.com/tenant_doesnt_matter_in_this_case', MinimalHttpClient())