def test_user_agent(): client_id = "client-id" transport = validating_transport( requests=[ Request(), Request(required_headers={"User-Agent": USER_AGENT}) ], responses=[ get_discovery_response(), mock_response(json_payload=build_aad_response( access_token="**", id_token=build_id_token(aud=client_id))), ], ) # mock local server fakes successful authentication by immediately returning a well-formed response oauth_state = "oauth-state" auth_code_response = {"code": "authorization-code", "state": [oauth_state]} server_class = Mock(return_value=Mock( wait_for_redirect=lambda: auth_code_response)) credential = InteractiveBrowserCredential(client_id=client_id, transport=transport, _server_class=server_class, _cache=TokenCache()) with patch("azure.identity._credentials.browser.uuid.uuid4", lambda: oauth_state): credential.get_token("scope")
def test_no_browser(): transport = validating_transport(requests=[Request()] * 2, responses=[get_discovery_response()] * 2) credential = InteractiveBrowserCredential( client_id="client-id", server_class=Mock(), transport=transport, _cache=TokenCache() ) with pytest.raises(ClientAuthenticationError, match=r".*browser.*"): credential.get_token("scope")
def test_cannot_bind_port(): """get_token should raise CredentialUnavailableError when the redirect listener can't bind a port""" credential = InteractiveBrowserCredential(_server_class=Mock( side_effect=socket.error)) with pytest.raises(CredentialUnavailableError): credential.get_token("scope")
def test_timeout(): """get_token should raise ClientAuthenticationError when the server times out without receiving a redirect""" timeout = 0.01 class GuaranteedTimeout(AuthCodeRedirectServer, object): def handle_request(self): time.sleep(timeout + 0.01) super(GuaranteedTimeout, self).handle_request() # mock transport handles MSAL's tenant discovery transport = Mock(send=lambda _, **__: mock_response( json_payload={ "authorization_endpoint": "https://a/b", "token_endpoint": "https://a/b" })) credential = InteractiveBrowserCredential(timeout=timeout, transport=transport, _cache=TokenCache(), _server_class=GuaranteedTimeout) with patch(WEBBROWSER_OPEN, lambda _: True): with pytest.raises(ClientAuthenticationError) as ex: credential.get_token("scope") assert "timed out" in ex.value.message.lower()
def test_interactive_credential_timeout(): # mock transport handles MSAL's tenant discovery transport = Mock(send=lambda _, **__: mock_response( json_payload={ "authorization_endpoint": "https://a/b", "token_endpoint": "https://a/b" })) # mock local server blocks long enough to exceed the timeout timeout = 0.01 server_instance = Mock( wait_for_redirect=functools.partial(time.sleep, timeout + 0.01)) server_class = Mock(return_value=server_instance) credential = InteractiveBrowserCredential( client_id="guid", client_secret="secret", server_class=server_class, timeout=timeout, transport=transport, instance_discovery= False, # kwargs are passed to MSAL; this one prevents an AAD verification request ) with pytest.raises(ClientAuthenticationError) as ex: credential.get_token("scope") assert "timed out" in ex.value.message.lower()
def test_policies_configurable(): policy = Mock(spec_set=SansIOHTTPPolicy, on_request=Mock()) client_id = "client-id" transport = validating_transport( requests=[Request()] * 2, responses=[ get_discovery_response(), mock_response(json_payload=build_aad_response( access_token="**", id_token=build_id_token(aud=client_id))), ], ) # mock local server fakes successful authentication by immediately returning a well-formed response oauth_state = "oauth-state" auth_code_response = {"code": "authorization-code", "state": [oauth_state]} server_class = Mock(return_value=Mock( wait_for_redirect=lambda: auth_code_response)) credential = InteractiveBrowserCredential(policies=[policy], client_id=client_id, transport=transport, _server_class=server_class, _cache=TokenCache()) with patch("azure.identity._credentials.browser.uuid.uuid4", lambda: oauth_state): credential.get_token("scope") assert policy.on_request.called
def test_browser_credential(): transport = Mock(wraps=RequestsTransport()) credential = InteractiveBrowserCredential(transport=transport) scope = "https://management.azure.com/.default" # N.B. this is valid only in Public Cloud record = credential.authenticate(scopes=(scope, )) assert record.authority assert record.home_account_id assert record.tenant_id assert record.username # credential should have a cached access token for the scope used in authenticate with patch( WEBBROWSER_OPEN, Mock(side_effect=Exception( "credential should authenticate silently"))): token = credential.get_token(scope) assert token.token credential = InteractiveBrowserCredential(transport=transport) token = credential.get_token(scope) assert token.token with patch( WEBBROWSER_OPEN, Mock(side_effect=Exception( "credential should authenticate silently"))): second_token = credential.get_token(scope) assert second_token.token == token.token # every request should have the correct User-Agent for call in transport.send.call_args_list: args, _ = call request = args[0] assert request.headers["User-Agent"] == USER_AGENT
def test_disable_automatic_authentication(): """When configured for strict silent auth, the credential should raise when silent auth fails""" transport = Mock(send=Mock(side_effect=Exception("no request should be sent"))) credential = InteractiveBrowserCredential(disable_automatic_authentication=True, transport=transport) with patch(WEBBROWSER_OPEN, Mock(side_effect=Exception("credential shouldn't try interactive authentication"))): with pytest.raises(AuthenticationRequiredError): credential.get_token("scope")
def test_cannot_bind_redirect_uri(): """When a user specifies a redirect URI, the credential shouldn't attempt to bind another""" server = Mock(side_effect=socket.error) credential = InteractiveBrowserCredential(redirect_uri="http://localhost:42", _server_class=server) with pytest.raises(CredentialUnavailableError): credential.get_token("scope") server.assert_called_once_with("localhost", 42, timeout=ANY)
def test_claims_challenge(): """get_token and authenticate should pass any claims challenge to MSAL token acquisition APIs""" expected_claims = '{"access_token": {"essential": "true"}' auth_code_response = {"code": "authorization-code", "state": ["..."]} server_class = Mock(return_value=Mock( wait_for_redirect=lambda: auth_code_response)) msal_acquire_token_result = dict( build_aad_response(access_token="**", id_token=build_id_token()), id_token_claims=id_token_claims("issuer", "subject", "audience", upn="upn"), ) transport = Mock(send=Mock(side_effect=Exception( "this test mocks MSAL, so no request should be sent"))) credential = InteractiveBrowserCredential(_server_class=server_class, transport=transport) with patch.object(InteractiveBrowserCredential, "_get_app") as get_mock_app: msal_app = get_mock_app() msal_app.initiate_auth_code_flow.return_value = { "auth_uri": "http://localhost" } msal_app.acquire_token_by_auth_code_flow.return_value = msal_acquire_token_result with patch(WEBBROWSER_OPEN, lambda _: True): credential.authenticate(scopes=["scope"], claims=expected_claims) assert msal_app.acquire_token_by_auth_code_flow.call_count == 1 args, kwargs = msal_app.acquire_token_by_auth_code_flow.call_args assert kwargs["claims_challenge"] == expected_claims with patch(WEBBROWSER_OPEN, lambda _: True): credential.get_token("scope", claims=expected_claims) assert msal_app.acquire_token_by_auth_code_flow.call_count == 2 args, kwargs = msal_app.acquire_token_by_auth_code_flow.call_args assert kwargs["claims_challenge"] == expected_claims msal_app.get_accounts.return_value = [{ "home_account_id": credential._auth_record.home_account_id }] msal_app.acquire_token_silent_with_error.return_value = msal_acquire_token_result credential.get_token("scope", claims=expected_claims) assert msal_app.acquire_token_silent_with_error.call_count == 1 args, kwargs = msal_app.acquire_token_silent_with_error.call_args assert kwargs["claims_challenge"] == expected_claims
def test_no_browser(): """The credential should raise CredentialUnavailableError when it can't open a browser""" transport = validating_transport(requests=[Request()] * 2, responses=[get_discovery_response()] * 2) credential = InteractiveBrowserCredential(client_id="client-id", _server_class=Mock(), transport=transport) with patch(InteractiveBrowserCredential.__module__ + "._open_browser", lambda _: False): with pytest.raises(CredentialUnavailableError, match=r".*browser.*"): credential.get_token("scope")
def test_policies_configurable(): # the policy raises an exception so this test can run without authenticating i.e. opening a browser expected_message = "test_policies_configurable" policy = Mock(spec_set=SansIOHTTPPolicy, on_request=Mock(side_effect=Exception(expected_message))) credential = InteractiveBrowserCredential(policies=[policy]) with pytest.raises(ClientAuthenticationError) as ex: credential.get_token("scope") assert expected_message in ex.value.message assert policy.on_request.called
def test_redirect_uri(): """The credential should configure the redirect server to use a given redirect_uri""" expected_hostname = "localhost" expected_port = 42424 expected_message = "test_redirect_uri" server = Mock(side_effect=Exception(expected_message)) # exception prevents this test actually authenticating credential = InteractiveBrowserCredential( redirect_uri="htps://{}:{}".format(expected_hostname, expected_port), _server_class=server ) with pytest.raises(ClientAuthenticationError) as ex: credential.get_token("scope") assert expected_message in ex.value.message server.assert_called_once_with(expected_hostname, expected_port, timeout=ANY)
def __init__(self, vault_url, credential=None): ''' Parameters: vault_url (str) – RL of the vault the client will access. This is also called the vault's "DNS Name". credential (str) – An object which can provide an access token for the vault, such as a service principal credential Example: .. code-block:: python import pynomial as pyn client = pyn.KeyVaultclient(vault_url='https://keyvaulttestinstance.vault.azure.net/') retrieved_secret = client.get_secret('MyPassword') retrieved_secret.value .. code-block:: python import pynomial as pyn credential = pyn.SPCredential() client = pyn.KeyVaultclient(vault_url='https://keyvaulttestinstance.vault.azure.net/', credential=credential) retrieved_secret = client.get_secret('MyPassword').value ''' from azure.identity import InteractiveBrowserCredential if credential is None: credential = InteractiveBrowserCredential() self.token = credential.get_token() self = super().__init__(vault_url=vault_url, credential=credential)
def test_no_browser(): discovery_response = mock_response( json_payload={ name: "https://foo/bar" for name in ("authorization_endpoint", "token_endpoint", "tenant_discovery_endpoint") }) transport = validating_transport( requests=[Request()] * 2, responses=[discovery_response, discovery_response]) credential = InteractiveBrowserCredential(client_id="client-id", client_secret="secret", server_class=Mock(), transport=transport) with pytest.raises(ClientAuthenticationError, match=r".*browser.*"): credential.get_token("scope")
def test_authenticate(): client_id = "client-id" environment = "localhost" issuer = "https://" + environment tenant_id = "some-tenant" authority = issuer + "/" + tenant_id access_token = "***" scope = "scope" # mock AAD response with id token object_id = "object-id" home_tenant = "home-tenant-id" username = "******" id_token = build_id_token(aud=client_id, iss=issuer, object_id=object_id, tenant_id=home_tenant, username=username) auth_response = build_aad_response( uid=object_id, utid=home_tenant, access_token=access_token, refresh_token="**", id_token=id_token ) transport = validating_transport( requests=[Request(url_substring=issuer)] * 3, responses=[get_discovery_response(authority)] * 2 + [mock_response(json_payload=auth_response)], ) # mock local server fakes successful authentication by immediately returning a well-formed response oauth_state = "state" auth_code_response = {"code": "authorization-code", "state": [oauth_state]} server_class = Mock(return_value=Mock(wait_for_redirect=lambda: auth_code_response)) with patch(InteractiveBrowserCredential.__module__ + ".uuid.uuid4", lambda: oauth_state): with patch(WEBBROWSER_OPEN, lambda _: True): credential = InteractiveBrowserCredential( _cache=TokenCache(), authority=environment, client_id=client_id, server_class=server_class, tenant_id=tenant_id, transport=transport, ) record = credential._authenticate(scopes=(scope,)) assert record.authority == environment assert record.home_account_id == object_id + "." + home_tenant assert record.tenant_id == home_tenant assert record.username == username # credential should have a cached access token for the scope used in authenticate with patch(WEBBROWSER_OPEN, Mock(side_effect=Exception("credential should authenticate silently"))): token = credential.get_token(scope) assert token.token == access_token
def test_wsl_fallback(uname, is_wsl): """the credential should invoke powershell.exe to open a browser in WSL when webbrowser.open fails""" auth_uri = "http://localhost" expected_access_token = "**" msal_acquire_token_result = dict( build_aad_response(access_token=expected_access_token, id_token=build_id_token()), id_token_claims=id_token_claims("issuer", "subject", "audience", upn="upn"), ) msal_app = Mock( initiate_auth_code_flow=Mock(return_value={"auth_uri": auth_uri}), acquire_token_by_auth_code_flow=Mock( return_value=msal_acquire_token_result), ) transport = Mock(send=Mock(side_effect=Exception( "this test mocks MSAL, so no request should be sent"))) credential = InteractiveBrowserCredential(_server_class=Mock(), transport=transport) with patch(InteractiveBrowserCredential.__module__ + ".subprocess.call") as subprocess_call: subprocess_call.return_value = 0 with patch(InteractiveBrowserCredential.__module__ + ".platform.uname", lambda: uname): with patch.object(InteractiveBrowserCredential, "_get_app", lambda _: msal_app): with patch(WEBBROWSER_OPEN, lambda _: False): try: token = credential.get_token("scope") except CredentialUnavailableError: assert not is_wsl, "credential should invoke powershell.exe in WSL" return assert is_wsl, "credential should raise CredentialUnavailableError when not in WSL" assert token.token == expected_access_token assert subprocess_call.call_count == 1 args, kwargs = subprocess_call.call_args assert args[0][0] == "powershell.exe" assert auth_uri in args[0][-1] if platform.python_version() >= "3.3": assert "timeout" in kwargs
def test_interactive_credential(): oauth_state = "state" expected_token = "access-token" transport = validating_transport( requests=[Request()] * 2, # not validating requests because they're formed by MSAL responses=[ # expecting tenant discovery then a token request mock_response( json_payload={ "authorization_endpoint": "https://a/b", "token_endpoint": "https://a/b" }), mock_response( json_payload={ "access_token": expected_token, "expires_in": 42, "token_type": "Bearer", "ext_expires_in": 42, }), ], ) # mock local server fakes successful authentication by immediately returning a well-formed response auth_code_response = {"code": "authorization-code", "state": [oauth_state]} server_class = Mock(return_value=Mock( wait_for_redirect=lambda: auth_code_response)) credential = InteractiveBrowserCredential( client_id="guid", client_secret="secret", server_class=server_class, transport=transport, instance_discovery= False, # kwargs are passed to MSAL; this one prevents an AAD verification request ) # ensure the request beginning the flow has a known state value with patch("azure.identity._credentials.browser.uuid.uuid4", lambda: oauth_state): token = credential.get_token("scope") assert token.token == expected_token
def test_interactive_credential(mock_open): mock_open.side_effect = _validate_auth_request_url oauth_state = "state" client_id = "client-id" expected_refresh_token = "refresh-token" expected_token = "access-token" expires_in = 3600 authority = "authority" tenant_id = "tenant_id" endpoint = "https://{}/{}".format(authority, tenant_id) discovery_response = mock_response( json_payload={ name: endpoint for name in ("authorization_endpoint", "token_endpoint", "tenant_discovery_endpoint") }) transport = validating_transport( requests=[Request(url_substring=endpoint)] * 3 + [ Request(authority=authority, url_substring=endpoint, required_data={"refresh_token": expected_refresh_token}) ], responses=[ discovery_response, # instance discovery discovery_response, # tenant discovery mock_response(json_payload=build_aad_response( access_token=expected_token, expires_in=expires_in, refresh_token=expected_refresh_token, uid="uid", utid="utid", token_type="Bearer", )), mock_response( json_payload=build_aad_response(access_token=expected_token, expires_in=expires_in, token_type="Bearer")), ], ) # mock local server fakes successful authentication by immediately returning a well-formed response auth_code_response = {"code": "authorization-code", "state": [oauth_state]} server_class = Mock(return_value=Mock( wait_for_redirect=lambda: auth_code_response)) credential = InteractiveBrowserCredential( authority=authority, tenant_id=tenant_id, client_id=client_id, client_secret="secret", server_class=server_class, transport=transport, instance_discovery=False, validate_authority=False, ) # The credential's auth code request includes a uuid which must be included in the redirect. Patching to # set the uuid requires less code here than a proper mock server. with patch("azure.identity._credentials.browser.uuid.uuid4", lambda: oauth_state): token = credential.get_token("scope") assert token.token == expected_token assert mock_open.call_count == 1 # token should be cached, get_token shouldn't prompt again token = credential.get_token("scope") assert token.token == expected_token assert mock_open.call_count == 1 # As of MSAL 1.0.0, applications build a new client every time they redeem a refresh token. # Here we patch the private method they use for the sake of test coverage. # TODO: this will probably break when this MSAL behavior changes app = credential._get_app() app._build_client = lambda *_: app.client # pylint:disable=protected-access now = time.time() # expired access token -> credential should use refresh token instead of prompting again with patch("time.time", lambda: now + expires_in): token = credential.get_token("scope") assert token.token == expected_token assert mock_open.call_count == 1 # ensure all expected requests were sent assert transport.send.call_count == 4
def _get_creds_from_login(self, authentication, reason=None): # use normal Key Value from azure.keyvault.secrets import SecretClient if authentication == "auto": authentication = "browser" if pc_utils.has_gui() else "device-code" if authentication == "browser": console.print("authenticating with azure thru browser... ", flush=True, end="") from azure.identity import InteractiveBrowserCredential if self.azure_tenant_id is not None: credential = InteractiveBrowserCredential( tenant_id=self.azure_tenant_id) else: credential = InteractiveBrowserCredential() elif authentication == "device-code": # console.print("authenticating with azure thru device code... ", flush=True, end="") from azure.identity import DeviceCodeCredential from azure.identity._constants import AZURE_CLI_CLIENT_ID console.print( "using device-code authorization (Azure AD currently requires 2-4 authenications here)" ) if self.azure_tenant_id is not None: credential = DeviceCodeCredential( tenant_id=self.azure_tenant_id, client_id=AZURE_CLI_CLIENT_ID) else: credential = DeviceCodeCredential( client_id=AZURE_CLI_CLIENT_ID) else: errors.syntax_error( "unrecognized authentication type '{}'".format(authentication)) new_creds = True outer_token = credential.get_token() token = outer_token.token # expires = outer_token[1] # elapsed = expires - time.time() #print(" [new token expires in {:.2f} mins] ".format(elapsed/60), end="") # get keys from keyvault self.client = SecretClient(self.vault_url, credential=credential) key_text = self.get_secret_live("xt-keys") console.print("authenticated successfully", flush=True) #xt_client_cert = self.get_secret_live("xt-clientcert") xt_server_cert = self.get_secret_live("xt-servercert") # write all our creds to self.keys self.apply_creds(key_text) self.keys["xt_server_cert"] = xt_server_cert self.keys["object_id"] = self.get_me_graph_property(token, "id") # return creds as json string return json.dumps(self.keys)
def test_no_browser(): transport = validating_transport(requests=[Request()] * 2, responses=[get_discovery_response()] * 2) credential = InteractiveBrowserCredential(client_id="client-id", _server_class=Mock(), transport=transport) with pytest.raises(ClientAuthenticationError, match=r".*browser.*"): with patch(WEBBROWSER_OPEN, lambda _: False): credential.get_token("scope")
def test_interactive_credential(mock_open, redirect_url): mock_open.side_effect = _validate_auth_request_url oauth_state = "state" client_id = "client-id" expected_refresh_token = "refresh-token" expected_token = "access-token" expires_in = 3600 authority = "authority" tenant_id = "tenant-id" endpoint = "https://{}/{}".format(authority, tenant_id) transport = msal_validating_transport( endpoint="https://{}/{}".format(authority, tenant_id), requests=[Request(url_substring=endpoint)] + [ Request(authority=authority, url_substring=endpoint, required_data={"refresh_token": expected_refresh_token}) ], responses=[ mock_response(json_payload=build_aad_response( access_token=expected_token, expires_in=expires_in, refresh_token=expected_refresh_token, uid="uid", utid=tenant_id, id_token=build_id_token(aud=client_id, object_id="uid", tenant_id=tenant_id, iss=endpoint), token_type="Bearer", )), mock_response( json_payload=build_aad_response(access_token=expected_token, expires_in=expires_in, token_type="Bearer")), ], ) # mock local server fakes successful authentication by immediately returning a well-formed response auth_code_response = {"code": "authorization-code", "state": [oauth_state]} server_class = Mock(return_value=Mock( wait_for_redirect=lambda: auth_code_response)) args = { "authority": authority, "tenant_id": tenant_id, "client_id": client_id, "transport": transport, "_cache": TokenCache(), "_server_class": server_class, } if redirect_url: # avoid passing redirect_url=None args["redirect_uri"] = redirect_url credential = InteractiveBrowserCredential(**args) # The credential's auth code request includes a uuid which must be included in the redirect. Patching to # set the uuid requires less code here than a proper mock server. with patch("azure.identity._credentials.browser.uuid.uuid4", lambda: oauth_state): token = credential.get_token("scope") assert token.token == expected_token assert mock_open.call_count == 1 assert server_class.call_count == 1 if redirect_url: server_class.assert_called_once_with(redirect_url, timeout=ANY) # token should be cached, get_token shouldn't prompt again token = credential.get_token("scope") assert token.token == expected_token assert mock_open.call_count == 1 assert server_class.call_count == 1 # expired access token -> credential should use refresh token instead of prompting again now = time.time() with patch("time.time", lambda: now + expires_in): token = credential.get_token("scope") assert token.token == expected_token assert mock_open.call_count == 1 # ensure all expected requests were sent assert transport.send.call_count == 4