Skip to content
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

Add cookie-based authentication feature #277

Open
wants to merge 1 commit into
base: develop
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
15 changes: 15 additions & 0 deletions docs/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,15 @@ The default is `knox.AuthToken`
## TOKEN_PREFIX
This is the prefix for the generated token that is used in the Authorization header. The default is just an empty string.
It can be up to `CONSTANTS.MAXIMUM_TOKEN_PREFIX_LENGTH` long.
## ENABLE_COOKIE_AUTH
This defines if the authentication will be done using a `HTTP-only` Cookie.
The default is `False`
## AUTH_COOKIE_SALT
This is the Salt `(String)` that is added to better store and secure token value inside Cookies
the default value is `"knox"` and it can be up to `CONSTANTS.MAXIMUM_SALT_LENGTH` long.
## AUTH_COOKIE_KEY
This is the `Cookie` key of the token. The default is `knox`.
It can be up to `CONSTANTS.MAXIMUM_COOKIE_KEY_LENGTH` long.

# Constants `knox.settings`
Knox also provides some constants for information. These must not be changed in
Expand All @@ -118,3 +127,9 @@ This is the length of the digest that will be stored in the database for each to

## MAXIMUM_TOKEN_PREFIX_LENGTH
This is the maximum length of the token prefix.

## MAXIMUM_COOKIE_KEY_LENGTH
This is the maximum length of the cookie `key`

## MAXIMUM_SALT_LENGTH
This is the maximum length of Salt `(String)` that encode token value inside the cookie.
18 changes: 17 additions & 1 deletion knox/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,24 @@ class TokenAuthentication(BaseAuthentication):
- `request.user` will be a django `User` instance
- `request.auth` will be an `AuthToken` instance
'''

def authenticate_through_cookie(self, request):
prefix = knox_settings.AUTH_COOKIE_KEY
if prefix in request.COOKIES:
auth =request.get_signed_cookie(prefix,False,salt=knox_settings.AUTH_COOKIE_SALT)
if not auth or len(auth) == 0:
msg = _('Failed to get token value from cookies')
raise exceptions.AuthenticationFailed(msg)
user, auth_token = self.authenticate_credentials(auth.encode())
return (user, auth_token)
else :
msg = _('No credentials provided through cookies')
raise exceptions.AuthenticationFailed(msg)

def authenticate(self, request):

if knox_settings.ENABLE_COOKIE_AUTH:
return self.authenticate_through_cookie(request)

auth = get_authorization_header(request).split()
prefix = knox_settings.AUTH_HEADER_PREFIX.encode()

Expand Down
9 changes: 9 additions & 0 deletions knox/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
'EXPIRY_DATETIME_FORMAT': api_settings.DATETIME_FORMAT,
'TOKEN_MODEL': getattr(settings, 'KNOX_TOKEN_MODEL', 'knox.AuthToken'),
'TOKEN_PREFIX': '',
'ENABLE_COOKIE_AUTH':False,
'AUTH_COOKIE_SALT':"knox",
'AUTH_COOKIE_KEY': 'knox',
}

IMPORT_STRINGS = {
Expand All @@ -35,6 +38,10 @@ def reload_api_settings(*args, **kwargs):
knox_settings = APISettings(value, DEFAULTS, IMPORT_STRINGS)
if len(knox_settings.TOKEN_PREFIX) > CONSTANTS.MAXIMUM_TOKEN_PREFIX_LENGTH:
raise ValueError("Illegal TOKEN_PREFIX length")
if len(knox_settings.AUTH_COOKIE_KEY) > CONSTANTS.MAXIMUM_COOKIE_KEY_LENGTH:
raise ValueError("unauthorized COOKIE_KEY length")
if len(knox_settings.AUTH_COOKIE_SALT) > CONSTANTS.MAXIMUM_SALT_LENGTH:
raise ValueError("unauthorized COOKIE_SALT length")


setting_changed.connect(reload_api_settings)
Expand All @@ -47,6 +54,8 @@ class CONSTANTS:
TOKEN_KEY_LENGTH = 15
DIGEST_LENGTH = 128
MAXIMUM_TOKEN_PREFIX_LENGTH = 10
MAXIMUM_SALT_LENGTH=10
MAXIMUM_COOKIE_KEY_LENGTH=8

def __setattr__(self, *args, **kwargs):
raise Exception('''
Expand Down
35 changes: 31 additions & 4 deletions knox/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ def get_user_serializer_class(self):

def get_expiry_datetime_format(self):
return knox_settings.EXPIRY_DATETIME_FORMAT
def get_cookie_auth_status(self):
return knox_settings.ENABLE_COOKIE_AUTH

def get_cookie_salt(self):
return knox_settings.AUTH_COOKIE_SALT

def get_cookie_key(self):
return knox_settings.AUTH_COOKIE_KEY

