diff --git a/AUTHORS b/AUTHORS index 35e150a..5d89409 100644 --- a/AUTHORS +++ b/AUTHORS @@ -26,3 +26,4 @@ Daniel Federschmidt Robert Dyer Patrick SzczepaƄski Hassen Ben Tanfous +Melissa Eckardt diff --git a/changes/227.removal b/changes/227.removal new file mode 100644 index 0000000..05b28ce --- /dev/null +++ b/changes/227.removal @@ -0,0 +1,12 @@ +``auth_app()`` now works with Taiga >= 3.1.0 (2016), which stopped returning a +``cyphered_token``. This is a breaking change: + +* the ``app_secret`` argument has been removed and ``state`` is now required; +* the method no longer sets ``self.token``/reinitializes the client, it returns + the plain token string instead - reuse it by passing it to the ``TaigaAPI`` + constructor as ``token=, token_type="Application"``; +* the unmaintained ``pyjwkest`` dependency (and its vulnerable transitive + dependency ``future``, `GHSA-xqrq-4mgf-ff32 + `_) is no longer required. + +See :ref:`Application authentication` in the usage docs for the updated flow. diff --git a/docs/usage.rst b/docs/usage.rst index e7f5a11..f56bb85 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -78,6 +78,41 @@ To ignore SSL certificate verification (use at your own risk!) use ``tls_verify` tls_verify=False ) +.. _Application authentication: + +Application authentication +=========================== + +To authenticate as a Taiga application (as opposed to a user), exchange the +``auth_code`` configured in the Taiga admin UI for a token with ``auth_app()``, +then reuse that token to build the client: + +.. code:: python + + from taiga import TaigaAPI + + api = TaigaAPI(host='http://taiga.my.host.org') + token = api.auth_app( + app_id='app-id', + auth_code='auth-code', + state='some-state', + ) + + # store `token` and reuse it - the auth_code is invalidated by Taiga + # after this call, and the token itself does not expire + api = TaigaAPI( + host='http://taiga.my.host.org', + token=token, + token_type='Application', + ) + +.. versionchanged:: 2.0.0 + Prior to this version ``auth_app()`` relied on the ``app_secret``/ + ``cyphered_token`` flow removed from Taiga since 3.1.0 (2016), and set + ``self.token`` directly instead of returning it. Update any code calling + ``auth_app()`` to drop ``app_secret``, pass ``state`` explicitly, and use + the returned token as shown above. + ****************************************************** Get projects, user stories, task and issues ****************************************************** diff --git a/setup.cfg b/setup.cfg index c85baca..f3cc12c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,7 +27,6 @@ include_package_data = True install_requires = requests>2.11 python-dateutil>=2.4 - pyjwkest>=1.0 packages = taiga python_requires = >=3.11 setup_requires = diff --git a/taiga/client.py b/taiga/client.py index c9006ab..fc30b44 100644 --- a/taiga/client.py +++ b/taiga/client.py @@ -161,13 +161,19 @@ def auth(self, username, password): ) self._init_resources() - def auth_app(self, app_id, app_secret, auth_code, state=""): + def auth_app(self, app_id, auth_code, state): """ - Authenticate an app + Retrieve an application token. + This only works once per token; in order to reset it, the auth code needs + to be set again in the Taiga admin UI. + + In order to use the token, initialize TaigaAPI with token_type="Application" + and token="token from this function". :param app_id: the app id - :param app_secret: the app secret - :param auth_code: the app auth code + :param auth_code: app auth code as specified in Taiga + :param state: state as specified in Taiga (any string; must not be empty) + :return: token string """ headers = {"Content-type": "application/json"} payload = {"application": app_id, "auth_code": auth_code, "state": state} @@ -180,31 +186,12 @@ def auth_app(self, app_id, app_secret, auth_code, state=""): raise exceptions.TaigaRestException(full_url, 400, "NETWORK ERROR", "POST") if response.status_code != 200: raise exceptions.TaigaRestException(full_url, response.status_code, response.text, "POST") - cyphered_token = response.json().get("cyphered_token", "") - if cyphered_token: - from jwkest.jwe import JWE - from jwkest.jwk import SYMKey - - sym_key = SYMKey(key=app_secret, alg="A128KW") - data, success = JWE().decrypt(cyphered_token, keys=[sym_key]), True - if isinstance(data, tuple): - data, success = data - try: - self.token = json.loads(data.decode("utf-8")).get("token", None) - except ValueError: # pragma: no cover - self.token = None - if not success: - self.token = None - else: - self.token = None - - if self.token is None: + token = response.json().get("token", None) + + if token is None: raise exceptions.TaigaRestException(full_url, 400, "INVALID TOKEN", "POST") - self.raw_request = RequestMaker( - "/api/v1", self.host, self.token, "Application", self.tls_verify, proxies=self.proxies - ) - self._init_resources() + return token def refresh_token(self, token_refresh=""): """ diff --git a/tests/resources/auth_app_no_token.json b/tests/resources/auth_app_no_token.json new file mode 100644 index 0000000..2c63c08 --- /dev/null +++ b/tests/resources/auth_app_no_token.json @@ -0,0 +1,2 @@ +{ +} diff --git a/tests/resources/auth_app_success.json b/tests/resources/auth_app_success.json index e26a90e..9ace9fd 100644 --- a/tests/resources/auth_app_success.json +++ b/tests/resources/auth_app_success.json @@ -1,3 +1,3 @@ { - "cyphered_token": "eyJhbGciOiJBMTI4S1ciLCJlbmMiOiJBMjU2R0NNIn0.9C2qwG_R0B22Qws6umB1gbqpLiH2rfVJbwgtJlxWBqPtYhhG-Ioc1g.RBBVW4k2k8t44aEo.VFsRiipfRMKXVGQxdGcnM6k.8uaF6FoQiPxX6wdFU2AyYA" + "token": "eyJhcHBfdG9rZW5faWQiOjN9:1utpZt:fXS-ifJ6TGWQEy7IkkxemDPGM5jXTOYYn7heGf8MFWU" } diff --git a/tests/test_auth_app.py b/tests/test_auth_app.py index 10a6ed8..8e92661 100644 --- a/tests/test_auth_app.py +++ b/tests/test_auth_app.py @@ -14,8 +14,20 @@ class TestAuthApp(unittest.TestCase): def test_auth_success(self, requests): requests.post.return_value = MockResponse(200, create_mock_json("tests/resources/auth_app_success.json")) api = TaigaAPI(host="host") - api.auth_app("valid-app-id", "valid-app-secret", "valid-auth-code", "valid-state") - self.assertEqual(api.token, "f4k3") + token = api.auth_app("valid-app-id", "valid-auth-code", "valid-state") + self.assertEqual(token, "eyJhcHBfdG9rZW5faWQiOjN9:1utpZt:fXS-ifJ6TGWQEy7IkkxemDPGM5jXTOYYn7heGf8MFWU") + + @patch("taiga.client.requests") + def test_auth_success_no_token(self, requests): + requests.post.return_value = MockResponse(200, create_mock_json("tests/resources/auth_app_no_token.json")) + api = TaigaAPI(host="host") + self.assertRaises( + taiga.exceptions.TaigaRestException, + api.auth_app, + "valid-app-id", + "valid-auth-code", + "valid-state", + ) @patch("taiga.client.requests") def test_auth_not_success(self, requests): @@ -25,7 +37,6 @@ def test_auth_not_success(self, requests): taiga.exceptions.TaigaRestException, api.auth_app, "valid-app-id", - "valid-app-secret", "valid-auth-code", "valid-state", ) @@ -38,7 +49,6 @@ def test_auth_connection_error(self, requests_post): taiga.exceptions.TaigaRestException, api.auth_app, "valid-app-id", - "valid-app-pass", "valid-auth-code", "valid-state", )