diff --git a/duo_universal/client.py b/duo_universal/client.py index c6f6f42..c6241d7 100644 --- a/duo_universal/client.py +++ b/duo_universal/client.py @@ -45,6 +45,7 @@ OAUTH_V1_AUTHORIZE_ENDPOINT = "https://{}/oauth/v1/authorize" OAUTH_V1_TOKEN_ENDPOINT = "https://{}/oauth/v1/token" DEFAULT_CA_CERT_PATH = os.path.join(os.path.dirname(__file__), 'ca_certs.pem') +CA_BUNDLE_VERSION = "1.0" CLIENT_ASSERT_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" @@ -316,10 +317,15 @@ def exchange_authorization_code_for_2fa_result(self, duoCode, username, nonce=No algorithm='HS512') } try: + ca_pinning_status = "disabled" if self._disable_ca_pinning else "enabled" user_agent = ("duo_universal_python/{version} " - "python/{python_version} {os_name}").format(version=__version__, - python_version=platform.python_version(), - os_name=platform.platform()) + "python/{python_version} {os_name} " + "ca_bundle/{ca_bundle_version} " + "(ca_pinning={ca_pinning_status})").format(version=__version__, + python_version=platform.python_version(), + os_name=platform.platform(), + ca_bundle_version=CA_BUNDLE_VERSION, + ca_pinning_status=ca_pinning_status) response = requests.post(token_endpoint, params=all_args, headers={"user-agent": diff --git a/tests/test_setup_client.py b/tests/test_setup_client.py index 03dd04f..296fc3a 100644 --- a/tests/test_setup_client.py +++ b/tests/test_setup_client.py @@ -253,5 +253,51 @@ def test_token_exchange_pinning_enabled_uses_bundled_certs(self, requests_mock): self.assertEqual(kwargs['verify'], client.DEFAULT_CA_CERT_PATH) +class TestUserAgent(unittest.TestCase): + + @patch('requests.post') + def test_user_agent_includes_ca_bundle_version(self, requests_mock): + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = {'id_token': 'fake'} + requests_mock.return_value = mock_response + c = client.Client(CLIENT_ID, CLIENT_SECRET, HOST, REDIRECT_URI) + try: + c.exchange_authorization_code_for_2fa_result('code', 'user') + except client.DuoException: + pass + _, kwargs = requests_mock.call_args + self.assertIn('ca_bundle/1.0', kwargs['headers']['user-agent']) + + @patch('requests.post') + def test_user_agent_ca_pinning_enabled(self, requests_mock): + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = {'id_token': 'fake'} + requests_mock.return_value = mock_response + c = client.Client(CLIENT_ID, CLIENT_SECRET, HOST, REDIRECT_URI) + try: + c.exchange_authorization_code_for_2fa_result('code', 'user') + except client.DuoException: + pass + _, kwargs = requests_mock.call_args + self.assertIn('(ca_pinning=enabled)', kwargs['headers']['user-agent']) + + @patch('requests.post') + def test_user_agent_ca_pinning_disabled(self, requests_mock): + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = {'id_token': 'fake'} + requests_mock.return_value = mock_response + c = client.Client(CLIENT_ID, CLIENT_SECRET, HOST, REDIRECT_URI, + disable_ca_pinning=True) + try: + c.exchange_authorization_code_for_2fa_result('code', 'user') + except client.DuoException: + pass + _, kwargs = requests_mock.call_args + self.assertIn('(ca_pinning=disabled)', kwargs['headers']['user-agent']) + + if __name__ == '__main__': unittest.main()