├── .gitignore ├── .travis.yml ├── CHANGES.txt ├── CONTRIBUTORS.txt ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── TODO.txt ├── manage.py ├── rest_framework_httpsignature ├── __init__.py ├── authentication.py ├── models.py ├── tests.py └── views.py ├── setup.py ├── test_settings.py └── utils ├── sign3.py └── test-install.sh /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.pyc 3 | dist/ 4 | build/ 5 | djangorestframework_httpsignature.egg-info/ 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - "2.7" 5 | 6 | before_install: 7 | - "pip install --upgrade pip wheel" 8 | - "pip install --use-wheel httpsig" 9 | 10 | install: 11 | - "python setup.py install" 12 | 13 | script: "python manage.py test" -------------------------------------------------------------------------------- /CHANGES.txt: -------------------------------------------------------------------------------- 1 | v1.0.0, 20150501 -- Relaxed dependencies 2 | 3 | * Let Django version float all the way up to 1.8.x and above 4 | Thanks Markus Holtermann 5 | * Let DRF version float all the way up to 3.x and above 6 | Thanks Sofia Margariti 7 | 8 | v0.2.1, 20140818 -- Fix tests 9 | 10 | * Update to latest version of httpsig 11 | * Brought 0.1.x fixes in to 0.2.x branch 12 | 13 | v0.2.0, 20140803 -- Move to use httpsig package 14 | 15 | * Update documentation 16 | * Adapt to httpsig (thanks Konstantinos Koukopoulos) 17 | * Using draft 03 lastest version 18 | 19 | v0.1.8, 20140818 -- Improve dependency management 20 | 21 | * Loosen dependencies in setup.py 22 | * Fix signature/headers regular expressions 23 | (Thanks Christopher Grebs) 24 | 25 | v0.1.7, 20140802 -- Fix fetching user data 26 | 27 | * Correctly handle a bad API KEY, adapt test code 28 | (Thanks @benctamas) 29 | 30 | v0.1.6, 20140706 -- Slight improvement to API 31 | 32 | * Return API key on successful authentication 33 | (Thanks Konstantinos Koukopoulos) 34 | 35 | v0.1.5, 20140613 -- Document installation issue 36 | 37 | * Document workaround on installation problems 38 | 39 | v0.1.4, 20140613 -- Improve installation 40 | 41 | * Make requirements file comply with docs 42 | * Decide on http_signature commit 43 | 44 | v0.1.3, 20140220 -- Upload to PyPI 45 | 46 | * Prepare docs to upload package to PyPI 47 | 48 | v0.1.2, 20140219 -- Package data and clean up 49 | 50 | * Updated package classifiers 51 | * Cleaned up unused code in authentication.py 52 | 53 | v0.1.1, 20140217 -- Documentation and clean up 54 | 55 | * The package can be installed 56 | * Continuous integration via Travis 57 | * Unit tests for the authentication code 58 | * General docuementation in the README file 59 | 60 | v0.1.0, 20140217 -- Initial release 61 | -------------------------------------------------------------------------------- /CONTRIBUTORS.txt: -------------------------------------------------------------------------------- 1 | Christopher Grebs - Dependency management and regexp improvements. 2 | Konstantinos Koukopoulos - Improve API and adapt to httpsig package. 3 | "benctamas" - Code review and fix. 4 | Alberto Gragera - Installation troubleshooting. 5 | David Lehn - Draft and Implemtation insight. 6 | Elvio Toccalino - Developer, maintainer. 7 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Elvio Toccalino 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.txt 2 | include *.rst 3 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | =================================== 2 | django-rest-framework-httpsignature 3 | =================================== 4 | 5 | .. image:: https://travis-ci.org/etoccalino/django-rest-framework-httpsignature.png?branch=master 6 | :target: https://travis-ci.org/etoccalino/django-rest-framework-httpsignature 7 | 8 | 9 | Overview 10 | ======== 11 | 12 | Provides `HTTP Signature`_ support for `Django REST framework`_. The HTTP Signature package provides a way to achieve origin authentication and message integrity for HTTP messages. Similar to Amazon's `HTTP Signature scheme`_, used by many of its services. The `HTTP Signature`_ specification is currently an IETF draft. 13 | 14 | .. contents:: 15 | 16 | Installation 17 | ============ 18 | 19 | Installing the package via the repository:: 20 | 21 | pip install djangorestframework-httpsignature 22 | 23 | Older version of ``pip`` don't support the Wheel format (which is how ``httpsig`` is distributed). The problem manifests when installing the requirements, ``pip`` will complain that it cannot find a ``httpsig``. In such cases, ``pip`` needs to be upgraded:: 24 | 25 | pip install --upgrade pip 26 | 27 | Another possible problem: while installing via ``python setup.py install`` you may encounter:: 28 | 29 | No local packages or download links found for httpsig 30 | error: Could not find suitable distribution for Requirement.parse('httpsig') 31 | 32 | If that is the case, use ``pip install httpsig`` to install the ``httpsig`` package and retry ``python setup.py install``. 33 | 34 | Running the tests 35 | ================= 36 | 37 | To run the tests for the packages, use the following command on the repository root directory:: 38 | 39 | python manage.py test 40 | 41 | Usage 42 | ===== 43 | 44 | To authenticate HTTP requests via HTTP signature, you need to: 45 | 46 | 1. Install this package in your Django project, as instructed in `Installation`_. 47 | 2. Add ``rest_framework_httpsignature`` to your ``settings.py`` INSTALLED_APPS. 48 | 3. In your app code, extend the ``SignatureAuthentication`` class, as follows:: 49 | 50 | # my_api/auth.py 51 | 52 | from rest_framework_httpsignature.authentication import SignatureAuthentication 53 | 54 | class MyAPISignatureAuthentication(SignatureAuthentication): 55 | # The HTTP header used to pass the consumer key ID. 56 | # Defaults to 'X-Api-Key'. 57 | API_KEY_HEADER = 'X-Api-Key' 58 | 59 | # A method to fetch (User instance, user_secret_string) from the 60 | # consumer key ID, or None in case it is not found. 61 | def fetch_user_data(self, api_key): 62 | # ... 63 | # example implementation: 64 | try: 65 | user = User.objects.get(api_key=api_key) 66 | return (user, user.secret) 67 | except User.DoesNotExist: 68 | return None 69 | 70 | 71 | 4. Configure Django REST framework to use you authentication class; e.g.:: 72 | 73 | # my_project/settings.py 74 | 75 | # ... 76 | REST_FRAMEWORK = { 77 | 'DEFAULT_AUTHENTICATION_CLASSES': ( 78 | 'my_api.auth.MyAPISignatureAuthentication', 79 | ), 80 | 'DEFAULT_PERMISSION_CLASSES': ( 81 | 'rest_framework.permissions.IsAuthenticated', 82 | ) 83 | } 84 | # The above will force HTTP signature for all requests. 85 | # ... 86 | 87 | You can also use another algorithm that is provided by `httpsig package`_:: 88 | 89 | class MyAPISignatureAuthentication(SignatureAuthentication): 90 | API_KEY_HEADER = 'X-Api-Key' 91 | ALGORITHM = 'rsa-256' 92 | 93 | def fetch_user_data(self, api_key): 94 | try: 95 | user = User.objects.get(api_key=api_key) 96 | fpath = user.private_key 97 | with open(fpath, 'rb') as fobj: 98 | secret = fobj.read() 99 | return (user, secret) 100 | except User.DoesNotExist: 101 | return (None, None) 102 | 103 | Currently supported throught ``httpsig`` are: ``rsa-sha1``, ``rsa-sha256``, ``rsa-sha512``, ``hmac-sha1``, ``hmac-sha256``, ``hmac-sha512`` 104 | 105 | Roadmap 106 | ======= 107 | 108 | - The ``REQUIREMENTS.txt`` file is fairly strict. It is very possible that previous versions of Django and Django REST framework are supported. 109 | - Since HTTP Signature uses a HTTP header for the request date and time, the authentication class could deal with request expiry. 110 | 111 | 112 | Example usage & session w/cURL 113 | ============================== 114 | 115 | Assuming the setup detailed in `Usage`_, a project running on ``localhost:8000`` could be probed with cURL as follows:: 116 | 117 | ~$ SSS=Base64(Hmac(SECRET, "Date: Mon, 17 Feb 2014 06:11:05 GMT", SHA256)) 118 | ~$ curl -v -H 'Date: "Mon, 17 Feb 2014 06:11:05 GMT"' -H 'Authorization: Signature keyId="my-key",algorithm="hmac-sha256",headers="date",signature="SSS"' 119 | 120 | And for a much less painful example, check out the `httpsig package`_ documentation to use ``requests`` and ``httpsig``. 121 | 122 | 123 | .. References: 124 | .. _`HTTP Signature`: https://datatracker.ietf.org/doc/draft-cavage-http-signatures/ 125 | .. _`Django REST framework`: http://django-rest-framework.org/ 126 | .. _`HTTP Signature scheme`: http://docs.aws.amazon.com/general/latest/gr/signature-version-4.html 127 | .. _`httpsig package`: https://pypi.python.org/pypi/httpsig 128 | -------------------------------------------------------------------------------- /TODO.txt: -------------------------------------------------------------------------------- 1 | * Christopher Grebs opened up the API to use algorithms other than hmac-sha256: 2 | * Document available algorithms 3 | * Document API changes (class attribute) 4 | * expose RSA? 5 | * version, update changelog & update PyPI (in maintenance right now) 6 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /rest_framework_httpsignature/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.2.1' 2 | -------------------------------------------------------------------------------- /rest_framework_httpsignature/authentication.py: -------------------------------------------------------------------------------- 1 | from rest_framework import authentication 2 | from rest_framework import exceptions 3 | from httpsig import HeaderSigner 4 | import re 5 | 6 | 7 | class SignatureAuthentication(authentication.BaseAuthentication): 8 | 9 | SIGNATURE_RE = re.compile('signature="(.+?)"') 10 | SIGNATURE_HEADERS_RE = re.compile('headers="([\(\)\sa-z0-9-]+?)"') 11 | 12 | API_KEY_HEADER = 'X-Api-Key' 13 | ALGORITHM = 'hmac-sha256' 14 | 15 | def get_signature_from_signature_string(self, signature): 16 | """Return the signature from the signature header or None.""" 17 | match = self.SIGNATURE_RE.search(signature) 18 | if not match: 19 | return None 20 | return match.group(1) 21 | 22 | def get_headers_from_signature(self, signature): 23 | """Returns a list of headers fields to sign. 24 | 25 | According to http://tools.ietf.org/html/draft-cavage-http-signatures-03 26 | section 2.1.3, the headers are optional. If not specified, the single 27 | value of "Date" must be used. 28 | """ 29 | match = self.SIGNATURE_HEADERS_RE.search(signature) 30 | if not match: 31 | return ['date'] 32 | headers_string = match.group(1) 33 | return headers_string.split() 34 | 35 | def header_canonical(self, header_name): 36 | """Translate HTTP headers to Django header names.""" 37 | # Translate as stated in the docs: 38 | # https://docs.djangoproject.com/en/1.6/ref/request-response/#django.http.HttpRequest.META 39 | header_name = header_name.lower() 40 | if header_name == 'content-type': 41 | return 'CONTENT-TYPE' 42 | elif header_name == 'content-length': 43 | return 'CONTENT-LENGTH' 44 | return 'HTTP_%s' % header_name.replace('-', '_').upper() 45 | 46 | def build_dict_to_sign(self, request, signature_headers): 47 | """Build a dict with headers and values used in the signature. 48 | 49 | "signature_headers" is a list of lowercase header names. 50 | """ 51 | d = {} 52 | for header in signature_headers: 53 | if header == '(request-target)': 54 | continue 55 | d[header] = request.META.get(self.header_canonical(header)) 56 | return d 57 | 58 | def build_signature(self, user_api_key, user_secret, request): 59 | """Return the signature for the request.""" 60 | path = request.get_full_path() 61 | sent_signature = request.META.get( 62 | self.header_canonical('Authorization')) 63 | signature_headers = self.get_headers_from_signature(sent_signature) 64 | unsigned = self.build_dict_to_sign(request, signature_headers) 65 | 66 | # Sign string and compare. 67 | signer = HeaderSigner( 68 | key_id=user_api_key, secret=user_secret, 69 | headers=signature_headers, algorithm=self.ALGORITHM) 70 | signed = signer.sign(unsigned, method=request.method, path=path) 71 | return signed['authorization'] 72 | 73 | def fetch_user_data(self, api_key): 74 | """Retuns (User instance, API Secret) or None if api_key is bad.""" 75 | return None 76 | 77 | def authenticate(self, request): 78 | # Check for API key header. 79 | api_key_header = self.header_canonical(self.API_KEY_HEADER) 80 | api_key = request.META.get(api_key_header) 81 | if not api_key: 82 | return None 83 | 84 | # Check if request has a "Signature" request header. 85 | authorization_header = self.header_canonical('Authorization') 86 | sent_string = request.META.get(authorization_header) 87 | if not sent_string: 88 | raise exceptions.AuthenticationFailed('No signature provided') 89 | sent_signature = self.get_signature_from_signature_string(sent_string) 90 | 91 | # Fetch credentials for API key from the data store. 92 | try: 93 | user, secret = self.fetch_user_data(api_key) 94 | except TypeError: 95 | raise exceptions.AuthenticationFailed('Bad API key') 96 | 97 | # Build string to sign from "headers" part of Signature value. 98 | computed_string = self.build_signature(api_key, secret, request) 99 | computed_signature = self.get_signature_from_signature_string( 100 | computed_string) 101 | 102 | if computed_signature != sent_signature: 103 | raise exceptions.AuthenticationFailed('Bad signature') 104 | 105 | return (user, api_key) 106 | -------------------------------------------------------------------------------- /rest_framework_httpsignature/models.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etoccalino/django-rest-framework-httpsignature/03ac3c213153ae6084c84b8ff61e101798b342a4/rest_framework_httpsignature/models.py -------------------------------------------------------------------------------- /rest_framework_httpsignature/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import SimpleTestCase, TestCase, RequestFactory 2 | from django.contrib.auth import get_user_model 3 | from rest_framework_httpsignature.authentication import SignatureAuthentication 4 | from rest_framework.exceptions import AuthenticationFailed 5 | import re 6 | 7 | User = get_user_model() 8 | 9 | ENDPOINT = '/api' 10 | METHOD = 'GET' 11 | KEYID = 'some-key' 12 | SECRET = 'my secret string' 13 | SIGNATURE = 'some.signature' 14 | 15 | 16 | def build_signature(headers, key_id=KEYID, signature=SIGNATURE): 17 | """Build a signature string.""" 18 | template = ('Signature keyId="%(keyId)s",algorithm="hmac-sha256",' 19 | 'headers="%(headers)s",signature="%(signature)s"') 20 | return template % { 21 | 'keyId': key_id, 22 | 'signature': signature, 23 | 'headers': ' '.join(headers), 24 | } 25 | 26 | 27 | class HeadersUnitTestCase(SimpleTestCase): 28 | 29 | request = RequestFactory() 30 | 31 | def setUp(self): 32 | self.auth = SignatureAuthentication() 33 | 34 | def test_special_header_names(self): 35 | for special in ['Content-Type', 'CONTENT-TYPE', 'content-type']: 36 | canon = self.auth.header_canonical(special) 37 | self.assertEqual('CONTENT-TYPE', canon) 38 | 39 | for special in ['Content-Length', 'CONTENT-LENGTH', 'content-length']: 40 | canon = self.auth.header_canonical(special) 41 | self.assertEqual('CONTENT-LENGTH', canon) 42 | 43 | def test_header_names(self): 44 | headers = ['X-Api-Key', 'Authentication', 'date', 'X-Something-Else'] 45 | for header in headers: 46 | canon = self.auth.header_canonical(header) 47 | expected = 'HTTP_%s' % header.upper().replace('-', '_') 48 | self.assertEqual(expected, canon) 49 | 50 | def test_build_signature_for_date(self): 51 | req = self.request.get(ENDPOINT, {}, HTTP_X_DATE="some date") 52 | dict_to_sign = self.auth.build_dict_to_sign(req, ['date']) 53 | self.assertTrue('date' in dict_to_sign.keys()) 54 | 55 | def test_build_signature_for_date_and_other(self): 56 | req = self.request.get(ENDPOINT, {}, HTTP_X_ACCEPT="*/*", 57 | HTTP_X_DATE="some date") 58 | dict_to_sign = self.auth.build_dict_to_sign(req, ['accept', 'date']) 59 | self.assertTrue('date' in dict_to_sign.keys()) 60 | self.assertTrue('accept' in dict_to_sign.keys()) 61 | 62 | def test_build_signature_for_request_line(self): 63 | req = self.request.get(ENDPOINT, {}, HTTP_X_DATE="some date") 64 | dict_to_sign = self.auth.build_dict_to_sign( 65 | req, 66 | ['(request-target)', 'date']) 67 | self.assertTrue('date' in dict_to_sign.keys()) 68 | self.assertTrue('(request-target)' not in dict_to_sign.keys()) 69 | 70 | 71 | class SignatureTestCase(SimpleTestCase): 72 | 73 | def setUp(self): 74 | self.auth = SignatureAuthentication() 75 | 76 | def test_no_headers_in_signature_is_date(self): 77 | signature = build_signature([]) 78 | headers = self.auth.get_headers_from_signature(signature) 79 | self.assertEqual(1, len(headers)) 80 | self.assertEqual("date", headers[0]) 81 | 82 | def test_date_in_signature(self): 83 | signature = build_signature(['date']) 84 | headers = self.auth.get_headers_from_signature(signature) 85 | self.assertTrue('date' in headers) 86 | 87 | def test_many_in_signature(self): 88 | signature = build_signature(['date', 'accept', '(request-target)']) 89 | headers = self.auth.get_headers_from_signature(signature) 90 | self.assertTrue('date' in headers) 91 | self.assertTrue('accept' in headers) 92 | self.assertTrue('(request-target)' in headers) 93 | 94 | def test_get_signature(self): 95 | signature_string = build_signature(['(request-target)', 'date']) 96 | signature = self.auth.get_signature_from_signature_string( 97 | signature_string) 98 | self.assertEqual(SIGNATURE, signature) 99 | 100 | def test_get_signature_without_headers(self): 101 | signature_string = build_signature([]) 102 | signature = self.auth.get_signature_from_signature_string( 103 | signature_string) 104 | self.assertEqual(SIGNATURE, signature) 105 | 106 | 107 | class BuildSignatureTestCase(SimpleTestCase): 108 | 109 | request = RequestFactory() 110 | KEYID = 'su-key' 111 | 112 | def setUp(self): 113 | self.auth = SignatureAuthentication() 114 | 115 | def test_build_signature(self): 116 | # TO SIGN: 117 | # 118 | # GET /packages/measures/ HTTP/1.1 119 | # host: localhost:8000 120 | # accept: application/json 121 | # date: Mon, 17 Feb 2014 06:11:05 GMT 122 | 123 | headers = ['(request-target)', 'host', 'accept', 'date'] 124 | expected_signature = '+dV3yojX7N5I5J+rx0N+7kL5zES2L9Goo4ApJIn33IM=' 125 | expected_signature_string = build_signature( 126 | headers, 127 | key_id=self.KEYID, 128 | signature=expected_signature) 129 | 130 | req = RequestFactory().get( 131 | '/packages/measures/', {}, 132 | HTTP_HOST='localhost:8000', 133 | HTTP_ACCEPT='application/json', 134 | HTTP_DATE='Mon, 17 Feb 2014 06:11:05 GMT', 135 | HTTP_AUTHORIZATION=expected_signature_string) 136 | 137 | signature_string = self.auth.build_signature( 138 | self.KEYID, SECRET, req) 139 | signature = re.match( 140 | '.*signature="(.+)",?.*', signature_string).group(1) 141 | self.assertEqual(expected_signature, signature) 142 | 143 | 144 | class SignatureAuthenticationTestCase(TestCase): 145 | 146 | class APISignatureAuthentication(SignatureAuthentication): 147 | """Extend the SignatureAuthentication to test it.""" 148 | 149 | API_KEY_HEADER = 'X-Api-Key' 150 | 151 | def __init__(self, user): 152 | self.user = user 153 | 154 | def fetch_user_data(self, api_key): 155 | if api_key != KEYID: 156 | return None 157 | 158 | return (self.user, SECRET) 159 | 160 | TEST_USERNAME = 'test-user' 161 | TEST_PASSWORD = 'test-password' 162 | 163 | def setUp(self): 164 | self.test_user = User(username=self.TEST_USERNAME) 165 | self.test_user.set_password(self.TEST_PASSWORD) 166 | self.auth = self.APISignatureAuthentication(self.test_user) 167 | 168 | def test_no_credentials(self): 169 | request = RequestFactory().get(ENDPOINT) 170 | res = self.auth.authenticate(request) 171 | self.assertIsNone(res) 172 | 173 | def test_only_api_key(self): 174 | request = RequestFactory().get( 175 | ENDPOINT, {}, 176 | HTTP_X_API_KEY=KEYID) 177 | self.assertRaises(AuthenticationFailed, 178 | self.auth.authenticate, request) 179 | 180 | def test_bad_signature(self): 181 | request = RequestFactory().get( 182 | ENDPOINT, {}, 183 | HTTP_X_API_KEY=KEYID, 184 | HTTP_AUTHORIZATION='some-wrong-value') 185 | self.assertRaises(AuthenticationFailed, 186 | self.auth.authenticate, request) 187 | 188 | def test_can_authenticate(self): 189 | headers = ['(request-target)', 'accept', 'date', 'host'] 190 | expected_signature = 'SelruOP39OWoJrSopfYJ99zOLoswmpyGXyDPdebeELc=' 191 | expected_signature_string = build_signature( 192 | headers, 193 | key_id=KEYID, 194 | signature=expected_signature) 195 | request = RequestFactory().get( 196 | '/packages/measures/', {}, 197 | HTTP_ACCEPT='application/json', 198 | HTTP_DATE='Mon, 17 Feb 2014 06:11:05 GMT', 199 | HTTP_HOST='localhost:8000', 200 | HTTP_AUTHORIZATION=expected_signature_string, 201 | HTTP_X_API_KEY=KEYID) 202 | 203 | result = self.auth.authenticate(request) 204 | self.assertIsNotNone(result) 205 | self.assertEqual(result[0], self.test_user) 206 | self.assertEqual(result[1], KEYID) 207 | -------------------------------------------------------------------------------- /rest_framework_httpsignature/views.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etoccalino/django-rest-framework-httpsignature/03ac3c213153ae6084c84b8ff61e101798b342a4/rest_framework_httpsignature/views.py -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | 4 | setup( 5 | name='djangorestframework-httpsignature', 6 | version='1.0.0', 7 | url='https://github.com/etoccalino/django-rest-framework-httpsignature', 8 | 9 | license='LICENSE.txt', 10 | description='HTTP Signature support for Django REST framework', 11 | long_description=open('README.rst').read(), 12 | 13 | install_requires=[ 14 | 'Django>=1.6.2', 15 | 'djangorestframework>=2.3.14', 16 | 'httpsig', 17 | ], 18 | author='Elvio Toccalino', 19 | author_email='me@etoccalino.com', 20 | packages=['rest_framework_httpsignature'], 21 | classifiers=[ 22 | 'Intended Audience :: Developers', 23 | 'Development Status :: 5 - Production/Stable', 24 | 'Environment :: Web Environment', 25 | 'Framework :: Django', 26 | 'Operating System :: OS Independent', 27 | 'Programming Language :: Python :: 2.7', 28 | 'License :: OSI Approved :: MIT License', 29 | 'Topic :: Internet :: WWW/HTTP', 30 | 'Topic :: Security', 31 | ] 32 | ) 33 | -------------------------------------------------------------------------------- /test_settings.py: -------------------------------------------------------------------------------- 1 | DATABASES = { 2 | 'default': { 3 | 'ENGINE': 'django.db.backends.sqlite3', 4 | 'NAME': ':memory:', 5 | }, 6 | } 7 | 8 | INSTALLED_APPS = ( 9 | 'django.contrib.contenttypes', 10 | 'django.contrib.auth', 11 | 'rest_framework', 12 | 'rest_framework_httpsignature', 13 | ) 14 | 15 | ROOT_URLCONF = 'rest_framework_httpsignature.tests' 16 | 17 | SECRET_KEY = 'MY PRIVATE SECRET' 18 | -------------------------------------------------------------------------------- /utils/sign3.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import hashlib 3 | import base64 4 | import hmac 5 | 6 | 7 | def parse_args(): 8 | """Parse program arguments into an object.""" 9 | parser = argparse.ArgumentParser() 10 | parser.add_argument('key', help='Key to sign with') 11 | parser.add_argument('key_id', help='an ID of the key in use') 12 | return parser.parse_args() 13 | 14 | 15 | def string_to_sign(): 16 | return '\n'.join([ 17 | '(request-target): get /packages/measures/', 18 | 'accept: application/json', 19 | 'date: Mon, 17 Feb 2014 06:11:05 GMT', 20 | 'host: localhost:8000']) 21 | 22 | 23 | def raw_sign(message, secret): 24 | """Sign a message.""" 25 | digest = hmac.new(secret, message, hashlib.sha256).digest() 26 | return base64.b64encode(digest) 27 | 28 | 29 | def http_signature(message, key_id, signature): 30 | """Return a tuple (message signature, HTTP header message signature).""" 31 | template = ('Signature keyId="%(keyId)s",algorithm="hmac-sha256",' 32 | 'headers="%(headers)s",signature="%(signature)s"') 33 | headers = ['(request-target)', 'host', 'accept', 'date'] 34 | return template % { 35 | 'keyId': key_id, 36 | 'signature': signature, 37 | 'headers': ' '.join(headers), 38 | } 39 | 40 | 41 | if __name__ == '__main__': 42 | args = parse_args() 43 | message = string_to_sign() 44 | signature = raw_sign(message, args.key) 45 | header = http_signature(message, args.key_id, signature) 46 | print "=== SIGNATURE ===" 47 | print signature 48 | print "=== HTTP HEADER SIGNATURE ===" 49 | print header 50 | -------------------------------------------------------------------------------- /utils/test-install.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | # 4 | # A simple & quick script to test that a local development source distribution 5 | # can be successfully installed in a fresh virtual environment via pip. 6 | # 7 | 8 | 9 | REPO_DIR="$HOME/programming/django-rest-framework-httpsignature" 10 | VIRTUALENV_DIR="${REPO_DIR}/env" 11 | DEST_DIR="$HOME/tmp/t" 12 | 13 | set -e 14 | set -x 15 | 16 | # Build the source distribution 17 | cd ${REPO_DIR} 18 | source ${VIRTUALENV_DIR}/bin/activate 19 | rm -rf dist/ 20 | python setup.py build sdist 21 | 22 | # Move to the distination directory, create the test environment and instal. 23 | mkdir ${DEST_DIR} 24 | cd ${DEST_DIR} 25 | virtualenv . 26 | source bin/activate 27 | 28 | pip install -U pip 29 | pip install ${REPO_DIR}/dist/* 30 | 31 | # The installation was successful. Destroy the test environment. 32 | cd ${REPO_DIR} 33 | rm -fr ${DEST_DIR} 34 | --------------------------------------------------------------------------------