Merge pull request #1 from alphagov/implement_auth

Implement auth
This commit is contained in:
minglis
2015-12-15 14:38:52 +00:00
13 changed files with 277 additions and 8 deletions

View File

@@ -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
mkvirtualenv -p /usr/local/bin/python3 notifications-api

View File

@@ -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__)

View File

@@ -1,7 +1,15 @@
from flask import Blueprint
from app.main.authentication.auth import requires_auth
AUTHORIZATION_HEADER = 'Authorization'
AUTHORIZATION_SCHEME = 'Bearer'
WINDOW = 1
main = Blueprint('main', __name__)
from .views import notifications, index
main.before_request(requires_auth)
from app.main.views import notifications, index
from app.main import errors

View File

@@ -0,0 +1,51 @@
from flask import request, jsonify, _request_ctx_stack
from client.authentication 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"
}

36
app/main/errors.py Normal file
View File

@@ -0,0 +1,36 @@
from flask import jsonify
from app.main 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

View File

@@ -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

View File

@@ -1,2 +1,5 @@
Flask==0.10.1
Flask-Script==2.0.5
Flask-Script==2.0.5
PyJWT==1.4.0
git+https://github.com/alphagov/notifications-python-client.git@0.1.5#egg=notifications-python-client==0.1.5

View File

@@ -1,3 +1,6 @@
-r requirements.txt
pep8==1.5.7
pytest==2.8.1
pytest==2.8.1
pytest-mock==0.8.1
pytest-cov==2.2.0
mock==1.0.1

0
tests/app/__init__.py Normal file
View File

View File

View File

@@ -0,0 +1,131 @@
from flask import json
from client.authentication 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 == 200
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'

31
tests/conftest.py Normal file
View File

@@ -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()

View File

@@ -1,2 +0,0 @@
def test_app():
assert 1 == 1