requests: add http hawk authenticator (#23693)
This commit is contained in:
parent
9018900d8e
commit
ace757d68c
|
@ -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
|
|
@ -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):
|
||||
|
|
Loading…
Reference in New Issue