From 6deaa61011472d71654869b9e3a0162ebfe0817b Mon Sep 17 00:00:00 2001 From: Martyn Inglis Date: Fri, 11 Dec 2015 16:57:00 +0000 Subject: [PATCH 1/5] Implement a JWT header into base client - adds a base client - adds a notifications client These do not proxy onto genuine methods. This pull request is the basic implication of the API Client. Still needs a few things before is ready, notably proper logging and actual API endpoints to hook into. Basic premise is to deliver the JWT tokens required for Notify API authentication so we can discuss the implementation/premise. --- README.md | 2 +- app/main/__init__.py | 95 ++++++++++++++++++++++++++- app/main/errors.py | 36 ++++++++++ app/main/views/index.py | 7 +- requirements.txt | 3 +- requirements_for_test.txt | 5 +- tests/app/__init__.py | 0 tests/app/main/test_authentication.py | 82 +++++++++++++++++++++++ tests/app/main/views/__init__.py | 0 tests/conftest.py | 31 +++++++++ tests/test_app.py | 2 - 11 files changed, 256 insertions(+), 7 deletions(-) create mode 100644 app/main/errors.py create mode 100644 tests/app/__init__.py create mode 100644 tests/app/main/test_authentication.py create mode 100644 tests/app/main/views/__init__.py create mode 100644 tests/conftest.py delete mode 100644 tests/test_app.py diff --git a/README.md b/README.md index 639fa11f2..95fda4be3 100644 --- a/README.md +++ b/README.md @@ -7,4 +7,4 @@ Application for the notification api. Read and write notifications/status queue. Get and update notification status. -mkvirtualenv -p /usr/local/bin/python3 notify-api \ No newline at end of file +mkvirtualenv -p /usr/local/bin/python3 notifications-api \ No newline at end of file diff --git a/app/main/__init__.py b/app/main/__init__.py index c1219ffbf..3bc30c684 100644 --- a/app/main/__init__.py +++ b/app/main/__init__.py @@ -1,7 +1,100 @@ -from flask import Blueprint +from flask import Blueprint, request, abort +from jwt import decode, DecodeError +import calendar +import time +import base64 +import hashlib +import hmac +AUTHORIZATION_HEADER = 'Authorization' +AUTHORIZATION_SCHEME = 'Bearer' +WINDOW = 1 main = Blueprint('main', __name__) +def get_secrets(service_identifier): + """ + Temp method until secrets are stored in database etc + :param service_identifier: + :return: (Boolean, String) + """ + secrets = { + 'service1': '1234' + } + if service_identifier not in secrets: + return False, None + return True, secrets[service_identifier] + + +def get_token_from_headers(headers): + auth_header = headers.get(AUTHORIZATION_HEADER, '') + if auth_header[:7] != AUTHORIZATION_SCHEME + " ": + return None + return auth_header[7:] + + +def token_is_valid(token): + try: + # decode token to get service identifier + # signature not checked + unverified = decode(token, verify=False, algorithms=['HS256']) + + # service identifier used to get secret + found, secret = get_secrets(unverified['iss']) + + # use secret to validate the token + verified = decode(token, key=secret.encode(), verify=True, algorithms=['HS256']) + + # check expiry + if not calendar.timegm(time.gmtime()) < verified['iat'] + WINDOW: + print("TIMESTAMP FAILED") + return False + + # check request + signed_url = base64.b64encode( + hmac.new( + secret.encode(), + "{} {}".format(request.method, request.path).encode(), + digestmod=hashlib.sha256 + ).digest() + ).decode() + if signed_url != verified['req']: + print("URL FAILED") + return False + + # check body + signed_json_request = base64.b64encode( + hmac.new( + secret.encode(), + request.data, + digestmod=hashlib.sha256 + ).digest() + ).decode() + + print(verified) + + if signed_json_request != verified['pay']: + print("PAYLOAD FAILED") + return False + + except DecodeError: + print("TOKEN VERIFICATION FAILED") + return False + + return True + + +def perform_authentication(): + incoming_token = get_token_from_headers(request.headers) + if not incoming_token: + abort(401) + if not token_is_valid(incoming_token): + abort(403) + + +main.before_request(perform_authentication) + + from .views import notifications, index +from . import errors diff --git a/app/main/errors.py b/app/main/errors.py new file mode 100644 index 000000000..9c7879b00 --- /dev/null +++ b/app/main/errors.py @@ -0,0 +1,36 @@ +from flask import jsonify + +from . import main + + +@main.app_errorhandler(400) +def bad_request(e): + return jsonify(error=str(e.description)), 400 + + +@main.app_errorhandler(401) +def unauthorized(e): + error_message = "Unauthorized, authentication token must be provided" + return jsonify(error=error_message), 401, [('WWW-Authenticate', 'Bearer')] + + +@main.app_errorhandler(403) +def forbidden(e): + error_message = "Forbidden, invalid authentication token provided" + return jsonify(error=error_message), 403 + + +@main.app_errorhandler(404) +def page_not_found(e): + return jsonify(error=e.description or "Not found"), 404 + + +@main.app_errorhandler(429) +def limit_exceeded(e): + return jsonify(error=str(e.description)), 429 + + +@main.app_errorhandler(500) +def internal_server_error(e): + # TODO: log the error + return jsonify(error="Internal error"), 500 diff --git a/app/main/views/index.py b/app/main/views/index.py index ad8c1c98d..4c1b535e5 100644 --- a/app/main/views/index.py +++ b/app/main/views/index.py @@ -3,5 +3,10 @@ from .. import main @main.route('/', methods=['GET']) -def index(): +def get_index(): + return jsonify(result="hello world"), 200 + + +@main.route('/', methods=['POST']) +def post_index(): return jsonify(result="hello world"), 200 diff --git a/requirements.txt b/requirements.txt index f49991eb8..3031f04ff 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ Flask==0.10.1 -Flask-Script==2.0.5 \ No newline at end of file +Flask-Script==2.0.5 +PyJWT==1.4.0 diff --git a/requirements_for_test.txt b/requirements_for_test.txt index f8aeacc70..263c2f4da 100644 --- a/requirements_for_test.txt +++ b/requirements_for_test.txt @@ -1,3 +1,6 @@ -r requirements.txt pep8==1.5.7 -pytest==2.8.1 \ No newline at end of file +pytest==2.8.1 +pytest-mock==0.8.1 +pytest-cov==2.2.0 +mock==1.0.1 \ No newline at end of file diff --git a/tests/app/__init__.py b/tests/app/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/app/main/test_authentication.py b/tests/app/main/test_authentication.py new file mode 100644 index 000000000..f8fd55f34 --- /dev/null +++ b/tests/app/main/test_authentication.py @@ -0,0 +1,82 @@ +from flask import json +import jwt +import hashlib +import hmac +import calendar +import time +import base64 + + +def sign_a_thing(thing_to_sign, secret_key): + return base64.b64encode( + hmac.new( + secret_key.encode(), + thing_to_sign.encode(), + digestmod=hashlib.sha256 + ).digest() + ) + + +def token(request_method, request_url, secret_key, service_id, json_body): + + # request method and resource path - hash of request url - POST /path-to-resource + signed_url = sign_a_thing("{} {}".format(request_method, request_url), secret_key) + signed_payload = sign_a_thing(json_body, secret_key) + + headers = { + "typ": "JWT", + "alg": "HS256" + } + + claims = { + 'iss': service_id, # issued by - identified by id of the service + 'iat': calendar.timegm(time.gmtime()), # issued at in epoch seconds + 'req': signed_url.decode(), + 'pay': signed_payload.decode() # signed payload + } + + return jwt.encode(payload=claims, key=secret_key, headers=headers).decode() + + +def test_should_not_allow_request_with_no_token(notify_api): + response = notify_api.test_client().get("/") + assert response.status_code == 401 + data = json.loads(response.get_data()) + assert data['error'] == 'Unauthorized, authentication token must be provided' + + +def test_should_not_allow_request_with_invalid_token(notify_api): + response = notify_api.test_client().get( + "/", + headers={'Authorization': 'Bearer 1234'} + ) + assert response.status_code == 403 + data = json.loads(response.get_data()) + assert data['error'] == 'Forbidden, invalid authentication token provided' + + +def test_should_allow_request_with_valid_token(notify_api): + + body = { + "1": "A", + "3": "A", + "7": "A", + "15": "A", + "2": "B" + } + request_body = json.dumps(body) + + request_token = token("POST", "/", "1234", "service1", request_body) + + # from time import sleep + # sleep(4) + + response = notify_api.test_client().post( + "/", + data=request_body, + headers={ + 'Authorization': "Bearer {}".format(request_token) + }, + content_type='application/json' + ) + assert response.status_code == 200 diff --git a/tests/app/main/views/__init__.py b/tests/app/main/views/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 000000000..7d8cf6f23 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,31 @@ +import pytest +import mock +from app import create_app +from config import configs + + +@pytest.fixture(scope='session') +def notify_api(request): + app = create_app('test') + ctx = app.app_context() + ctx.push() + + def teardown(): + ctx.pop() + + request.addfinalizer(teardown) + return app + + +@pytest.fixture(scope='function') +def notify_config(notify_api): + notify_api.config['NOTIFY_API_ENVIRONMENT'] = 'test' + notify_api.config.from_object(configs['test']) + + +@pytest.fixture(scope='function') +def os_environ(request): + env_patch = mock.patch('os.environ', {}) + request.addfinalizer(env_patch.stop) + + return env_patch.start() diff --git a/tests/test_app.py b/tests/test_app.py deleted file mode 100644 index 3fbfeedd1..000000000 --- a/tests/test_app.py +++ /dev/null @@ -1,2 +0,0 @@ -def test_app(): - assert 1 == 1 From 6a3bbbf890bb815e48accee16adc72ebb72b0f97 Mon Sep 17 00:00:00 2001 From: Martyn Inglis Date: Mon, 14 Dec 2015 12:58:49 +0000 Subject: [PATCH 2/5] Added python client to dependencies --- requirements.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/requirements.txt b/requirements.txt index 3031f04ff..27f67358a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,5 @@ Flask==0.10.1 Flask-Script==2.0.5 PyJWT==1.4.0 + +git+https://github.com/alphagov/notifications-python-client.git@0.0.2#egg=notifications-python-client==0.0.2 From dbf70ec1ebf2458cfc93ed0cdec3f3cbca288e07 Mon Sep 17 00:00:00 2001 From: Martyn Inglis Date: Tue, 15 Dec 2015 10:47:20 +0000 Subject: [PATCH 3/5] First pass at implementing API authentication using new JWT tokens - NOTE - this does not manage secrets. There is only one URL and there is no functionality implemented - prior to rolling out we need to store secrets properly Uses the JWT libraries in [https://github.com/alphagov/notifications-python-client](https://github.com/alphagov/notifications-python-client) - Tokens are checked on every request and will be rejected if token is invalid as per the rules in the python clients. --- app/__init__.py | 5 +- app/main/__init__.py | 91 +------------- app/main/authentication/auth.py | 51 ++++++++ requirements.txt | 2 +- tests/app/main/test_authentication.py | 82 ------------ tests/app/main/views/test_authentication.py | 131 ++++++++++++++++++++ 6 files changed, 190 insertions(+), 172 deletions(-) create mode 100644 app/main/authentication/auth.py delete mode 100644 tests/app/main/test_authentication.py create mode 100644 tests/app/main/views/test_authentication.py diff --git a/app/__init__.py b/app/__init__.py index e2acdc531..1c12d0066 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,9 +1,12 @@ import os from flask._compat import string_types -from flask import Flask +from flask import Flask, _request_ctx_stack +from werkzeug.local import LocalProxy from config import configs +api_user = LocalProxy(lambda: _request_ctx_stack.top.api_user) + def create_app(config_name): application = Flask(__name__) diff --git a/app/main/__init__.py b/app/main/__init__.py index 3bc30c684..67cf4dcfb 100644 --- a/app/main/__init__.py +++ b/app/main/__init__.py @@ -1,10 +1,5 @@ -from flask import Blueprint, request, abort -from jwt import decode, DecodeError -import calendar -import time -import base64 -import hashlib -import hmac +from flask import Blueprint +from app.main.authentication.auth import requires_auth AUTHORIZATION_HEADER = 'Authorization' AUTHORIZATION_SCHEME = 'Bearer' @@ -13,87 +8,7 @@ WINDOW = 1 main = Blueprint('main', __name__) -def get_secrets(service_identifier): - """ - Temp method until secrets are stored in database etc - :param service_identifier: - :return: (Boolean, String) - """ - secrets = { - 'service1': '1234' - } - if service_identifier not in secrets: - return False, None - return True, secrets[service_identifier] - - -def get_token_from_headers(headers): - auth_header = headers.get(AUTHORIZATION_HEADER, '') - if auth_header[:7] != AUTHORIZATION_SCHEME + " ": - return None - return auth_header[7:] - - -def token_is_valid(token): - try: - # decode token to get service identifier - # signature not checked - unverified = decode(token, verify=False, algorithms=['HS256']) - - # service identifier used to get secret - found, secret = get_secrets(unverified['iss']) - - # use secret to validate the token - verified = decode(token, key=secret.encode(), verify=True, algorithms=['HS256']) - - # check expiry - if not calendar.timegm(time.gmtime()) < verified['iat'] + WINDOW: - print("TIMESTAMP FAILED") - return False - - # check request - signed_url = base64.b64encode( - hmac.new( - secret.encode(), - "{} {}".format(request.method, request.path).encode(), - digestmod=hashlib.sha256 - ).digest() - ).decode() - if signed_url != verified['req']: - print("URL FAILED") - return False - - # check body - signed_json_request = base64.b64encode( - hmac.new( - secret.encode(), - request.data, - digestmod=hashlib.sha256 - ).digest() - ).decode() - - print(verified) - - if signed_json_request != verified['pay']: - print("PAYLOAD FAILED") - return False - - except DecodeError: - print("TOKEN VERIFICATION FAILED") - return False - - return True - - -def perform_authentication(): - incoming_token = get_token_from_headers(request.headers) - if not incoming_token: - abort(401) - if not token_is_valid(incoming_token): - abort(403) - - -main.before_request(perform_authentication) +main.before_request(requires_auth) from .views import notifications, index diff --git a/app/main/authentication/auth.py b/app/main/authentication/auth.py new file mode 100644 index 000000000..e0dcd0966 --- /dev/null +++ b/app/main/authentication/auth.py @@ -0,0 +1,51 @@ +from flask import request, jsonify, _request_ctx_stack +from client.jwt import decode_jwt_token, get_token_issuer +from client.errors import TokenDecodeError, TokenRequestError, TokenExpiredError, TokenPayloadError + + +def authentication_response(message, code): + return jsonify( + error=message + ), code + + +def requires_auth(): + auth_header = request.headers.get('Authorization', None) + if not auth_header: + return authentication_response('Unauthorized, authentication token must be provided', 401) + + auth_scheme = auth_header[:7] + + if auth_scheme != 'Bearer ': + return authentication_response('Unauthorized, authentication bearer scheme must be used', 401) + + try: + auth_token = auth_header[7:] + api_client = fetch_client(get_token_issuer(auth_token)) + + if api_client is None: + authentication_response("Invalid credentials", 403) + + decode_jwt_token( + auth_token, + api_client['secret'], + request.method, + request.path, + request.data.decode() if request.data else None + ) + _request_ctx_stack.top.api_user = api_client + except TokenRequestError: + return authentication_response("Invalid token: request", 403) + except TokenExpiredError: + return authentication_response("Invalid token: expired", 403) + except TokenPayloadError: + return authentication_response("Invalid token: payload", 403) + except TokenDecodeError: + return authentication_response("Invalid token: signature", 403) + + +def fetch_client(client): + return { + "client": client, + "secret": "secret" + } diff --git a/requirements.txt b/requirements.txt index 27f67358a..3fd5aeed9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,4 @@ Flask==0.10.1 Flask-Script==2.0.5 PyJWT==1.4.0 -git+https://github.com/alphagov/notifications-python-client.git@0.0.2#egg=notifications-python-client==0.0.2 +git+https://github.com/alphagov/notifications-python-client.git@0.1.2#egg=notifications-python-client==0.1.2 diff --git a/tests/app/main/test_authentication.py b/tests/app/main/test_authentication.py deleted file mode 100644 index f8fd55f34..000000000 --- a/tests/app/main/test_authentication.py +++ /dev/null @@ -1,82 +0,0 @@ -from flask import json -import jwt -import hashlib -import hmac -import calendar -import time -import base64 - - -def sign_a_thing(thing_to_sign, secret_key): - return base64.b64encode( - hmac.new( - secret_key.encode(), - thing_to_sign.encode(), - digestmod=hashlib.sha256 - ).digest() - ) - - -def token(request_method, request_url, secret_key, service_id, json_body): - - # request method and resource path - hash of request url - POST /path-to-resource - signed_url = sign_a_thing("{} {}".format(request_method, request_url), secret_key) - signed_payload = sign_a_thing(json_body, secret_key) - - headers = { - "typ": "JWT", - "alg": "HS256" - } - - claims = { - 'iss': service_id, # issued by - identified by id of the service - 'iat': calendar.timegm(time.gmtime()), # issued at in epoch seconds - 'req': signed_url.decode(), - 'pay': signed_payload.decode() # signed payload - } - - return jwt.encode(payload=claims, key=secret_key, headers=headers).decode() - - -def test_should_not_allow_request_with_no_token(notify_api): - response = notify_api.test_client().get("/") - assert response.status_code == 401 - data = json.loads(response.get_data()) - assert data['error'] == 'Unauthorized, authentication token must be provided' - - -def test_should_not_allow_request_with_invalid_token(notify_api): - response = notify_api.test_client().get( - "/", - headers={'Authorization': 'Bearer 1234'} - ) - assert response.status_code == 403 - data = json.loads(response.get_data()) - assert data['error'] == 'Forbidden, invalid authentication token provided' - - -def test_should_allow_request_with_valid_token(notify_api): - - body = { - "1": "A", - "3": "A", - "7": "A", - "15": "A", - "2": "B" - } - request_body = json.dumps(body) - - request_token = token("POST", "/", "1234", "service1", request_body) - - # from time import sleep - # sleep(4) - - response = notify_api.test_client().post( - "/", - data=request_body, - headers={ - 'Authorization': "Bearer {}".format(request_token) - }, - content_type='application/json' - ) - assert response.status_code == 200 diff --git a/tests/app/main/views/test_authentication.py b/tests/app/main/views/test_authentication.py new file mode 100644 index 000000000..f823e83e5 --- /dev/null +++ b/tests/app/main/views/test_authentication.py @@ -0,0 +1,131 @@ +from flask import json +from client.jwt import create_jwt_token + + +def test_should_not_allow_request_with_no_token(notify_api): + response = notify_api.test_client().get("/") + assert response.status_code == 401 + data = json.loads(response.get_data()) + assert data['error'] == 'Unauthorized, authentication token must be provided' + + +def test_should_not_allow_request_with_incorrect_header(notify_api): + response = notify_api.test_client().get( + "/", + headers={ + 'Authorization': 'Basic 1234' + } + ) + assert response.status_code == 401 + data = json.loads(response.get_data()) + assert data['error'] == 'Unauthorized, authentication bearer scheme must be used' + + +def test_should_not_allow_request_with_incorrect_token(notify_api): + response = notify_api.test_client().get( + "/", + headers={ + 'Authorization': 'Bearer 1234' + } + ) + assert response.status_code == 403 + data = json.loads(response.get_data()) + assert data['error'] == 'Invalid token: signature' + + +def test_should_not_allow_incorrect_path(notify_api): + token = create_jwt_token(request_method="GET", request_path="/bad", secret="secret", client_id="client_id") + response = notify_api.test_client().get( + "/", + headers={ + 'Authorization': "Bearer {}".format(token) + } + ) + assert response.status_code == 403 + data = json.loads(response.get_data()) + assert data['error'] == 'Invalid token: request' + + +def test_should_not_allow_incorrect_method(notify_api): + token = create_jwt_token(request_method="POST", request_path="/", secret="secret", client_id="client_id") + response = notify_api.test_client().get( + "/", + headers={ + 'Authorization': "Bearer {}".format(token) + } + ) + assert response.status_code == 403 + data = json.loads(response.get_data()) + assert data['error'] == 'Invalid token: request' + + +def test_should_not_allow_invalid_secret(notify_api): + token = create_jwt_token(request_method="POST", request_path="/", secret="not-so-secret", client_id="client_id") + response = notify_api.test_client().get( + "/", + headers={ + 'Authorization': "Bearer {}".format(token) + } + ) + assert response.status_code == 403 + data = json.loads(response.get_data()) + assert data['error'] == 'Invalid token: signature' + + +def test_should_allow_valid_token(notify_api): + token = create_jwt_token(request_method="GET", request_path="/", secret="secret", client_id="client_id") + response = notify_api.test_client().get( + "/", + headers={ + 'Authorization': 'Bearer {}'.format(token) + } + ) + assert response.status_code == 201 + + +def test_should_allow_valid_token_with_post_body(notify_api): + json_body = json.dumps({ + "key1": "value1", + "key2": "value2", + "key3": "value3" + }) + token = create_jwt_token( + request_method="POST", + request_path="/", + secret="secret", + client_id="client_id", + request_body=json_body + ) + response = notify_api.test_client().post( + "/", + data=json_body, + headers={ + 'Authorization': 'Bearer {}'.format(token) + } + ) + assert response.status_code == 200 + + +def test_should_not_allow_valid_token_with_invalid_post_body(notify_api): + json_body = json.dumps({ + "key1": "value1", + "key2": "value2", + "key3": "value3" + }) + token = create_jwt_token( + request_method="POST", + request_path="/", + secret="secret", + client_id="client_id", + request_body=json_body + ) + response = notify_api.test_client().post( + "/", + data="spurious", + headers={ + 'Authorization': 'Bearer {}'.format(token) + } + ) + assert response.status_code == 403 + data = json.loads(response.get_data()) + assert data['error'] == 'Invalid token: payload' From 87138c86f8ac98b79e523d5aac89a8e86b519eeb Mon Sep 17 00:00:00 2001 From: Martyn Inglis Date: Tue, 15 Dec 2015 11:54:16 +0000 Subject: [PATCH 4/5] Fix failing build - test deliberatly moved to error for some investigations. Fixed now. --- tests/app/main/views/test_authentication.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/app/main/views/test_authentication.py b/tests/app/main/views/test_authentication.py index f823e83e5..eb7f2aa83 100644 --- a/tests/app/main/views/test_authentication.py +++ b/tests/app/main/views/test_authentication.py @@ -80,7 +80,7 @@ def test_should_allow_valid_token(notify_api): 'Authorization': 'Bearer {}'.format(token) } ) - assert response.status_code == 201 + assert response.status_code == 200 def test_should_allow_valid_token_with_post_body(notify_api): From 4caf0fcad622b689f2436f7f91e0c6816d8931cf Mon Sep 17 00:00:00 2001 From: Martyn Inglis Date: Tue, 15 Dec 2015 14:36:57 +0000 Subject: [PATCH 5/5] Fixing pull requests comments - full path to imports - renamed client lib from jwt->authentication --- app/main/__init__.py | 4 ++-- app/main/authentication/auth.py | 2 +- app/main/errors.py | 2 +- requirements.txt | 2 +- tests/app/main/views/test_authentication.py | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/main/__init__.py b/app/main/__init__.py index 67cf4dcfb..9631e7011 100644 --- a/app/main/__init__.py +++ b/app/main/__init__.py @@ -11,5 +11,5 @@ main = Blueprint('main', __name__) main.before_request(requires_auth) -from .views import notifications, index -from . import errors +from app.main.views import notifications, index +from app.main import errors diff --git a/app/main/authentication/auth.py b/app/main/authentication/auth.py index e0dcd0966..2762d5052 100644 --- a/app/main/authentication/auth.py +++ b/app/main/authentication/auth.py @@ -1,5 +1,5 @@ from flask import request, jsonify, _request_ctx_stack -from client.jwt import decode_jwt_token, get_token_issuer +from client.authentication import decode_jwt_token, get_token_issuer from client.errors import TokenDecodeError, TokenRequestError, TokenExpiredError, TokenPayloadError diff --git a/app/main/errors.py b/app/main/errors.py index 9c7879b00..d7720e5ac 100644 --- a/app/main/errors.py +++ b/app/main/errors.py @@ -1,6 +1,6 @@ from flask import jsonify -from . import main +from app.main import main @main.app_errorhandler(400) diff --git a/requirements.txt b/requirements.txt index 3fd5aeed9..34cea9bc1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,4 @@ Flask==0.10.1 Flask-Script==2.0.5 PyJWT==1.4.0 -git+https://github.com/alphagov/notifications-python-client.git@0.1.2#egg=notifications-python-client==0.1.2 +git+https://github.com/alphagov/notifications-python-client.git@0.1.5#egg=notifications-python-client==0.1.5 diff --git a/tests/app/main/views/test_authentication.py b/tests/app/main/views/test_authentication.py index eb7f2aa83..dd7d482cd 100644 --- a/tests/app/main/views/test_authentication.py +++ b/tests/app/main/views/test_authentication.py @@ -1,5 +1,5 @@ from flask import json -from client.jwt import create_jwt_token +from client.authentication import create_jwt_token def test_should_not_allow_request_with_no_token(notify_api):