Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ Daniel Federschmidt <daniel@federschmidt.xyz>
Robert Dyer <rdyer@unl.edu>
Patrick Szczepański <p.szczepanski996@gmail.com>
Hassen Ben Tanfous <hassen.ben-tanfous@docaposte.fr>
Melissa Eckardt <melissa.eckardt@gdata-adan.de>
12 changes: 12 additions & 0 deletions changes/227.removal
Original file line number Diff line number Diff line change
@@ -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=<value>, token_type="Application"``;
* the unmaintained ``pyjwkest`` dependency (and its vulnerable transitive
dependency ``future``, `GHSA-xqrq-4mgf-ff32
<https://github.com/advisories/GHSA-xqrq-4mgf-ff32>`_) is no longer required.

See :ref:`Application authentication` in the usage docs for the updated flow.
35 changes: 35 additions & 0 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
******************************************************
Expand Down
1 change: 0 additions & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
41 changes: 14 additions & 27 deletions taiga/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -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=""):
"""
Expand Down
2 changes: 2 additions & 0 deletions tests/resources/auth_app_no_token.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{
}
2 changes: 1 addition & 1 deletion tests/resources/auth_app_success.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"cyphered_token": "eyJhbGciOiJBMTI4S1ciLCJlbmMiOiJBMjU2R0NNIn0.9C2qwG_R0B22Qws6umB1gbqpLiH2rfVJbwgtJlxWBqPtYhhG-Ioc1g.RBBVW4k2k8t44aEo.VFsRiipfRMKXVGQxdGcnM6k.8uaF6FoQiPxX6wdFU2AyYA"
"token": "eyJhcHBfdG9rZW5faWQiOjN9:1utpZt:fXS-ifJ6TGWQEy7IkkxemDPGM5jXTOYYn7heGf8MFWU"
}
18 changes: 14 additions & 4 deletions tests/test_auth_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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",
)
Expand All @@ -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",
)
Loading