Skip to content

Adding a middleware that ensures api key authentication. #8

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,6 @@ ENV/

# Rope project settings
.ropeproject

# PyCharm files
.idea
2 changes: 1 addition & 1 deletion rest_framework_api_key/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.0.3"
__version__ = "0.0.4"
4 changes: 4 additions & 0 deletions rest_framework_api_key/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,7 @@

def generate_key():
return binascii.hexlify(os.urandom(20)).decode()


def get_key_from_headers(request):
return request.META.get('HTTP_API_KEY', '')
59 changes: 59 additions & 0 deletions rest_framework_api_key/middleware.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""
Middleware verifying every request to the server passes the API key validation.
"""
from django.conf import settings
from django.core.exceptions import PermissionDenied

from rest_framework_api_key.helpers import get_key_from_headers
from rest_framework_api_key.models import APIKey


# Loaded dynamically to handle cases where the settings variable is not defined.
excluded_prefixes = getattr(settings, 'API_KEY_MIDDLEWARE_EXCLUDED_URL_PREFIXES', ())


class APIKeyMiddleware(object):
"""
A custom middleware to provide API key validation for all requests.
"""

def __init__(self, get_response):
"""
The middleware initialization method.

:param get_response: The response rendering view.
:type get_response: function
"""
self.get_response = get_response

def is_key_valid(self, api_key):
"""
A wrapper function around api key validation, to make the
process more generic and easier to mock.

:param api_key: The api key value from the request.
:type api_key: str
:return: Whether the key has been registered.
:rtype: bool
"""
return APIKey.is_valid(api_key)

def __call__(self, request):
"""
Middleware processing method, API key validation happens here.

:param request: The HTTP request.
:type request: :class:`django.http.HttpRequest`
:returns: The HTTP response.
:rtype: :class:`django.http.HttpResponse`
"""
api_key = get_key_from_headers(request)
api_key_object = APIKey.objects.filter(key=api_key).first()

is_valid = (
self.is_key_valid(api_key) or
request.path.startswith(excluded_prefixes))
if not is_valid:
raise PermissionDenied('API key missing or invalid.')
request.api_key = api_key_object
return self.get_response(request)
4 changes: 4 additions & 0 deletions rest_framework_api_key/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,7 @@ class Meta:

def __str__(self):
return self.name

@classmethod
def is_valid(cls, api_key):
return cls.objects.filter(key=api_key).exists()
6 changes: 4 additions & 2 deletions rest_framework_api_key/permissions.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from rest_framework import permissions
from rest_framework_api_key.models import APIKey

from rest_framework_api_key.helpers import get_key_from_headers


class HasAPIAccess(permissions.BasePermission):
message = 'Invalid or missing API Key.'

def has_permission(self, request, view):
api_key = request.META.get('HTTP_API_KEY', '')
return APIKey.objects.filter(key=api_key).exists()
api_key = get_key_from_headers(request)
return APIKey.is_valid(api_key)
6 changes: 5 additions & 1 deletion tests/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
)
}

MIDDLEWARE_CLASSES = (
MIDDLEWARE = (
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
Expand Down Expand Up @@ -63,3 +63,7 @@
},
},
]

API_KEY_MIDDLEWARE_EXCLUDED_URL_PREFIXES = (
'/admin',
)
55 changes: 55 additions & 0 deletions tests/test_middleware.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from django.core.urlresolvers import reverse
from django.test import override_settings, modify_settings

from tests.test_admin import APIAuthenticatedTestCase


class APIMiddlewareTest(APIAuthenticatedTestCase):
"""
Test authentication using API key middleware.
"""

@override_settings(REST_FRAMEWORK={
'DEFAULT_PERMISSION_CLASSES':
('rest_framework.permissions.AllowAny',),
})
@modify_settings(MIDDLEWARE={
'append': 'rest_framework_api_key.middleware.APIKeyMiddleware'
})
def test_get_view_authorized(self):
"""
Test successful authentication.
"""
response = self.client.get(reverse("test-view"), **self.header)

self.assertEqual(response.status_code, 200)
self.assertEqual(response.data["msg"], "Hello World!")

@override_settings(REST_FRAMEWORK={
'DEFAULT_PERMISSION_CLASSES':
('rest_framework.permissions.AllowAny',),
})
@modify_settings(MIDDLEWARE={
'append': 'rest_framework_api_key.middleware.APIKeyMiddleware'
})
def test_get_view_unauthorized(self):
"""
Test failed authentication.
"""
response = self.client.get(reverse("test-view"))

self.assertEqual(response.status_code, 403)

@override_settings(REST_FRAMEWORK={
'DEFAULT_PERMISSION_CLASSES':
('rest_framework.permissions.AllowAny',),
})
@modify_settings(MIDDLEWARE={
'append': 'rest_framework_api_key.middleware.APIKeyMiddleware'
})
def test_get_view_excluded(self):
"""
Test not required for paths excluded in settings.
"""
response = self.client.get(reverse("admin:index"))
self.assertNotEqual(response.status_code, 403)