requests: add http hawk authenticator (#23693)

This commit is contained in:
Serghei Mihai 2018-05-14 15:03:56 +02:00
parent 9018900d8e
commit ace757d68c
3 changed files with 112 additions and 0 deletions

View File

@ -0,0 +1,70 @@
# passerelle - uniform access to multiple data sources and services
# Copyright (C) 2018 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import base64
import hashlib
import hmac
import time
import urlparse
from uuid import uuid4
from requests.auth import AuthBase
class HawkAuth(AuthBase):
def __init__(self, id, key, algorithm='sha256', ext=''):
self.id = id.encode('utf-8')
self.key = key.encode('utf-8')
self.algorithm = algorithm
self.timestamp = str(int(time.time()))
self.nonce = uuid4().hex
self.ext = ext
def get_payload_hash(self, req):
p_hash = hashlib.new(self.algorithm)
p_hash.update('hawk.1.payload\n')
p_hash.update(req.headers.get('Content-Type', '') + '\n')
p_hash.update(req.body or '')
p_hash.update('\n')
return base64.b64encode(p_hash.digest())
def get_authorization_header(self, req):
url_parts = urlparse.urlparse(req.url)
uri = url_parts.path
if url_parts.query:
uri += '?' + url_parts.query
if url_parts.port is None:
if url_parts.scheme == 'http':
port = '80'
elif url_parts.scheme == 'https':
port = '443'
hash = self.get_payload_hash(req)
data = ['hawk.1.header', self.timestamp, self.nonce, req.method.upper(), uri,
url_parts.hostname, port, hash, self.ext, '']
digestmod = getattr(hashlib, self.algorithm)
result = hmac.new(self.key, '\n'.join(data), digestmod)
mac = base64.b64encode(result.digest())
authorization = 'Hawk id="%s", ts="%s", nonce="%s", hash="%s", mac="%s"'% (self.id, self.timestamp, self.nonce,
hash, mac)
if self.ext:
authorization += ', ext="%s"' % self.ext
return authorization
def __call__(self, r):
r.headers['Authorization'] = self.get_authorization_header(r)
return r

View File

@ -1,12 +1,14 @@
import logging
import pytest
import mohawk
import mock
from httmock import urlmatch, HTTMock, response
from django.test import override_settings
from passerelle.utils import Request, CaseInsensitiveDict
from passerelle.utils.http_authenticators import HawkAuth
import utils
from utils import FakedResponse
@ -187,6 +189,45 @@ def test_resource_auth(mocked_get, caplog, endpoint_response):
request.get('http://example.net/whatever', auth=None)
assert mocked_get.call_args[1].get('auth') is None
@mock.patch('passerelle.utils.RequestSession.send')
def test_resource_hawk_auth(mocked_send, caplog, endpoint_response):
mocked_send.return_value = endpoint_response
logger = logging.getLogger('requests')
resource = MockResource()
request = Request(resource=resource, logger=logger)
credentials = {'id': 'id', 'key': 'key', 'algorithm': 'sha256'}
hawk_auth = HawkAuth(**credentials)
resp = request.get('http://httpbin.org/get', auth=hawk_auth)
prepared_method = mocked_send.call_args[0][0]
assert 'Authorization' in prepared_method.headers
generated_header = prepared_method.headers['Authorization']
sender = mohawk.Sender(credentials, nonce=hawk_auth.nonce, _timestamp=hawk_auth.timestamp,
url='http://httpbin.org/get', method='GET', content_type='',
content='')
expected_header = sender.request_header
generated_parts = [tuple(e.strip().split('=', 1)) for e in generated_header[5:].split(',')]
expected_parts = [tuple(e.strip().split('=', 1)) for e in expected_header[5:].split(',')]
# compare generated header elements
assert dict(generated_parts) == dict(expected_parts)
hawk_auth = HawkAuth(ext='extra attribute', **credentials)
resp = request.post('http://httpbin.org/post', auth=hawk_auth, json={'key': 'value'})
prepared_method = mocked_send.call_args[0][0]
assert 'Authorization' in prepared_method.headers
generated_header = prepared_method.headers['Authorization']
sender = mohawk.Sender(credentials, nonce=hawk_auth.nonce, _timestamp=hawk_auth.timestamp,
url='http://httpbin.org/post', method='POST', content_type='application/json',
content='{"key": "value"}', ext="extra attribute")
expected_header = sender.request_header
generated_parts = [tuple(e.strip().split('=', 1)) for e in generated_header[5:].split(',')]
expected_parts = [tuple(e.strip().split('=', 1)) for e in expected_header[5:].split(',')]
assert dict(generated_parts) == dict(expected_parts)
@mock.patch('passerelle.utils.RequestSession.request')
def test_resource_certificates(mocked_get, caplog, endpoint_response):

View File

@ -24,6 +24,7 @@ deps =
pylint-django<0.9
django-webtest<1.9.3
lxml
mohawk
commands =
py.test {env:FAST:} {env:COVERAGE:} {posargs:tests/}
pylint: ./pylint.sh passerelle/