From 35815169b68f7f4bab860f0e3c15db868bc4ba18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20S=C3=A1nchez?= Date: Mon, 19 Oct 2020 15:16:08 -0500 Subject: [PATCH 01/24] Logic for jwt token --- cuenca/http/client.py | 77 ++++++++++++++++++++++--------------------- requirements.txt | 1 - 2 files changed, 40 insertions(+), 38 deletions(-) diff --git a/cuenca/http/client.py b/cuenca/http/client.py index b3af7baa..88eb74e1 100644 --- a/cuenca/http/client.py +++ b/cuenca/http/client.py @@ -1,9 +1,11 @@ +import base64 +import datetime as dt +import json import os from typing import Optional, Tuple, Union from urllib.parse import urljoin import requests -from aws_requests_auth.aws_auth import AWSRequestsAuth from cuenca_validations.typing import ( ClientRequestParams, DictStrAny, @@ -24,7 +26,7 @@ class Session: host: str = API_HOST basic_auth: Tuple[str, str] - iam_auth: Optional[AWSRequestsAuth] = None + jwt_token: Optional[str] = None session: requests.Session def __init__(self): @@ -41,31 +43,20 @@ def __init__(self): api_secret = os.getenv('CUENCA_API_SECRET', '') self.basic_auth = (api_key, api_secret) - # IAM auth - aws_access_key = os.getenv('AWS_ACCESS_KEY_ID', '') - aws_secret_access_key = os.getenv('AWS_SECRET_ACCESS_KEY', '') - aws_region = os.getenv('AWS_DEFAULT_REGION', AWS_DEFAULT_REGION) - if aws_access_key and aws_secret_access_key: - self.iam_auth = AWSRequestsAuth( - aws_access_key=aws_access_key, - aws_secret_access_key=aws_secret_access_key, - aws_host=self.host, - aws_region=aws_region, - aws_service=AWS_SERVICE, - ) - @property - def auth(self) -> Union[AWSRequestsAuth, Tuple[str, str]]: + def auth(self) -> Union[Tuple[str, str], str]: # preference to basic auth - return self.basic_auth if all(self.basic_auth) else self.iam_auth + return ( + self.basic_auth + if all(self.basic_auth) and not self.jwt_token + else None + ) def configure( self, api_key: Optional[str] = None, api_secret: Optional[str] = None, - aws_access_key: Optional[str] = None, - aws_secret_access_key: Optional[str] = None, - aws_region: str = AWS_DEFAULT_REGION, + use_jwt: Optional[bool] = None, sandbox: Optional[bool] = None, ): """ @@ -84,23 +75,8 @@ def configure( api_secret or self.basic_auth[1], ) - # IAM auth - if self.iam_auth is not None: - self.iam_auth.aws_access_key = ( - aws_access_key or self.iam_auth.aws_access_key - ) - self.iam_auth.aws_secret_access_key = ( - aws_secret_access_key or self.iam_auth.aws_secret_access_key - ) - self.iam_auth.aws_region = aws_region or self.iam_auth.aws_region - elif aws_access_key and aws_secret_access_key: - self.iam_auth = AWSRequestsAuth( - aws_access_key=aws_access_key, - aws_secret_access_key=aws_secret_access_key, - aws_host=self.host, - aws_region=aws_region, - aws_service=AWS_SERVICE, - ) + if use_jwt: + self.set_jwt_headers() def get( self, @@ -126,6 +102,8 @@ def request( data: OptionalDict = None, **kwargs, ) -> DictStrAny: + if self.jwt_token: + self.set_jwt_headers() resp = self.session.request( method=method, url='https://' + self.host + urljoin('/', endpoint), @@ -137,6 +115,31 @@ def request( self._check_response(resp) return resp.json() + def set_jwt_headers(self): + # Make sure the current token is still valid + try: + payload_encoded = self.jwt_token.split('.')[1] + payload = json.loads(base64.b64decode(payload_encoded)) + # Get a new token if there's less than 5 mins for the actual + # to be expired + if payload['exp'] - dt.datetime.utcnow() <= dt.timedelta( + minutes=5 + ): + raise Exception('Expired token') + except Exception: + self.jwt_token = None + + # Get a new one otherwise + if not self.jwt_token: + self.jwt_token = self.post('/token')['token'] + + # Set headers with valid token + self.session.headers.update( + { + 'X-Cuenca-Token': self.jwt_token, + } + ) + @staticmethod def _check_response(response: Response): if response.ok: diff --git a/requirements.txt b/requirements.txt index 7be43d2b..b761df7e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,3 @@ requests==2.25.0 cuenca-validations==0.6.9 dataclasses>=0.7;python_version<"3.7" -aws-requests-auth==0.4.3 From a001a9c9beba1ba66bdd8465098db26b6df33b6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20S=C3=A1nchez?= Date: Tue, 20 Oct 2020 17:25:42 -0500 Subject: [PATCH 02/24] Adding jwt support --- cuenca/http/client.py | 6 +-- tests/http/cassettes/test_configures_jwt.yaml | 53 +++++++++++++++++++ tests/http/conftest.py | 7 --- tests/http/test_client.py | 23 ++------ 4 files changed, 61 insertions(+), 28 deletions(-) create mode 100644 tests/http/cassettes/test_configures_jwt.yaml diff --git a/cuenca/http/client.py b/cuenca/http/client.py index 88eb74e1..30cd8a80 100644 --- a/cuenca/http/client.py +++ b/cuenca/http/client.py @@ -2,7 +2,7 @@ import datetime as dt import json import os -from typing import Optional, Tuple, Union +from typing import Optional, Tuple from urllib.parse import urljoin import requests @@ -44,7 +44,7 @@ def __init__(self): self.basic_auth = (api_key, api_secret) @property - def auth(self) -> Union[Tuple[str, str], str]: + def auth(self) -> Optional[Tuple[str, str]]: # preference to basic auth return ( self.basic_auth @@ -131,7 +131,7 @@ def set_jwt_headers(self): # Get a new one otherwise if not self.jwt_token: - self.jwt_token = self.post('/token')['token'] + self.jwt_token = self.post('/token', data=None)['token'] # Set headers with valid token self.session.headers.update( diff --git a/tests/http/cassettes/test_configures_jwt.yaml b/tests/http/cassettes/test_configures_jwt.yaml new file mode 100644 index 00000000..4ef7be00 --- /dev/null +++ b/tests/http/cassettes/test_configures_jwt.yaml @@ -0,0 +1,53 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - DUMMY + Connection: + - keep-alive + Content-Length: + - '0' + User-Agent: + - cuenca-python/0.3.6 + X-Cuenca-Api-Version: + - '2020-03-19' + method: POST + uri: https://api.cuenca.com/token + response: + body: + string: '{"message":"Authorization header requires ''Credential'' parameter. + Authorization header requires ''Signature'' parameter. Authorization header + requires ''SignedHeaders'' parameter. Authorization header requires existence + of either a ''X-Amz-Date'' or a ''Date'' header. Authorization=Basic YXBpX2tleTpzZWNyZXQ="}' + headers: + Connection: + - keep-alive + Content-Length: + - '303' + Content-Type: + - application/json + Date: + - Mon, 19 Oct 2020 20:26:58 GMT + Via: + - 1.1 7566f16c791b2604304a10c0249c0e6d.cloudfront.net (CloudFront) + X-Amz-Cf-Id: + - MKCQyoVeti260tSMcv77vuB1ty_QA3In2A_KiXudwT7YYAsmgIlM9g== + X-Amz-Cf-Pop: + - IAH50-C4 + X-Cache: + - Error from cloudfront + x-amz-apigw-id: + - UrN21Gk4oAMFU5A= + x-amzn-ErrorType: + - IncompleteSignatureException + x-amzn-RequestId: + - a5ec85cc-0362-424d-b296-87ad937f42f0 + status: + code: 403 + message: Forbidden +version: 1 diff --git a/tests/http/conftest.py b/tests/http/conftest.py index 308deecb..74fdbd7b 100644 --- a/tests/http/conftest.py +++ b/tests/http/conftest.py @@ -5,10 +5,3 @@ def cuenca_creds(monkeypatch) -> None: monkeypatch.setenv('CUENCA_API_KEY', 'api_key') monkeypatch.setenv('CUENCA_API_SECRET', 'secret') - - -@pytest.fixture -def aws_creds(monkeypatch) -> None: - monkeypatch.setenv('AWS_ACCESS_KEY_ID', 'aws_key') - monkeypatch.setenv('AWS_SECRET_ACCESS_KEY', 'aws_secret') - monkeypatch.setenv('AWS_DEFAULT_REGION', 'us-east-1') diff --git a/tests/http/test_client.py b/tests/http/test_client.py index 867e41a9..162b1124 100644 --- a/tests/http/test_client.py +++ b/tests/http/test_client.py @@ -22,27 +22,14 @@ def test_basic_auth_configuration(): session = Session() assert session.auth == session.basic_auth assert session.auth == ('api_key', 'secret') - assert not session.iam_auth + assert not session.jwt_token -@pytest.mark.usefixtures('cuenca_creds', 'aws_creds') -def test_gives_preference_to_basic_auth_configuration(): - session = Session() - assert session.auth == session.basic_auth - assert session.iam_auth - - -@pytest.mark.usefixtures('aws_creds') -def test_aws_iam_auth_configuration(): - session = Session() - assert session.auth == session.iam_auth - - -def test_configures_new_aws_creds(): +@pytest.mark.vcr +@pytest.mark.usefixtures('cuenca_creds') +def test_configures_jwt(): session = Session() - session.configure( - aws_access_key='new_aws_key', aws_secret_access_key='new_aws_secret' - ) + session.configure(use_jwt=True) assert session.auth.aws_secret_access_key == 'new_aws_secret' assert session.auth.aws_access_key == 'new_aws_key' assert session.auth.aws_region == 'us-east-1' From cd57e2314b2a2e791da1189f6a18495a6e37ab5e Mon Sep 17 00:00:00 2001 From: cloud-init created default user Date: Wed, 21 Oct 2020 00:09:03 +0000 Subject: [PATCH 03/24] Cassette --- tests/http/cassettes/test_configures_jwt.yaml | 33 +++++++------------ 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/tests/http/cassettes/test_configures_jwt.yaml b/tests/http/cassettes/test_configures_jwt.yaml index 4ef7be00..6e21a732 100644 --- a/tests/http/cassettes/test_configures_jwt.yaml +++ b/tests/http/cassettes/test_configures_jwt.yaml @@ -17,37 +17,28 @@ interactions: X-Cuenca-Api-Version: - '2020-03-19' method: POST - uri: https://api.cuenca.com/token + uri: https://stage.cuenca.com/token response: body: - string: '{"message":"Authorization header requires ''Credential'' parameter. - Authorization header requires ''Signature'' parameter. Authorization header - requires ''SignedHeaders'' parameter. Authorization header requires existence - of either a ''X-Amz-Date'' or a ''Date'' header. Authorization=Basic YXBpX2tleTpzZWNyZXQ="}' + string: '{"id":1,"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MDM4NDM2NTUsImlhdCI6MTYwMzIzODg1NSwic3ViIjoiQUtib3hCeXNTd1NTbVR0VmtoT1I2X21RIn0.VAT9w746K0hfOKnvZCFxwBDKjV71j-ItaFdZrIc0xCM","created_at":"2020-10-21T00:07:35.290968+00:00","api_key_id":"AKboxBysSwSSmTtVkhOR6_mQ"}' headers: Connection: - keep-alive Content-Length: - - '303' + - '279' Content-Type: - application/json Date: - - Mon, 19 Oct 2020 20:26:58 GMT - Via: - - 1.1 7566f16c791b2604304a10c0249c0e6d.cloudfront.net (CloudFront) - X-Amz-Cf-Id: - - MKCQyoVeti260tSMcv77vuB1ty_QA3In2A_KiXudwT7YYAsmgIlM9g== - X-Amz-Cf-Pop: - - IAH50-C4 - X-Cache: - - Error from cloudfront + - Wed, 21 Oct 2020 00:07:35 GMT + Server: + - nginx/1.18.0 + X-Amzn-Trace-Id: + - Root=1-5f8f7bc6-16318c2505b404c030bbcaf4;Sampled=0 x-amz-apigw-id: - - UrN21Gk4oAMFU5A= - x-amzn-ErrorType: - - IncompleteSignatureException + - UvBHAHOGCYcFZTw= x-amzn-RequestId: - - a5ec85cc-0362-424d-b296-87ad937f42f0 + - 856c8bef-f148-48e4-a27b-391749b6b9f6 status: - code: 403 - message: Forbidden + code: 201 + message: Created version: 1 From 48cf3842a8c5c6e5756776d3c73da98c0f6b8943 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20S=C3=A1nchez?= Date: Wed, 21 Oct 2020 12:02:22 -0500 Subject: [PATCH 04/24] Testing --- requirements-test.txt | 1 + setup.cfg | 3 ++ tests/http/cassettes/test_configures_jwt.yaml | 2 +- tests/http/test_client.py | 35 ++++++++++++------- 4 files changed, 27 insertions(+), 14 deletions(-) diff --git a/requirements-test.txt b/requirements-test.txt index adcb757b..9d9625fa 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -5,3 +5,4 @@ black==20.8b1 isort==5.7.* flake8==3.8.* mypy==0.790 +freezegun==1.0.* diff --git a/setup.cfg b/setup.cfg index 84f0fbe8..f659ddbd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -19,3 +19,6 @@ ignore_missing_imports = True [mypy-aws_requests_auth.*] ignore_missing_imports = True + +[mypy-freezegun.*] +ignore_missing_imports = True diff --git a/tests/http/cassettes/test_configures_jwt.yaml b/tests/http/cassettes/test_configures_jwt.yaml index 6e21a732..aa638e36 100644 --- a/tests/http/cassettes/test_configures_jwt.yaml +++ b/tests/http/cassettes/test_configures_jwt.yaml @@ -17,7 +17,7 @@ interactions: X-Cuenca-Api-Version: - '2020-03-19' method: POST - uri: https://stage.cuenca.com/token + uri: https://api.cuenca.com/token response: body: string: '{"id":1,"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MDM4NDM2NTUsImlhdCI6MTYwMzIzODg1NSwic3ViIjoiQUtib3hCeXNTd1NTbVR0VmtoT1I2X21RIn0.VAT9w746K0hfOKnvZCFxwBDKjV71j-ItaFdZrIc0xCM","created_at":"2020-10-21T00:07:35.290968+00:00","api_key_id":"AKboxBysSwSSmTtVkhOR6_mQ"}' diff --git a/tests/http/test_client.py b/tests/http/test_client.py index 162b1124..9f36e9a4 100644 --- a/tests/http/test_client.py +++ b/tests/http/test_client.py @@ -1,6 +1,8 @@ from unittest.mock import MagicMock, patch +import datetime as dt import pytest +from freezegun import freeze_time from cuenca.exc import CuencaResponseException from cuenca.http.client import Session @@ -30,22 +32,29 @@ def test_basic_auth_configuration(): def test_configures_jwt(): session = Session() session.configure(use_jwt=True) - assert session.auth.aws_secret_access_key == 'new_aws_secret' - assert session.auth.aws_access_key == 'new_aws_key' - assert session.auth.aws_region == 'us-east-1' + assert not session.auth + assert session.jwt_token -@pytest.mark.usefixtures('aws_creds') -def test_overrides_aws_creds(): +@pytest.mark.vcr +@pytest.mark.usefixtures('cuenca_creds') +def test_request_valid_token(): session = Session() - session.configure( - aws_access_key='new_aws_key', - aws_secret_access_key='new_aws_secret', - aws_region='us-east-2', - ) - assert session.auth.aws_secret_access_key == 'new_aws_secret' - assert session.auth.aws_access_key == 'new_aws_key' - assert session.auth.aws_region == 'us-east-2' + session.configure(use_jwt=True) + response = session.get('/api_keys') + assert response.status_code == 200 + + +@pytest.mark.vcr +@pytest.mark.usefixtures('cuenca_creds') +def test_request_expired_token(): + session = Session() + session.configure(use_jwt=True) + previous_jwt = session.jwt_token + with freeze_time(dt.datetime.utcnow() + dt.timedelta(days=40)): + response = session.get('/api_keys') + assert response.status_code == 200 + assert session.jwt_token != previous_jwt @patch('cuenca.http.client.requests.Session.request') From bc00876fd780c4c46751dc4ad8934b2516ba4754 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20S=C3=A1nchez?= Date: Wed, 21 Oct 2020 12:59:55 -0500 Subject: [PATCH 05/24] Fix date --- cuenca/http/client.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cuenca/http/client.py b/cuenca/http/client.py index 30cd8a80..2c87e650 100644 --- a/cuenca/http/client.py +++ b/cuenca/http/client.py @@ -119,12 +119,12 @@ def set_jwt_headers(self): # Make sure the current token is still valid try: payload_encoded = self.jwt_token.split('.')[1] - payload = json.loads(base64.b64decode(payload_encoded)) + payload = json.loads(base64.b64decode(f'{payload_encoded}==')) # Get a new token if there's less than 5 mins for the actual # to be expired - if payload['exp'] - dt.datetime.utcnow() <= dt.timedelta( - minutes=5 - ): + if dt.datetime.utcfromtimestamp( + payload['exp'] + ) - dt.datetime.utcnow() <= dt.timedelta(minutes=5): raise Exception('Expired token') except Exception: self.jwt_token = None From 3b5bfc9eed56aeefdafaae7bdc9076b6bcfe7b4e Mon Sep 17 00:00:00 2001 From: cloud-init created default user Date: Wed, 21 Oct 2020 18:50:23 +0000 Subject: [PATCH 06/24] Cassettes --- tests/conftest.py | 2 +- .../cassettes/test_request_expired_token.yaml | 128 ++++++++++++++++++ .../cassettes/test_request_valid_token.yaml | 84 ++++++++++++ 3 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 tests/http/cassettes/test_request_expired_token.yaml create mode 100644 tests/http/cassettes/test_request_valid_token.yaml diff --git a/tests/conftest.py b/tests/conftest.py index c189b790..576961ac 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,5 +8,5 @@ @pytest.fixture(scope='module') def vcr_config(): config = dict() - config['filter_headers'] = [('Authorization', 'DUMMY')] + config['filter_headers'] = [('Authorization', 'DUMMY'), ('X-Cuenca-Token', 'DUMMY')] return config diff --git a/tests/http/cassettes/test_request_expired_token.yaml b/tests/http/cassettes/test_request_expired_token.yaml new file mode 100644 index 00000000..c828893c --- /dev/null +++ b/tests/http/cassettes/test_request_expired_token.yaml @@ -0,0 +1,128 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - DUMMY + Connection: + - keep-alive + Content-Length: + - '0' + User-Agent: + - cuenca-python/0.3.6 + X-Cuenca-Api-Version: + - '2020-03-19' + method: POST + uri: https://stage.cuenca.com/token + response: + body: + string: '{"id":12,"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MDM5MTA5NDksImlhdCI6MTYwMzMwNjE0OSwic3ViIjoiQUtib3hCeXNTd1NTbVR0VmtoT1I2X21RIiwidWlkIjoiMWFkNWUwZTMtMTNjZS0xMWViLTg0MjItNDE3MmZhYTQwZjQyIn0.5OX-MCcgi4cyiGexSsDIwk8HyhJxuIT8VF7X41GkO6w","created_at":"2020-10-21T18:49:09.935845","api_key":{"id":"AKboxBysSwSSmTtVkhOR6_mQ","created_at":"2020-10-20T23:56:28.845757","secret":"********","deactivated_at":null,"user_id":"rsc"}}' + headers: + Connection: + - keep-alive + Content-Length: + - '438' + Content-Type: + - application/json + Date: + - Wed, 21 Oct 2020 18:49:09 GMT + Server: + - nginx/1.18.0 + X-Amzn-Trace-Id: + - Root=1-5f9082a5-37ce7b1275ae2055215eb14d;Sampled=0 + x-amz-apigw-id: + - UxlZ8FEyCYcFUuQ= + x-amzn-RequestId: + - ee534c24-2878-4efe-b91e-3c99757adff6 + status: + code: 201 + message: Created +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - DUMMY + Connection: + - keep-alive + Content-Length: + - '0' + User-Agent: + - cuenca-python/0.3.6 + X-Cuenca-Api-Version: + - '2020-03-19' + X-Cuenca-Token: + - DUMMY + method: POST + uri: https://stage.cuenca.com/token + response: + body: + string: '{"id":13,"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MDM5MTA5NTAsImlhdCI6MTYwMzMwNjE1MCwic3ViIjoiQUtib3hCeXNTd1NTbVR0VmtoT1I2X21RIiwidWlkIjoiMWFlMjQ5MjUtMTNjZS0xMWViLTk0ZDQtNDE3MmZhYTQwZjQyIn0.GmDxVfvt2Es6mf1upY5Y7RdkGgeos7mTpA_7wFb8ZaI","created_at":"2020-10-21T18:49:10.017165","api_key":{"id":"AKboxBysSwSSmTtVkhOR6_mQ","created_at":"2020-10-20T23:56:28.845757","secret":"********","deactivated_at":null,"user_id":"rsc"}}' + headers: + Connection: + - keep-alive + Content-Length: + - '438' + Content-Type: + - application/json + Date: + - Wed, 21 Oct 2020 18:49:10 GMT + Server: + - nginx/1.18.0 + X-Amzn-Trace-Id: + - Root=1-5f9082a5-25f2f46b7c8b558769ecf708;Sampled=0 + x-amz-apigw-id: + - UxlZ8Gs5CYcFtFg= + x-amzn-RequestId: + - 6553d156-d57e-48a4-b16a-42588c221827 + status: + code: 201 + message: Created +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - cuenca-python/0.3.6 + X-Cuenca-Api-Version: + - '2020-03-19' + X-Cuenca-Token: + - DUMMY + method: GET + uri: https://stage.cuenca.com/api_keys + response: + body: + string: '{"items":[{"id":"AKboxBysSwSSmTtVkhOR6_mQ","created_at":"2020-10-20T23:56:28.845757","secret":"********","deactivated_at":null,"user_id":"rsc"}],"next_page_uri":null}' + headers: + Connection: + - keep-alive + Content-Length: + - '166' + Content-Type: + - application/json + Date: + - Wed, 21 Oct 2020 18:49:10 GMT + Server: + - nginx/1.18.0 + X-Amzn-Trace-Id: + - Root=1-5f9082a6-6a70a6d03f0698b549541398;Sampled=0 + x-amz-apigw-id: + - UxlZ9G4tiYcFmjQ= + x-amzn-RequestId: + - e3f54e5b-13ec-4cf8-83da-4f93f7bd9c54 + status: + code: 200 + message: OK +version: 1 diff --git a/tests/http/cassettes/test_request_valid_token.yaml b/tests/http/cassettes/test_request_valid_token.yaml new file mode 100644 index 00000000..b182b5c2 --- /dev/null +++ b/tests/http/cassettes/test_request_valid_token.yaml @@ -0,0 +1,84 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - DUMMY + Connection: + - keep-alive + Content-Length: + - '0' + User-Agent: + - cuenca-python/0.3.6 + X-Cuenca-Api-Version: + - '2020-03-19' + method: POST + uri: https://stage.cuenca.com/token + response: + body: + string: '{"id":11,"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MDM5MTA4MDksImlhdCI6MTYwMzMwNjAwOSwic3ViIjoiQUtib3hCeXNTd1NTbVR0VmtoT1I2X21RIiwidWlkIjoiYzcxZDhiZWYtMTNjZC0xMWViLTlmYzItNDE3MmZhYTQwZjQyIn0.fXy3EYJTaKQv61wQuKRUwR9BHvnovsUO74X3XpEhvBk","created_at":"2020-10-21T18:46:49.514738","api_key":{"id":"AKboxBysSwSSmTtVkhOR6_mQ","created_at":"2020-10-20T23:56:28.845757","secret":"********","deactivated_at":null,"user_id":"rsc"}}' + headers: + Connection: + - keep-alive + Content-Length: + - '438' + Content-Type: + - application/json + Date: + - Wed, 21 Oct 2020 18:46:49 GMT + Server: + - nginx/1.18.0 + X-Amzn-Trace-Id: + - Root=1-5f908218-39f33c5c4167d480550635de;Sampled=0 + x-amz-apigw-id: + - UxlD0EUliYcFsKQ= + x-amzn-RequestId: + - c3dc4715-5946-421f-889c-763837d156fc + status: + code: 201 + message: Created +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - cuenca-python/0.3.6 + X-Cuenca-Api-Version: + - '2020-03-19' + X-Cuenca-Token: + - DUMMY + method: GET + uri: https://stage.cuenca.com/api_keys + response: + body: + string: '{"items":[{"id":"AKboxBysSwSSmTtVkhOR6_mQ","created_at":"2020-10-20T23:56:28.845757","secret":"********","deactivated_at":null,"user_id":"rsc"}],"next_page_uri":null}' + headers: + Connection: + - keep-alive + Content-Length: + - '166' + Content-Type: + - application/json + Date: + - Wed, 21 Oct 2020 18:46:49 GMT + Server: + - nginx/1.18.0 + X-Amzn-Trace-Id: + - Root=1-5f908219-170a4d1628d8c2e3614879c7;Sampled=0 + x-amz-apigw-id: + - UxlEAEmgiYcFbUQ= + x-amzn-RequestId: + - 58dd24dd-11aa-4ab0-a174-63de03131988 + status: + code: 200 + message: OK +version: 1 From 0ece32af3e436b890ab7e61605f2de244c9f7bc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20S=C3=A1nchez?= Date: Wed, 21 Oct 2020 13:57:09 -0500 Subject: [PATCH 07/24] Final fixes --- tests/conftest.py | 5 ++++- tests/http/cassettes/test_request_expired_token.yaml | 6 +++--- tests/http/cassettes/test_request_valid_token.yaml | 4 ++-- tests/http/test_client.py | 4 ++-- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 576961ac..a28b192e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,5 +8,8 @@ @pytest.fixture(scope='module') def vcr_config(): config = dict() - config['filter_headers'] = [('Authorization', 'DUMMY'), ('X-Cuenca-Token', 'DUMMY')] + config['filter_headers'] = [ + ('Authorization', 'DUMMY'), + ('X-Cuenca-Token', 'DUMMY'), + ] return config diff --git a/tests/http/cassettes/test_request_expired_token.yaml b/tests/http/cassettes/test_request_expired_token.yaml index c828893c..1b4e041d 100644 --- a/tests/http/cassettes/test_request_expired_token.yaml +++ b/tests/http/cassettes/test_request_expired_token.yaml @@ -17,7 +17,7 @@ interactions: X-Cuenca-Api-Version: - '2020-03-19' method: POST - uri: https://stage.cuenca.com/token + uri: https://api.cuenca.com/token response: body: string: '{"id":12,"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MDM5MTA5NDksImlhdCI6MTYwMzMwNjE0OSwic3ViIjoiQUtib3hCeXNTd1NTbVR0VmtoT1I2X21RIiwidWlkIjoiMWFkNWUwZTMtMTNjZS0xMWViLTg0MjItNDE3MmZhYTQwZjQyIn0.5OX-MCcgi4cyiGexSsDIwk8HyhJxuIT8VF7X41GkO6w","created_at":"2020-10-21T18:49:09.935845","api_key":{"id":"AKboxBysSwSSmTtVkhOR6_mQ","created_at":"2020-10-20T23:56:28.845757","secret":"********","deactivated_at":null,"user_id":"rsc"}}' @@ -61,7 +61,7 @@ interactions: X-Cuenca-Token: - DUMMY method: POST - uri: https://stage.cuenca.com/token + uri: https://api.cuenca.com/token response: body: string: '{"id":13,"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MDM5MTA5NTAsImlhdCI6MTYwMzMwNjE1MCwic3ViIjoiQUtib3hCeXNTd1NTbVR0VmtoT1I2X21RIiwidWlkIjoiMWFlMjQ5MjUtMTNjZS0xMWViLTk0ZDQtNDE3MmZhYTQwZjQyIn0.GmDxVfvt2Es6mf1upY5Y7RdkGgeos7mTpA_7wFb8ZaI","created_at":"2020-10-21T18:49:10.017165","api_key":{"id":"AKboxBysSwSSmTtVkhOR6_mQ","created_at":"2020-10-20T23:56:28.845757","secret":"********","deactivated_at":null,"user_id":"rsc"}}' @@ -101,7 +101,7 @@ interactions: X-Cuenca-Token: - DUMMY method: GET - uri: https://stage.cuenca.com/api_keys + uri: https://api.cuenca.com/api_keys response: body: string: '{"items":[{"id":"AKboxBysSwSSmTtVkhOR6_mQ","created_at":"2020-10-20T23:56:28.845757","secret":"********","deactivated_at":null,"user_id":"rsc"}],"next_page_uri":null}' diff --git a/tests/http/cassettes/test_request_valid_token.yaml b/tests/http/cassettes/test_request_valid_token.yaml index b182b5c2..325b5f4e 100644 --- a/tests/http/cassettes/test_request_valid_token.yaml +++ b/tests/http/cassettes/test_request_valid_token.yaml @@ -17,7 +17,7 @@ interactions: X-Cuenca-Api-Version: - '2020-03-19' method: POST - uri: https://stage.cuenca.com/token + uri: https://api.cuenca.com/token response: body: string: '{"id":11,"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MDM5MTA4MDksImlhdCI6MTYwMzMwNjAwOSwic3ViIjoiQUtib3hCeXNTd1NTbVR0VmtoT1I2X21RIiwidWlkIjoiYzcxZDhiZWYtMTNjZC0xMWViLTlmYzItNDE3MmZhYTQwZjQyIn0.fXy3EYJTaKQv61wQuKRUwR9BHvnovsUO74X3XpEhvBk","created_at":"2020-10-21T18:46:49.514738","api_key":{"id":"AKboxBysSwSSmTtVkhOR6_mQ","created_at":"2020-10-20T23:56:28.845757","secret":"********","deactivated_at":null,"user_id":"rsc"}}' @@ -57,7 +57,7 @@ interactions: X-Cuenca-Token: - DUMMY method: GET - uri: https://stage.cuenca.com/api_keys + uri: https://api.cuenca.com/api_keys response: body: string: '{"items":[{"id":"AKboxBysSwSSmTtVkhOR6_mQ","created_at":"2020-10-20T23:56:28.845757","secret":"********","deactivated_at":null,"user_id":"rsc"}],"next_page_uri":null}' diff --git a/tests/http/test_client.py b/tests/http/test_client.py index 9f36e9a4..b6b47b55 100644 --- a/tests/http/test_client.py +++ b/tests/http/test_client.py @@ -42,7 +42,7 @@ def test_request_valid_token(): session = Session() session.configure(use_jwt=True) response = session.get('/api_keys') - assert response.status_code == 200 + assert response['items'] @pytest.mark.vcr @@ -53,7 +53,7 @@ def test_request_expired_token(): previous_jwt = session.jwt_token with freeze_time(dt.datetime.utcnow() + dt.timedelta(days=40)): response = session.get('/api_keys') - assert response.status_code == 200 + assert response['items'] assert session.jwt_token != previous_jwt From 515818f0cb9da1c5d442580130ce3dd0e9d6a7e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20S=C3=A1nchez?= Date: Wed, 21 Oct 2020 14:01:19 -0500 Subject: [PATCH 08/24] This is not neede --- setup.cfg | 3 --- 1 file changed, 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index f659ddbd..e5b11289 100644 --- a/setup.cfg +++ b/setup.cfg @@ -17,8 +17,5 @@ combine_as_imports=True [mypy-pytest] ignore_missing_imports = True -[mypy-aws_requests_auth.*] -ignore_missing_imports = True - [mypy-freezegun.*] ignore_missing_imports = True From 5c5d42c91338caa41364324066086530f3163a5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20S=C3=A1nchez?= Date: Thu, 26 Nov 2020 15:02:07 -0600 Subject: [PATCH 09/24] Fix test --- tests/http/test_client.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/http/test_client.py b/tests/http/test_client.py index b6b47b55..a1584c25 100644 --- a/tests/http/test_client.py +++ b/tests/http/test_client.py @@ -40,8 +40,11 @@ def test_configures_jwt(): @pytest.mark.usefixtures('cuenca_creds') def test_request_valid_token(): session = Session() - session.configure(use_jwt=True) - response = session.get('/api_keys') + # Set date when the cassette was created otherwise will retrieve + # an expired token + with freeze_time(dt.date(2020, 10, 22)): + session.configure(use_jwt=True) + response = session.get('/api_keys') assert response['items'] From 76e4ccab1b494fe5ea1d1900faf8e83df7285d8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20S=C3=A1nchez?= Date: Tue, 15 Dec 2020 10:47:04 -0600 Subject: [PATCH 10/24] Upgrade version --- cuenca/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cuenca/version.py b/cuenca/version.py index 6603bec5..bc40b51d 100644 --- a/cuenca/version.py +++ b/cuenca/version.py @@ -1,3 +1,3 @@ -__version__ = '0.4.2' +__version__ = '0.5.1' CLIENT_VERSION = __version__ API_VERSION = '2020-03-19' From 3ecb36f632f674b736d74dc6615486b6c5876e4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20S=C3=A1nchez?= Date: Tue, 15 Dec 2020 11:13:58 -0600 Subject: [PATCH 11/24] Pre release --- cuenca/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cuenca/version.py b/cuenca/version.py index bc40b51d..8ee615c8 100644 --- a/cuenca/version.py +++ b/cuenca/version.py @@ -1,3 +1,3 @@ -__version__ = '0.5.1' +__version__ = '0.5.1.dev0' CLIENT_VERSION = __version__ API_VERSION = '2020-03-19' From 79529dab28274f33daafaf8c6eb83b75371ba994 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20S=C3=A1nchez?= Date: Tue, 15 Dec 2020 11:17:22 -0600 Subject: [PATCH 12/24] Otro --- cuenca/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cuenca/version.py b/cuenca/version.py index 8ee615c8..8df76090 100644 --- a/cuenca/version.py +++ b/cuenca/version.py @@ -1,3 +1,3 @@ -__version__ = '0.5.1.dev0' +__version__ = '0.5.1.dev1' CLIENT_VERSION = __version__ API_VERSION = '2020-03-19' From 5e3c112ccce69d79461814c9e1310038389bb327 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20S=C3=A1nchez?= Date: Tue, 15 Dec 2020 11:18:08 -0600 Subject: [PATCH 13/24] Correct version --- cuenca/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cuenca/version.py b/cuenca/version.py index 8df76090..799fd7f5 100644 --- a/cuenca/version.py +++ b/cuenca/version.py @@ -1,3 +1,3 @@ -__version__ = '0.5.1.dev1' +__version__ = '0.5.0.dev0' CLIENT_VERSION = __version__ API_VERSION = '2020-03-19' From 1f6aaf5f0aac49061184080802692971b87645a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20S=C3=A1nchez?= Date: Tue, 15 Dec 2020 14:31:27 -0600 Subject: [PATCH 14/24] Version --- cuenca/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cuenca/version.py b/cuenca/version.py index 799fd7f5..8b60b9c3 100644 --- a/cuenca/version.py +++ b/cuenca/version.py @@ -1,3 +1,3 @@ -__version__ = '0.5.0.dev0' +__version__ = '0.5.0' CLIENT_VERSION = __version__ API_VERSION = '2020-03-19' From cc13a5f163928f3c4fc6c763f510816a70c9ed6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20S=C3=A1nchez?= Date: Wed, 23 Dec 2020 13:44:32 -0600 Subject: [PATCH 15/24] Default false --- cuenca/http/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cuenca/http/client.py b/cuenca/http/client.py index 2c87e650..25b37e65 100644 --- a/cuenca/http/client.py +++ b/cuenca/http/client.py @@ -56,7 +56,7 @@ def configure( self, api_key: Optional[str] = None, api_secret: Optional[str] = None, - use_jwt: Optional[bool] = None, + use_jwt: Optional[bool] = False, sandbox: Optional[bool] = None, ): """ From fa6c551852ec2bef99919d228070743f40f1a0f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20S=C3=A1nchez?= Date: Wed, 23 Dec 2020 18:22:11 -0600 Subject: [PATCH 16/24] Create jwt class --- cuenca/exc.py | 4 ++++ cuenca/http/client.py | 38 +++++++--------------------------- cuenca/jwt.py | 43 +++++++++++++++++++++++++++++++++++++++ tests/http/test_client.py | 2 +- tests/test_jwt.py | 43 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 98 insertions(+), 32 deletions(-) create mode 100644 cuenca/jwt.py create mode 100644 tests/test_jwt.py diff --git a/cuenca/exc.py b/cuenca/exc.py index abc49568..f0e9ad30 100644 --- a/cuenca/exc.py +++ b/cuenca/exc.py @@ -7,6 +7,10 @@ class CuencaException(Exception): ... +class MalformedJwtToken(CuencaException): + """An invalid JWT token was obtained during authentication""" + + class NoResultFound(CuencaException): """No results were found""" diff --git a/cuenca/http/client.py b/cuenca/http/client.py index 25b37e65..0f403f2f 100644 --- a/cuenca/http/client.py +++ b/cuenca/http/client.py @@ -1,6 +1,3 @@ -import base64 -import datetime as dt -import json import os from typing import Optional, Tuple from urllib.parse import urljoin @@ -14,6 +11,7 @@ from requests import Response from ..exc import CuencaResponseException +from ..jwt import Jwt from ..version import API_VERSION, CLIENT_VERSION API_HOST = 'api.cuenca.com' @@ -26,7 +24,7 @@ class Session: host: str = API_HOST basic_auth: Tuple[str, str] - jwt_token: Optional[str] = None + jwt_token: Optional[Jwt] = None session: requests.Session def __init__(self): @@ -76,7 +74,7 @@ def configure( ) if use_jwt: - self.set_jwt_headers() + self.jwt_token = Jwt.create(self) def get( self, @@ -103,7 +101,10 @@ def request( **kwargs, ) -> DictStrAny: if self.jwt_token: - self.set_jwt_headers() + if self.jwt_token.is_expired: + self.jwt_token = Jwt.create(self) + self.session.headers['X-Cuenca-Token'] = self.jwt_token.token + resp = self.session.request( method=method, url='https://' + self.host + urljoin('/', endpoint), @@ -115,31 +116,6 @@ def request( self._check_response(resp) return resp.json() - def set_jwt_headers(self): - # Make sure the current token is still valid - try: - payload_encoded = self.jwt_token.split('.')[1] - payload = json.loads(base64.b64decode(f'{payload_encoded}==')) - # Get a new token if there's less than 5 mins for the actual - # to be expired - if dt.datetime.utcfromtimestamp( - payload['exp'] - ) - dt.datetime.utcnow() <= dt.timedelta(minutes=5): - raise Exception('Expired token') - except Exception: - self.jwt_token = None - - # Get a new one otherwise - if not self.jwt_token: - self.jwt_token = self.post('/token', data=None)['token'] - - # Set headers with valid token - self.session.headers.update( - { - 'X-Cuenca-Token': self.jwt_token, - } - ) - @staticmethod def _check_response(response: Response): if response.ok: diff --git a/cuenca/jwt.py b/cuenca/jwt.py new file mode 100644 index 00000000..a8a4f3c8 --- /dev/null +++ b/cuenca/jwt.py @@ -0,0 +1,43 @@ +import base64 +import binascii +import datetime as dt +import json +from dataclasses import dataclass + +from .exc import MalformedJwtToken + + +@dataclass +class Jwt: + expires_at: dt.datetime + token: str + + @property + def is_expired(self) -> bool: + return self.expires_at - dt.datetime.utcnow() <= dt.timedelta( + minutes=5 + ) + + @staticmethod + def get_expiration_date(token: str) -> dt.datetime: + """ + Jwt tokens contains the exp field in the payload data, + this function extracts the date so we can't validate the + token before any request + More info about JWT tokens at: https://jwt.io/ + """ + try: + payload_encoded = token.split('.')[1] + payload = json.loads(base64.b64decode(f'{payload_encoded}==')) + except (IndexError, json.JSONDecodeError, binascii.Error): + raise MalformedJwtToken(f'Invalid JWT: {token}') + # Expiration timestamp can be found in the `exp` key in the payload + exp_timestamp = payload['exp'] + return dt.datetime.utcfromtimestamp(exp_timestamp) + + @classmethod + def create(cls, session) -> 'Jwt': + session.jwt_token = None + token = session.post('/token', data=None)['token'] + expires_at = Jwt.get_expiration_date(token) + return cls(expires_at, token) diff --git a/tests/http/test_client.py b/tests/http/test_client.py index a1584c25..607458d1 100644 --- a/tests/http/test_client.py +++ b/tests/http/test_client.py @@ -53,7 +53,7 @@ def test_request_valid_token(): def test_request_expired_token(): session = Session() session.configure(use_jwt=True) - previous_jwt = session.jwt_token + previous_jwt = session.jwt_token.token with freeze_time(dt.datetime.utcnow() + dt.timedelta(days=40)): response = session.get('/api_keys') assert response['items'] diff --git a/tests/test_jwt.py b/tests/test_jwt.py new file mode 100644 index 00000000..2eab8cf2 --- /dev/null +++ b/tests/test_jwt.py @@ -0,0 +1,43 @@ +import datetime as dt +from unittest.mock import MagicMock + +import pytest + +from cuenca.exc import MalformedJwtToken +from cuenca.jwt import Jwt + +TEST_TOKEN = ( + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6' + 'IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE2MDYyNjI0MDB9.gbrIYP6Q0yf2' + 'V3GEgxnzO5fNHKgHPqaIzZ-cjvFlnik' +) + + +@pytest.mark.parametrize( + 'expires_at, should_be_expired', + [ + (dt.datetime.utcnow() + dt.timedelta(days=-1), True), + (dt.datetime.utcnow() + dt.timedelta(days=1), False), + ], +) +def test_expired(expires_at: dt.datetime, should_be_expired: bool): + jwt = Jwt(expires_at, '_') + assert jwt.is_expired == should_be_expired + + +def test_get_expiration_date(): + assert Jwt.get_expiration_date(TEST_TOKEN) == dt.datetime(2020, 11, 25) + + +def test_invalid_token(): + test_token = 'INVALID' + with pytest.raises(MalformedJwtToken): + Jwt.get_expiration_date(test_token) + + +def test_create_token(): + session = MagicMock() + session.post.return_value = dict(token=TEST_TOKEN) + jwt = Jwt.create(session) + assert jwt.expires_at == dt.datetime(2020, 11, 25) + assert jwt.token == TEST_TOKEN From a8814080605f0190e6be12500f68cd251aa4bfd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20S=C3=A1nchez?= Date: Wed, 23 Dec 2020 18:30:53 -0600 Subject: [PATCH 17/24] README clarification --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index d2945c75..4556e074 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,22 @@ import cuenca cuenca.configure(api_key='PKxxxx', api_secret='yyyyyy') ``` +## Jwt + +JWT tokens can also be used if your credentials have enough permissions. To +do so, you may include the parameter `use_jwt` as part of your `configure` + +```python +import cuenca + +cuenca.configure(api_key='PKxxxx', api_secret='yyyyyy', use_jwt=True) +``` + +A new token will be created at this moment and automatically renewed before +sending any request if there is less than 5 minutes to be expired according +to its payload data. + + ## Transfers ### Create transfer From 86c2051f6c4a6b7de36ee97ae530147a1d1555e4 Mon Sep 17 00:00:00 2001 From: Ricardo Date: Wed, 23 Dec 2020 18:33:03 -0600 Subject: [PATCH 18/24] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4556e074..b3800f51 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ import cuenca cuenca.configure(api_key='PKxxxx', api_secret='yyyyyy') ``` -## Jwt +### Jwt JWT tokens can also be used if your credentials have enough permissions. To do so, you may include the parameter `use_jwt` as part of your `configure` From fd6fa401c7e8cb0c797b1d4443628fb81b6773af Mon Sep 17 00:00:00 2001 From: Ricardo Date: Wed, 23 Dec 2020 18:34:34 -0600 Subject: [PATCH 19/24] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b3800f51..56958291 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ do so, you may include the parameter `use_jwt` as part of your `configure` ```python import cuenca -cuenca.configure(api_key='PKxxxx', api_secret='yyyyyy', use_jwt=True) +cuenca.configure(use_jwt=True) ``` A new token will be created at this moment and automatically renewed before From 5dc849cb746e714f7140e6f952c7c61ee9425dbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20S=C3=A1nchez?= Date: Wed, 23 Dec 2020 18:38:55 -0600 Subject: [PATCH 20/24] Fix comment --- cuenca/jwt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cuenca/jwt.py b/cuenca/jwt.py index a8a4f3c8..2d536b0e 100644 --- a/cuenca/jwt.py +++ b/cuenca/jwt.py @@ -22,7 +22,7 @@ def is_expired(self) -> bool: def get_expiration_date(token: str) -> dt.datetime: """ Jwt tokens contains the exp field in the payload data, - this function extracts the date so we can't validate the + this function extracts the date so we can validate the token before any request More info about JWT tokens at: https://jwt.io/ """ From 3e5341144c3bd825158eec0784401d0cf898952e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20S=C3=A1nchez?= Date: Thu, 7 Jan 2021 18:09:06 -0600 Subject: [PATCH 21/24] Add type hint --- cuenca/jwt.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/cuenca/jwt.py b/cuenca/jwt.py index 2d536b0e..b4335d77 100644 --- a/cuenca/jwt.py +++ b/cuenca/jwt.py @@ -2,10 +2,14 @@ import binascii import datetime as dt import json +import typing from dataclasses import dataclass from .exc import MalformedJwtToken +if typing.TYPE_CHECKING: + from .http import Session + @dataclass class Jwt: @@ -36,8 +40,8 @@ def get_expiration_date(token: str) -> dt.datetime: return dt.datetime.utcfromtimestamp(exp_timestamp) @classmethod - def create(cls, session) -> 'Jwt': + def create(cls, session: 'Session') -> 'Jwt': session.jwt_token = None - token = session.post('/token', data=None)['token'] + token = session.post('/token', dict())['token'] expires_at = Jwt.get_expiration_date(token) return cls(expires_at, token) From 837580bd9b826424dea0ac954e25df562e04e80c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20S=C3=A1nchez?= Date: Thu, 7 Jan 2021 18:47:12 -0600 Subject: [PATCH 22/24] Fix auth --- cuenca/http/client.py | 7 +------ cuenca/jwt.py | 2 +- tests/http/test_client.py | 2 +- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/cuenca/http/client.py b/cuenca/http/client.py index 0f403f2f..ce807b38 100644 --- a/cuenca/http/client.py +++ b/cuenca/http/client.py @@ -43,12 +43,7 @@ def __init__(self): @property def auth(self) -> Optional[Tuple[str, str]]: - # preference to basic auth - return ( - self.basic_auth - if all(self.basic_auth) and not self.jwt_token - else None - ) + return self.basic_auth if all(self.basic_auth) else None def configure( self, diff --git a/cuenca/jwt.py b/cuenca/jwt.py index b4335d77..e1867000 100644 --- a/cuenca/jwt.py +++ b/cuenca/jwt.py @@ -8,7 +8,7 @@ from .exc import MalformedJwtToken if typing.TYPE_CHECKING: - from .http import Session + from .http import Session # pragma: no cover @dataclass diff --git a/tests/http/test_client.py b/tests/http/test_client.py index 607458d1..53db6066 100644 --- a/tests/http/test_client.py +++ b/tests/http/test_client.py @@ -32,7 +32,7 @@ def test_basic_auth_configuration(): def test_configures_jwt(): session = Session() session.configure(use_jwt=True) - assert not session.auth + assert session.auth assert session.jwt_token From 64506c52da4a973b9cb0925bc5d2b958ef6e4ae1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20S=C3=A1nchez?= Date: Sat, 9 Jan 2021 12:10:35 -0600 Subject: [PATCH 23/24] Add ignore in config --- cuenca/jwt.py | 6 +++--- setup.cfg | 5 +++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/cuenca/jwt.py b/cuenca/jwt.py index e1867000..3bf40a8d 100644 --- a/cuenca/jwt.py +++ b/cuenca/jwt.py @@ -2,13 +2,13 @@ import binascii import datetime as dt import json -import typing from dataclasses import dataclass +from typing import TYPE_CHECKING from .exc import MalformedJwtToken -if typing.TYPE_CHECKING: - from .http import Session # pragma: no cover +if TYPE_CHECKING: + from .http import Session @dataclass diff --git a/setup.cfg b/setup.cfg index e5b11289..48119983 100644 --- a/setup.cfg +++ b/setup.cfg @@ -19,3 +19,8 @@ ignore_missing_imports = True [mypy-freezegun.*] ignore_missing_imports = True + +[coverage:report] +exclude_lines = + pragma: no cover + if TYPE_CHECKING: From aed0a32247a1b670f1878e7b08d9181d4cc9c188 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20S=C3=A1nchez?= Date: Tue, 12 Jan 2021 20:28:37 -0600 Subject: [PATCH 24/24] Rebase --- tests/http/test_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/http/test_client.py b/tests/http/test_client.py index 53db6066..e2580e0f 100644 --- a/tests/http/test_client.py +++ b/tests/http/test_client.py @@ -1,5 +1,5 @@ -from unittest.mock import MagicMock, patch import datetime as dt +from unittest.mock import MagicMock, patch import pytest from freezegun import freeze_time