def format_expiry_datetime(self, expiry):
datetime_format = self.get_expiry_datetime_format()
Expand Down Expand Up @@ -72,18 +80,27 @@ def post(self, request, format=None):
user_logged_in.send(sender=request.user.__class__,
request=request, user=request.user)
data = self.get_post_response_data(request, token, instance)
return Response(data)

response=Response(data)
if self.get_cookie_auth_status():
response.set_signed_cookie(self.get_cookie_key(), token, httponly=True,salt=self.get_cookie_salt())
return response

class LogoutView(APIView):
authentication_classes = (TokenAuthentication,)
permission_classes = (IsAuthenticated,)
def get_cookie_auth_status(self):
return knox_settings.ENABLE_COOKIE_AUTH
def get_cookie_key(self):
return knox_settings.AUTH_COOKIE_KEY

def post(self, request, format=None):
request._auth.delete()
user_logged_out.send(sender=request.user.__class__,
request=request, user=request.user)
return Response(None, status=status.HTTP_204_NO_CONTENT)
response=Response(None, status=status.HTTP_204_NO_CONTENT)
if self.get_cookie_auth_status():
response.delete_cookie(self.get_cookie_key())
return response


class LogoutAllView(APIView):
Expand All @@ -94,8 +111,18 @@ class LogoutAllView(APIView):
authentication_classes = (TokenAuthentication,)
permission_classes = (IsAuthenticated,)

def get_cookie_auth_status(self):
return knox_settings.ENABLE_COOKIE_AUTH

def get_cookie_key(self):
return knox_settings.AUTH_COOKIE_KEY

def post(self, request, format=None):
request.user.auth_token_set.all().delete()
user_logged_out.send(sender=request.user.__class__,
request=request, user=request.user)
return Response(None, status=status.HTTP_204_NO_CONTENT)
response=Response(None, status=status.HTTP_204_NO_CONTENT)
if self.get_cookie_auth_status():
response.delete_cookie(self.get_cookie_key())
return response

55 changes: 55 additions & 0 deletions tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ def get_basic_auth_header(username, password):
token_prefix_too_long_knox = knox_settings.defaults.copy()
token_prefix_too_long_knox["TOKEN_PREFIX"] = token_prefix_too_long

cookie_key='cookie'
cookie_key_knox=knox_settings.defaults.copy()
cookie_key_knox['AUTH_COOKIE_KEY']=cookie_key

class AuthTestCase(TestCase):

def setUp(self):
Expand Down Expand Up @@ -488,3 +492,54 @@ def test_tokens_created_before_prefix_still_work(self):
response = self.client.get(root_url, {}, format='json')
self.assertEqual(response.status_code, 200)
reload_module(views)
def test_login_create_token_cookie(self):
with override_settings(REST_KNOX=cookie_key_knox):
reload_module(auth)
reload_module(views)
self.assertEqual(AuthToken.objects.count(), 0)
url = reverse('knox_login')
self.client.credentials(
HTTP_AUTHORIZATION=get_basic_auth_header(self.username, self.password)
)
response = self.client.post(url, {}, format='json')
self.assertEqual(response.status_code, 200)
self.assertIn(cookie_key_knox["AUTH_COOKIE_KEY"], self.client.cookies.keys())
reload_module(auth)
response = self.client.get(root_url, {}, format='json')
self.assertEqual(response.status_code, 200)
reload_module(views)

def test_logout_deletes_token_cookie(self):
with override_settings(REST_KNOX=cookie_key_knox):
reload_module(auth)
reload_module(views)
self.assertEqual(AuthToken.objects.count(), 0)
for _ in range(2):
instance, token = AuthToken.objects.create(user=self.user)
self.assertEqual(AuthToken.objects.count(), 2)

url = reverse('knox_logout')

self.client.post(url, {}, format='json')
self.assertEqual(AuthToken.objects.count(), 1,
'other tokens should remain after logout')
self.assertNotIn(cookie_key_knox["AUTH_COOKIE_KEY"], self.client.cookies.keys())
reload_module(auth)
reload_module(views)

def test_logout_all_deletes_token_cookie(self):
with override_settings(REST_KNOX=cookie_key_knox):
reload_module(auth)
reload_module(views)
self.assertEqual(AuthToken.objects.count(), 0)
for _ in range(10):
instance, token = AuthToken.objects.create(user=self.user)
self.assertEqual(AuthToken.objects.count(), 10)

url = reverse('knox_logoutall')
self.client.credentials(HTTP_AUTHORIZATION=('Token %s' % token))
self.client.post(url, {}, format='json')
self.assertEqual(AuthToken.objects.count(), 0)
self.assertNotIn(cookie_key_knox["AUTH_COOKIE_KEY"], self.client.cookies.keys())
reload_module(auth)
reload_module(views)